Code Design in Embedded Systems: Achieving Flexible and Scalable Code

In the previous discussion, Ma Yinan delved deeply into architectural design with the master and then immersed himself in the development of a brand new embedded system.In the busy days, a lingering doubt remained in his heart: How can the new system flexibly respond to the differentiated needs of various provinces across the country with a single codebase?The “componentization” concept mentioned by the master requires that these components be extensible, replaceable, and composable to achieve a high degree of scalability and configurability.These ideas echoed in his mind, but he struggled to realize this concept.Thus, he decided to visit the master again for guidance.

Ma Yinan: Master, I’m here to bother you again.

Master: Haha, welcome, little Ma. You don’t come to the temple without a reason; do you have any new progress to share with me?

Ma Yinan: Master, you see through me. The project has been progressing smoothly recently, but I still have some specific implementation questions regarding the extensible, replaceable, and composable architectural design you mentioned last time.

Master: What are your thoughts?

Ma Yinan: It would be great to achieve such extensibility. However, I have only seen such ideas in books and have never seen any project achieve them. So, this question has been troubling me. Can this kind of extensibility be realized? How can it be achieved?

Master: Very well, you’ve asked a very pertinent question. To achieve an extensible and replaceable design, you must understand the SOLID principles.

Ma Yinan: You mean the SOLID principles proposed by Uncle Bob, which are Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion?

Master: Yes. Uncle Bob summarized several important design principles together; they are very classic.

Ma Yinan: To be honest, I have read these principles more than once. I recognize the words and seem to understand them, but I just don’t know how to implement them.

Master: These principles are like martial arts secrets; you need a certain foundation to master them. But I believe you have the foundation; you just need a string to string these pearls together. Once you succeed in stringing them together, you will feel as if you have opened the meridians.

Ma Yinan: How can I string them together?

Master: I suggest you first read this blog: “After so many years of coding, do you really understand SOLID?” It explains the SOLID principles and the relationships between them in detail, and it also includes example code.

Ma Yinan: Okay, I will study it first.

Master: Coincidentally, I need to brew a cup of coffee. Would you like a cup too?

Ma Yinan: Sure, thank you, Master!

Wait a moment

Ma Yinan: I have read that blog, and for the first time, I truly understand the relationships between the SOLID principles; it is indeed profound! However, I am still a bit confused about how to apply these principles in actual code.

Master: No problem. We can start from your goal. What is your goal?

Ma Yinan: My goal is to achieve components that can be extended and replaced, thus realizing high scalability and configurability.

Master: Which principle do you think best reflects your goal?

Ma Yinan: I think it should be the Open/Closed Principle. The Open/Closed Principle states that software entities should be open for extension but closed for modification. It means that you can extend it directly without modifying it.

Master: Yes, you can change the behavior of software through extension rather than modification.

Ma Yinan: Although I agree with this, I find it a bit abstract.

Master: For example, a USB port can be extended; you can plug in any USB device without making any modifications to accept a new device. Therefore, for USB devices, your computer with a USB interface is open for extension and closed for modification.

Ma Yinan: So, when software needs to change, we can implement new functionality by adding new code rather than modifying existing code. It’s like building with blocks; you can keep adding new blocks to expand the structure without destroying the already built parts.

Master: Yes. On one hand, as you said, you can add new code to increase new functionality. On the other hand, you can also extend by replacing.

Ma Yinan: How to understand that?

Master: Just like when your mechanical hard drive is too slow, you replace it with a solid-state drive. If your laptop screen is too small, you can connect a larger monitor. In programming, we can achieve the Open/Closed Principle through abstraction and inheritance. For example, you can define a base class or interface and then create different subclasses or implementation classes to extend functionality.

Ma Yinan: In my system, for the same collection task, if some provinces are different, I can use a new implementation to replace the standard implementation? Is this the Strategy Pattern?

Master: The replacement method usually involves the Strategy Pattern. The Strategy Pattern is a behavioral design pattern that allows you to change the behavior of an object at runtime. You can define a series of algorithms or business processing logic and encapsulate each algorithm so that they can be interchanged with one another. In this way, you can choose different strategies based on different contexts to meet business needs.

For example, suppose your system needs to handle payment methods based on different provinces. You can define a payment strategy interface and implement specific payment strategies for each province. Then, at runtime, you can choose the corresponding payment strategy based on the province information for processing.

Ma Yinan: I understand. Isn’t that just if/else? In our old system, there are too many and too deep if/else statements, which makes it difficult to maintain.

Master: You are right; too many if/else statements can indeed lead to code that is difficult to maintain. However, the Strategy Pattern is not simply about using if/else to make judgments. The Strategy Pattern usually only involves if/else judgments in a few places, or even no judgments at all. It typically selects strategies dynamically based on configuration or contextual information. Once a certain strategy is selected, subsequent operations are related to that strategy and no longer to other strategies.

For example, you can encapsulate the strategy selection logic in a factory class or configuration file, so you don’t need to frequently use if/else statements in your business code.

Ma Yinan: I see. The Strategy Pattern is indeed a more elegant way to handle business changes. It’s like playing with LEGO blocks, where you can easily replace a specific block.

However, I thought of a problem: if our system is very large, it seems quite challenging to replace a certain part of its functionality.

Master: You are correct. Replacing a certain part of functionality in a large system is indeed a challenge. This requires you to identify appropriately sized components during system design and define their interfaces well. In this way, you can replace components to achieve functionality replacement without modifying the entire system.

