Enning v1r0
Enning v1r0
The most common design pattern I’ve seen in C code is the function-call data-structure
free-for-all. Not all C code is a mess, but it sure seems that there is a lot of it. It’s not
easy, but C code does not have to be a mess. Good designs are modular, and the
modules have high cohesion and loose coupling. We’ve heard those terms for years,
but what do they mean?
To build good designs, we have to change the usual way of design evaluation from Not
Invented Here (NIH) to using SOLID design principles. The SOLID design principles
give us some speci c things to look for in a design to develop modules with high
cohesion and loose coupling. The ve design principles, described in Bob Martin’s book
(Agile Software Development, Principles, Patterns, and Practices [Mar02]), spell the
word SOLID.
Let’s look at the SOLID design principles, which are tried-and-true principles that help
build better designs. They come from the Object Oriented world, but there is no reason
we cannot apply them and get bene t from them when programming in C. We’ll look at
examples of code using these principles.
As it turns out, making code that is unit testable leads to better designs. Testable code
has to be modular and loosely coupled. In my book, Test-Driven Development for
Embedded C [Gre11], I go into how Test-Driven Development can help to steer a
design, but in this paper, we’ll mainly look at some of the ways to structure C code to
build designs that can pass the test of time. Let’s start by looking at the principles and
them some design models in C to implement them.
Admin Light
Console Scheduler
+ScheduleTurnOn()
+RemoveSchedule()
+WakeUp() <<anonymous callback>>
Hardware RTOS
The admin console subsystem can tell the LightScheduler module to turn on or off a
light at a scheduled time. It has the responsibility of managing the light schedule. The
LightController interacts with the hardware that can turn on or off some light by its ID.
The TimeService has the responsibility of providing the time and periodically waking its
client through a callback mechanism.
You can apply SRP to functions. Well-focused responsibilities help you recognize where
changes should be made as requirements evolve. When this principle is not followed,
you get those 1,000-line functions participating in global function and data orgies.
Let me explain OCP by metaphor: a USB port can be extended (you can plug any
compliant USB devices into the port) but does not need to be modi ed to accept a new
device. So, a computer that has a USB port is open for extension but closed for
modi cation for compliant USB devices.
When some aspect of a design follows the OCP, it can be extended by adding new
code, rather than modifying existing code. We can say that the LightScheduler (from the
previous example) is open for extension for new kinds of LightControllers. Why? If the
interface is obeyed, the calling code (the client) does not care about the type of the
called code (the server). OCP supports substitution of service providers in such a way
that no change is needed to the client to accommodate a new server. This diagram
illustrates that LightScheduler can work, unmodi ed, with Model 42 and X10
LightControllers as well as Acme and Linux versions of TimeService.
Admin Light
Console Scheduler
+ ScheduleTurnOn()
+ RemoveSchedule()
+WakeUp()
<<interface>> <<interface>>
Light Controller Time Service
+ On(id) + GetTime()
+ Off(id) + SetPeriodicAlarm()
<<implements>> <<implements>>
Model 42 Acme
Light Controller Time Service
Light
Light
Scheduler
Scheduler
Test
+ ScheduleTurnOn()
+ RemoveSchedule()
+wakeUp()
<<interface>> <<interface>>
Light Controller Time Service
+ On(id) + GetTime()
+ Off(id) + SetPeriodicAlarm()
<<implements>> <<implements>>
This means we can’t let hardware or OS implementation knowledge make its way into
the LightScheduler. This design allows substitutability of depended upon modules. In
C, the header le is the interface and the C le is the implementation. We can use the
linker to substitute in different version of LightControllers and TimeServices. But there is
more to substitutability than having the same interface.
As long as the LightScheduler does not have to behave differently when interacting with
the test stub, LightControllerSpy, the design adheres to LSP.
<<interface>> <<interface>>
Light Controller Time Service
+ On(id) + GetTime()
+ Off(id) + SetPeriodicAlarm()
<<implements>> <<implements>>
Model 42 RTOS
Light Controller Time Service
There may be many more time-related functions in the target operating system. The
target OS tries to be everything for every application, while the TimeService is focused
on the needs of this system. By tailoring interfaces, we limit dependencies, make code
more easily ported, and make it easier to test the code that uses the interface.
LightScheduler LightScheduler
int (*RandomMinute_Get)()
<<implements>>
The right side of the gure shows an inverted dependency. Here the high level depends
on an abstraction, which is an interface in the form of a function pointer. The details also
depend on the abstraction; RandomMinute_Get( ) implements the interface.
Operating systems use the same mechanism to keep the OS code from depending
directly on your code. A callback function is a form of dependency inversion.
Dependency inversion in C does not have to involve function pointers. In C, it’s almost
more a state of mind. When we we look at the Single Instance Module pattern later in
the paper, we will hide data structures inside the C le while only revealing the name of
the structure. There we are applying DIP.
Model Purpose
Per-type dynamic interface Allows multiple types of modules with the same
interface to have unique interface functions
Each model is more complex than the previous model. I suggest that you choose a
model that is the simplest that works for your needs. As things change, and you employ
SOLID in your designs (and add automated tests) you will nd your code is much softer
and more exible.
Single-instance module
For single-instance modules, the header le de nes everything needed to interact with
the module. The LightController header would only contain these function prototypes.
void LightController_Create(void);
void LightController_Destroy(void);
void LightController_TurnOn(int id);
void LightController_TurnOff(int id);
Anything that can be hidden should be hidden. The data structures that the scheduler
needs to do its job are hidden as le scope variables in the .c le. The scheduler’s data
structures are not needed in the header because no other modules should care. This
makes it impossible for other modules to depend on the structure and assures its
integrity is the scheduler’s responsibility. If enums or #de nes needed to interact with
the module they would go into the header le (but they are not in this case).
Multiple-instance module
Sometimes an application needs several instances of a module that contains different
data and state. For example, an application might need several rst-in rst-out data
This is a well-established design model based on Barbara Liskov’s abstract data type
[Lis74]. The members of the CircularBufferStruct are not revealed in the header le. The
typedef statement declares that there is a struct of a given name but hides the members
of the struct to users of the interface. This prevents users of the CircularBuffer from
directly depending upon the data in the struct. The struct is de ned in the .c le, hidden
from view. Not that it is relevant, here is what the structure would look like de ned near
the top of the .c le.
Dynamic interface
In a dynamic interface, we are solving the problem of duplicate conditional logic. Let’s
say that your application has numerous light controlling hardware implementations. It’s
likely that your code has a data structure for each type of LightDriver. There’s probably
an enum or set of #de nes like this:
Also there would be a struct that all the speci c LightDriver types would include as their
rst member like this:
typedef struct LightDriverStruct
{
LightDriverType type;
int id;
} LightDriverStruct;
All that is ne, until we get to the usage of that data. Here is how the LightControler
would turn on lights.
if (NULL == driver)
return;
switch (driver->type)
{
case X10:
X10LightDriver_TurnOn(driver);
break;
case AcmeWireless:
AcmeWirelessLightDriver_TurnOn(driver);
break;
case MemoryMapped:
MemMappedLightDriver_TurnOn(driver);
break;
case TestLightDriver:
LightDriverSpy_TurnOn(driver);
break;
default:
/* now what? */
break;
}
}
You can see with this approach that there will be a very similar function for turning off a
light, or destroying the driver. Later when more light operations are needed (like
dimming and strobe), more switch statements will be needed. This duplication is bad
and makes a mess of the code and an opportunity for errors.
How the dynamic interface helps sovle this problem is by allowing the driver functions to
be set at runtime. Instead of direct function calls, the driver functions are called through
function pointers. The interface looks like this:
void LightDriver_Create(void);
void (*LightDriver_Destroy)(void);
void (*LightDriver_TurnOn)(int id);
void (*Lightriver_TurnOff)(int id);
During initialization or con guring the pointers could be set, eliminating the need for the
duplicate switch statements. There would be a single switch statement that sets up the
pointers.
Having function pointers is very convenient for test purposes also. A test stub version of
the driver functions can be dropped into the function pointers, allowing the test code to
Embedded Systems Conference, San Jose, May 2012 Class ESC-231
Copyright © 2012 James W Grenning wingman-sw.com
All rights reserved [email protected]
fi
monitor the light operations. Unlike the LightControllerSpy_TurnOn function in the
switch statement, now there would be no dependency on the test code in the production
code. That dependency is inverted.
A single set of function pointers works ne for when the same functions are used for
each driver type. Although, when there can be multiple supported drivers concurrently,
a different solution is needed.
Put the struct in a le called LightDriverPrivate.h. It is needed by all the different kinds
of LightDrivers, but not the users of the LightDriver. Code that is not a LightDriver
implementation should not include that le. There is no stopping them, but still, that’s
what they should do.
TEST(LightDriverSpy, On)
{
LightDriver lightDriverSpy = LightDriverSpy_Create(1);
LightDriver_TurnOn(lightDriverSpy);
LONGS_EQUAL(LIGHT_ON, LightDriverSpy_GetState(1));
}
Notice that the spy is created for light ID number 1. This would initialize the driver and
the function pointers so that when LightDriver_TurnOn is called, the spy’s turn on
function is called. The spy remembers that it was called and you can tell by getting the
state it has saved for light number 1.
Here is the LightDriver data structure that supports the per-type dynamic interface:
LightDriverInterface was de ned on the previous page. The name vtable is borrowed
from C++. Virtual functions in C++ work similarly to this. The vtable is initialized like
this and stored in a le scope variable.
It’s kind of hard to look at, and error prone, so it is good it is hidden behind the scenes.
Also there is no need to duplicate that in clients of the driver.
The really safe way to initialize the LightDriverInterfaceStruct, if your compiler supports
it is:
ANSI compilers don’t support the named eld initialization. You need a C99 compiler for
that. As long as we are looking at the safest way to do these things, here is the safest
way to dispatch through a vtable:
When combining named eld initialization with the above, you could add a new function
pointer to the LightDriverInterfaceStruct and it would be initialized to the null pointer
value for all initializers that don’t mention it. So if we added the Strobe function, and it’s
not supported by all implementations, there is no work to do. Calling
LightDriver_StrobeOn would have no effect because its function pointer is null.
I am not suggesting that you don’t think ahead. It is impossible to not think ahead, but
you can choose what you will act on now vs. what you will act on later. There is a thin
line between thinking ahead and analysis paralysis. When you start piling guesses on
top of guesses, consider that you’ve gone too far ahead, and it’s time to try the ideas in
code.
When there is uncertainty in the hardware/software boundary, you can start from the
inside by solving the application problem, working your way to where application code
Embedded Systems Conference, San Jose, May 2012 Class ESC-231
Copyright © 2012 James W Grenning wingman-sw.com
All rights reserved [email protected]
fi
fi
can articulate what it wants from the hardware. Create an interface that provides exactly
the services the application needs from the hardware. The LightScheduler/LightController
relationship is an example of this. The LightController became part of our hardware
abstraction layer.
A nice side effect of the application driving the interface is that hardware implementation
details are less likely to pollute the application’s core. The LightScheduler knows
nothing about X10 or any of the other drivers, and that’s a good thing.
We can’t anticipate all the coming product changes; that is why we have to get good at
design evolution. Underlying many of these ideas are the Extreme Programming Rules
of Simple Design based on Kent Beck’s book Extreme Programming Explained [Bec00]
Let’s look at them and see how they help us keep the design good for today’s
requirements.
The fourth rule tells us to not over-engineer the design. It should be perfect for the
currently implemented features. Adding complexity early delays features and integration
opportunities. It wastes time when the premature design is wrong. Carrying around
unused or unneeded design elements slows progress. Designs always evolve. We need
to be good at keeping the design right for the currently supported features.
That fourth rule may be the hardest to follow for people new to TDD. Like I said before,
it’s OK to think ahead; just be careful what you action. Let the tests pull in the design
Bibliography