One fact you can’t escape from: some parts of your system are always going to be specific to the underlying system, whether it’s the hardware setup, an OS, a given processor, a particular library, or an external system’s interface. For a specific example, at some level, you do need to declare an IMU driver for a specific part. But most of your system probably doesn’t care about the specific IMU being used: it simply needs IMU samples provided within a specified time frame and in a specific data format. From this perspective, you can switch the IMU as needed as long as you can satisfy your program’s expectations.
Your goal is to isolate these types of dependencies. You can do this by using abstract interfaces to hide the implementation details so that the rest of the program can be more generic and not worry about these details (i.e., write code against YOUR abstractions, not theirs). By interacting with abstract interfaces, other modules can remain loosely coupled. This ensures you can easily modify and swap out the implementations of those interfaces without needing to change other parts of our system. If we are directly interacting with the implementations, our code has a higher chance of becoming tightly coupled to the implementation, especially if the implementation provides capabilities that are not exposed in the abstract interface.
This even applies to our IMU driver example. It likely needs to communicate over I2C or SPI – but it should not be tightly coupled to the processor’s specific driver for that protocol. Instead, there should be an abstraction that sits between them so the IMU driver is independent of the processor it runs on
This idea is formally known as the “dependency inversion principle”, which states that:
- High-level modules should not depend on low-level modules. Both should depend on abstractions and interfaces
- Abstractions and interfaces should not depend on details. Details (concrete implementations) should depend on abstractions.
The reason for the name is that the principle dictates that both high-level and low-level objects must depend on the same abstraction – inverting the way that most people naturally think about software, where high-level modules depend on low-level modules.
When this principle is applied to source code, it results in the dependency inversion pattern.
References
- Wikipedia: Dependency inversion principle
- C2 Wiki: Dependency Inversion Principle
A. High level modules should not depend upon low level modules. Both should depend upon abstractions.
B. Abstractions should not depend upon details. Details should depend upon abstractions.We wish to avoid designs which are:
- Rigid (Hard to change due to dependencies. Especially since dependencies are transitive.)
- Fragile (Changes cause unexpected bugs.)
- Immobile (Difficult to reuse due to implicit dependence on current application code.)
- Playbook: Writing Portable Embedded Software
Portable code must be kept “generic” by using these generic interfaces, standard language features, and portable libraries. Portable code should never directly reference any platform-specific modules or details. This includes vendor SDK driver interfaces, RTOS-specific APIs, direct register accesses, and platform-specific debugging functionality. These items prevent us from easily porting our code from one platform to another and belong behind a barrier of some kind.
- Use of Abstract Interfaces in the Development of Software for Embedded Computer Systems by David Parnas
In summary, the procedure that we are discussing can be formulated in yet another way:
- Specify an Abstract interface embodying all the information shared by all of the possible interfaces
- Procure programs to meet this abstract interface
- Procure additional programs in order to meet the actual interface.
« Back to Glossary Index