This involves the Liskov Substitution Principle. The Liskov Substitution Principle tells us that subclasses must be able to substitute their parent classes, and the behavior of the system should remain consistent after substitution. This requires us to follow certain specifications when designing interfaces and classes to ensure that subclasses can correctly replace their parent classes.

Ma Yinan: The Liskov Substitution Principle sounds advanced, but how can it be implemented in C language?

Master: To implement the Liskov Substitution Principle in C language, you need to pay more attention to interface design and the independence of components. You can use function pointer types to achieve this. In this way, you can replace the implementation of a certain component at runtime without modifying the code that calls that component.

Ma Yinan: I occasionally used these methods in actual programming but never thought about their relationship with the SOLID principles. However, I found that even if I defined interfaces in real code, it was still difficult to achieve true reuse or replacement. Why is that?

Master: This is a very common problem. Often, our components are designed too large, and the interfaces are too bloated. This leads to our components being like a chicken rib—somewhat useful but inconvenient or costly to use.

For example, if you have a toolbox containing various tools, including wrenches, screwdrivers, hammers, etc., and you also have a portable multifunction Swiss Army knife that contains small wrenches, screwdrivers, and other tools. When you actually start working, you will find that although the multifunction Swiss Army knife is useful, it is not as easy to use as the tools in your toolbox.

Ma Yinan: Are you saying that a single-responsibility wrench is better than a multifunction Swiss Army knife? Indeed, a single-responsibility wrench of fixed size is easier to use.

Master: The same principle applies; if your component interface is too complex and contains too many functionalities, other developers will feel confused and inconvenienced when using your component. They may only need part of the functionalities but have to deal with the entire complex interface.

Ma Yinan: Yes, having more functionalities means more dependencies, making it difficult to utilize or extend a specific functionality, and the learning cost is also high. So how should we solve this?

Master: Therefore, you need to consider the Interface Segregation Principle. This principle tells us to refine interfaces as much as possible, with each interface only assuming one role. In this way, other developers can focus only on the interfaces they need when using your component without worrying about other unrelated interfaces.

This is actually also a reflection of the Single Responsibility Principle. Each component or interface should have only one reason to change. If you find that your interface is taking on too many responsibilities, you should consider splitting it into smaller interfaces.

Ma Yinan: I understand. The Interface Segregation Principle can indeed help us design components that are easier to use and reuse. When I was reading that blog, I found that in the past, we defined interfaces from the server’s perspective, rather than from the consumer’s perspective as mentioned in the blog.

Master: Yes, if you are a real estate developer, the simplest way is to build all houses the same, which would minimize costs. But you definitely won’t be able to sell them because you didn’t design from the consumer’s perspective.

Ma Yinan: Hehe, this is easy to understand. As long as the perspective is correct, it’s hard to go wrong. From now on, we should define single-responsibility components from the consumer’s perspective, with their interface being the Role Interface. Such components are relatively small and can easily satisfy the Liskov Substitution Principle, thus making it easy to extend and replace, enabling us to meet the Open/Closed Principle.

Master: Your understanding is very accurate!

Ma Yinan: What about the last Dependency Inversion Principle? It seems to focus more on the ideas of object-oriented programming. But I am using C language, which is a procedural language. Is the Dependency Inversion Principle also applicable to C language?

Master: Of course, it is applicable. The core idea of the Dependency Inversion Principle is: depend on abstractions, not on concrete implementations. This is not just the patent of object-oriented programming; it is equally applicable to procedural languages like C.

Ma Yinan: Let me think. This blog says that the Dependency Inversion Principle actually guides how to implement the Interface Segregation Principle. If my previous component interface is the Role Interface, then it is the abstract interface, which means that both the consumer side and the producer side depend on this abstract interface.

Master: Yes.

Ma Yinan: In C language, this abstract interface is usually a function pointer type. So I have actually satisfied the Dependency Inversion Principle.

Master: Exactly. You can define a function pointer type as an abstract interface and then pass in specific implementation functions at runtime. In this way, your code depends on this abstract interface rather than on specific implementation functions.

Ma Yinan: The Dependency Inversion Principle seems profound, but it feels very simple once understood.

Master: If you have many layers of windows to break through on your path to becoming an architect, this is one very important layer. Once you master it, you can easily design loosely coupled components and architectures and write testable code.

Ma Yinan: Thank you so much, Master! I really feel like I have opened the meridians!

Master: Remember, implementing the SOLID principles comes at a cost, and highly scalable systems also come at a cost. You cannot achieve arbitrary extensibility and replaceability for all functionalities from the start. You can only abstract components and interfaces after identifying the points of change. In this way, you can allow the system to have good extensibility. Of course, this does not mean that you cannot design the system well from the beginning. On the contrary, you should try to consider possible points of change during the design phase and plan components and interfaces in advance. However, as the project progresses and requirements change, you may still need to continuously refactor and evolve the code.

Ma Yinan: I understand. Implementing the SOLID principles does require weighing costs and benefits. I will try to apply these principles in my project and adjust and optimize according to the actual situation.

Master: Waiting for your good news!

Series of articles on embedded systems:

  • Team Design in Embedded Systems—Team First

  • Team Design in Embedded Systems—Team Topology

  • Requirement Management in Embedded Systems—How to Double Delivery Efficiency?

  • Quality in Embedded Systems—How to Get Things Right the First Time?

  • Architectural Design in Embedded Systems—Application of Event-Driven Architecture

Leave a Comment

×