In this post, we will see how code modularity and layered-ness can be exploited neatly to insert more functionality as an intervening 'Shim' layer between two existing layers. It will be like inserting a new card in an existing deck of cards.
Before you read any further, please make sure you have skimmed through my previous article where we saw how to design layered modules. Note that less emphasis was placed on the interface that connects the application to the module in the previous post. To recapitulate, Modular and Layered Software Architectures rely heavily on how the interfaces are specified and implemented. One of the most important premises while writing modular code is that the participating modules or applications should be agnostic to the implementational details on the other end (Please refer to the figure below to see what we mean by 'Module' and 'Application'). This means - Once the Module has been designed and the Application starts using it, changes in either of them should not require changes on the other end of the interface. For example, if something changes in the Module, it must not affect the Application.
The biggest advantage of this type of design is that it makes the code scalable. There can be a complete change in the code of the Module but it does not involve parallel changes in the Application and vice versa. As a result, the software life expectancy increases. Also, this allows multiple folks to work on the same project in a more effective manner, as the Application developer need not know what's going on in the Module and the Module developer does not have to care about who or which application drives it. The end goal of such a design is to make plug and play modules which are what managers expect us to build when they blabber jargon related to modularity, scalability, genericity etcetera. :-P
Once the interface is specified, the other end is supposed to be like a magic black box. Turn the right levers as specified in the interface specification and everything works like magic!
So here I would take the liberty of introducing the concept of a 'Shim' Layer. Ta Da! What exactly is this Shim Layer? Wikipedia defines a Shim Layer as follows:
"In computer programming, a shim is a small library that transparently intercepts an API, changing the parameters passed, handling the operation itself, or redirecting the operation elsewhere. Shims typically come about when the behaviour of an API changes, thereby causing compatibility issues for older applications that still rely on the older functionality. In these cases, the older API can still be supported by a thin compatibility layer on top of the newer code. Shims can also be used to run programs on different software platforms than they were developed for."
The definition is quite complete by itself. But the functionality is not. Shim Layers are used to interface applications to modules. But then, they might also be thought of as third party applications which connect the main Application to a number of Modules and the main Application remains agnostic of this fact! Deceit! Sounds crazy?
Let's take the same example from the previous tutorial of the Module that manages TCP connections, sending and receiving of packets on the connections for the Application. But this time the Application doesn't care about the underlying protocol - whether it's over TCP, UDP, via HTTP curl requests or MQTT. All it cares about is sending and receiving packets using an API. This is where the functionality of the Shim Layer jumps in. The situation is illustrated in the figure below:
Here, the 'Shim' layer provides two kinds of functionalities based on the needs of the Application:
- The Shim Layer in itself takes care of selection of the appropriate module, i.e. which underlying module to use when the Application requests service based on some configuration. In the figure shown above, the Application would just call a function in the Shim Layer to send data via an API and the Shim Layer decides on how to send the packet - use TCP or UDP sockets or simply use cURL requests or use MQTT. The application has delegated the task to the Shim layer. This makes the Application agnostic to the underlying module(s) and its (their) dependencies.
- Another use case would be where the Application sends in additional arguments to the Shim layer to select a particular module explicitly. In the example above, the Application might want to select MQTT or cURL to send data and the Shim Layer would broker the interaction between the appropriate module and the Application.
Another good example of using the Shim layer as an interface layer is when a particular Application is re-used over multiple platforms. For example, let us take the case where the same application needs to be supported over different micro-controller families. In such cases, the Shim layer acts as a request translator. For any platform related calls, the Application would always call a fixed function in the Shim layer. It is the Shim layer that would re-direct the call to the micro-controller family that is present, based on the configuration provided upfront. Let's look at the code where the Application wants to initialize UART and the same is to be used over two platforms M3 and M4. The Shim layer function could look like the following:
device_shim_interface_return_e device_shim_interface_init_uart ( device_shim_interface_uart_init_req_s * device_shim_interface_uart_init_req ) { /* Local Variables */ device_shim_interface_return_e api_status = DEVICE_INTERFACE_SUCCESS; /*!> The status of the API */ /* Validation */ if ( NULL == device_shim_interface_uart_init_req ) { CRITICAL_MSG ( device_log, "device_shim_interface_uart_init_req is NULL"); return ( DEVICE_INTERFACE_NULL_POINTER ); } /* Algorithm */ // Check if the platform is M3 #ifdef PLATFORM_IS_M3 // api_status = Call the M3 device drivers Module with params to initialize UART #endif /* PLATFORM_IS_M3 */ #ifdef PLATFORM_IS_M4 // api_status = Call the M4 device drivers Module with params to initialize UART #endif /* PLATFORM_IS_M4 */ return ( api_status ); }/* device_shim_interface_init_uart */
Hence, when the Application calls the Shim layer function device_shim_interface_init_uart()
, the application has no idea if the platform underneath is M3 or M4. It just populates the structure device_shim_interface_uart_init_req
with all parameters and passes it on. Now the Shim layer function device_shim_interface_init_uart()
, based on the definition of the macros PLATFORM_IS_M3
and PLATFORM_IS_M4
would redirect the call to either the M3 device drivers module and initialize the UART on M3 or the M4 device drivers module and initialize UART on M4 respectively. Obviously, there needs to be a check that both M3 and M4 macros are not defined simultaneously. One way of avoiding such conditions is explained in this post. There is also a good chance that in some cases the application would pass parameters that are used only by some modules and are not used by others! In such case, the Shim layer may expose a union of all values and selectively filter out the parameters on a per-platform basis.
Let's see this example Visually:
The biggest and most relevant point to note here is that the Application should never have access to any of the resources and code of the modules or else, the entire design of the Shim layer would break. It should only be calling the Shim layer and accessing the Shim Layer resources. This cannot be ensured procedural languages like C, and one has to stick to the design rules discussed offline. In languages that natively support the concepts of Object Orientation, this can be done.
So, I hope that you can now appreciate the kind of scalability and modularity a Shim layer may bring to the code design. Most importantly, it increases the life expectancy of the software as a whole.