Sometimes a lower-layer component needs to invoke a service on a higher-layer component. Consider, for example, a timer component (T) that periodically calls a handler function in a user-interface component (U). Component T is probably part of the OS kernel and thus clearly “lower” than component U.
In this setting, there is an upward dependency from T to U; such upward dependencies are undesirable, at least if they are bound at compile-time. Implemented naively, there is a hard-coded call to the UI component like this:
1 2 3 4 5 6 7 8 9 10 11 |
// file: OS/Timer.c #include "UI/UIManager.h" // Bad! ... static void OSTimerNotify(void) { ... UIManagerNotify10msTimeout(); // Bad! ... } |
Dependency lines that point up in a component diagram are not just ugly: they denote that the lower-layer component cannot be independently reused and tested.
The classic dependency inversion principle (DIP) is usually applied to solve this problem: instead of having a hard-coded function call in the timer to the handling component, the timer calls back on a function pointer that is set to the timer-handling routine in the initialization code of the higher-layer component:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
// file: UI/UIManager.c #include "OS/Timer.h" static void UIManager10msTimout(void) { // Do something ever 10 ms. } void UIManagerInit(void) { ... OSTimerSet10msCallback(&UIMananager10msTimeout); ... } // file: OS/Timer.h typedef void (*TIMER_10MS_CALLBACK)(void); // file: OS/Timer.c #include "OS/Timer.h" static TIMER_10MS_CALLBACK s_10msCallback; void OSTimerSet10msCallback(TIMER_10MS_CALLBACK callback) { s_10msCallback = callback; } static void OSTimerNotify(void) { ... if (s_10msCallback != NULL) (*s_10msCallback)(); ... } |
Note that there is still a T to U dependency, but now this dependency is only present at run-time, which is OK, as this doesn’t hinder reuse and testability. The U to T compile-time dependency is quite natural and doesn’t violate any design principles. So, the undesirable compile-time dependency has been successfully inverted. The classic DIP recipe looks like this:
1. In T export a callback interface
2. In U implement the callback interface
3. In U (or some init/startup code) register the implementation with T
4. In T call back on the interface
When you are working in a constrained environment like embedded systems, you often cannot afford the memory and performance overhead that accompanies such late (run-time) binding, so you might try what I call the “Poor Man’s DIP”: simply export a “callback interface” as a function prototype and “implement” it by defining the function in the upper-layer component:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// file: UI/UIManager.c #include "OS/Timer.h" // Implementation of the call-back interface. void OSTimerNotify10msTimeout(void) { // Do something ever 10 ms. } // file: OS/Timer.h // Call-back interface. extern void OSTimerNotify10msTimeout(void); // file: OS/Timer.c #include "OS/Timer.h" static void OSTimerNotify(void) { ... OSTimerNotify10msTimeout(); ... } |
This pattern gives you most of the advantages of the classical (run-time bound) DIP but doesn’t incur any overhead. It can (and should) be applied whenever there is a dependency from a lower-layer component to an upper-layer component that doesn’t need to change at run-time but stays fixed throughout the lifetime of the application.