If you want to achieve all of the potential of C++ then decoupling the modules which make up your project is as important as modeling the project in terms of objects. In a “massively decoupled” project the objects don’t know about the operating system, the hardware or the project unless they need to.
The diagram in Figure 1 below shows how to partition your classes to achieve this sort of decoupling. Each class in your system should fall into one and only one of the following categories, each represented by a box in the diagram.
Platform dependent classes model operating system or hardware items like ThreadxTask , NucleusTimer or PowerPcGpioPin should only be accessed through platform interface classes like Task , Timer and GpioPin .
In a similar way, product interface classes allow product independent classes to interact with product-specific classes without knowing details of the product. An example would be a TimerHandler class which allows a Timer to call up into the product specific code which needs to be periodically executed.
All of the classes in the diagram are reuseable across multiple projects, except for the product specific classes. Sometimes these classes have quite a bit of code, since the behavior of the product can be complex. Many, however, are very small, since all they do is modify or expand the operation of an existing reuseable class. An example of these small classes would be a project-specific LCD class which does extra setup like asserting a GPIO pin to enable power.
If code is required for an object which has parts which are generic, parts which are platform specific, parts which are platform and product independent, and parts which are product specific, then separate but related classes should be created, each of which fits into one box in the diagram in Figure 2 below:
This inheritance relationship shows a generic LCD interface class called Lcd , a class which modules a hardware specific LCD controller, in this case the EPSON S1D13806, a class called AcmeCorpLcd which does processing such as error handling required by all products developed at the company where the software is written, Acme Corp, and a project-specific class which does LCD initialization and other functions specific to Acme Corp’s “widget” project.
The Lcd , EpsonS1d13806Lcd and AcmeCorpLcd classes are all fully reuseable, but the AcmeCorpLcd is still tightly coupled to a particular implementation, the EpsonS1d13806Lcd . Later in this section you’ll see how to use design patterns like “inheritance by reference” to decouple things even further, and make the AcmeCorpLcd truly platform independent.
Decoupling is the key to writing large scale software systems. The complexity of a decoupled system increases at a linear rate as the number of classes in the system increases, but coupled systems become more complex at a geometric rate. This happens because the number of relationships between classes increases more rapidly in a coupled system.
Code which is decoupled is inherently easier to develop and maintain, and also raises whole new possibilities, like code simulation. If the software doesn’t care what platform it’s running on then you can write software simulators, which allow code written for a wireless phone or a hospital ventilator to run on a Windows PC, with elements like the LCD or flash memory modeled using the PC screen and disk files. Simulators can be taken on a laptop computer into the field, allowing end users to evaluate the product at a very early stage. The earlier user evaluation happens, the more easily it can be used to shape the final product.
The more you decouple the modules which make up your system, the easier it becomes both to debug and to reuse the modules. Massively decoupled code means easier development, debugging and enhancement.
If you always access platform dependent classes through platform interface classes, then you can change any part of the platform by changing a single platform dependent class. This allows easier porting from platform to platform, and it’s easier to change to different hardware even when the project is well underway.
The interface classes will often be abstract base classes, and many of the classes which are independent of the product and the platform can also be abstract base classes. However, there’s no need for every function of these classes to be pure virtual, or even virtual. Generic objects can often do useful work without knowing anything about their derived classes.
Another key technique for decoupling classes from each other is to pass configuration data through the constructor or some other member function, rather than through #defines . The database should be given a pointer or a reference to the Eeprom object through its constructor. A generic RS232Link class shouldn’t know what baud rate to set itself to, instead its client should give it the required baud rate through its constructor or through a function like SetBaudRate() .
Once you have a good set of classes defined for the industry you work in, you can start using them as building blocks. You can start combining them in different ways to suit the project that you’re working on. Examples of recombined classes I’ve used include a TaskAwareFlash which combines a Flash and a Task to create a new type of flash driver which doesn’t lock out low priority tasks while a flash part is being erased; a FlashingLed which combines a Timer and an Led , and an InterruptDrivenRs232Link which turns any Rs232Link into one which has interrupt-driven receive and transmit buffers.
There are several design patterns which can also help you decouple code and make it more reuseable. For instance, the standard “bridge” design pattern separates the interface of a platform specific class from its implementation. This decouples the application specific code from the platform, and allows you to easily change the platform.
A very similar design pattern called “Inheritance by reference” also allows you to decouple a project specific class from a platform specific class. This is how it works out if we take the previous example of the ThreadxTimer and FlashingLedTimer , both derived from the Timer class.
As shown in Figure 3 above, if we arrange it to use the inheritance by reference pattern then both take a reference to a Timer, which is used to access the other derived class. ThreadxTimer uses its reference to make function calls up to FlashingLedTimer::HandleExpiry() and FlashingLedTimer uses its reference to make calls down to ThreadxTimer::SetPeriod() . If the linking functions are declared inline, then there's little or no performance overhead.
To read Part 1: go to Using object-oriented methods to achieve C++'s promise. Next week in Part 3: Designing Objects as Active Agents
Originally from New Zealand, RichardSeaman has been working in the United States as a contractsoftware engineer since 1997, in the telecommunications, automotive,wireless audio and medical industries. He also does architecting,design and implementation of embedded real-time software systems. Hecan be contacted at .