Introduction
Being a programmer, we have all read source code that really hard to understand and follow. Words like spaghetti code instantly pop up in our head when we see it. Files are open in one file, used in a second and closed in a third file. Magic things are happening here and there. Code seems to be written by copy and paste. Structs-in-structs are suddenly used and modified reminding us of future train wrecks that most likely will occur and the bugs that will be created as a result of the fog around the code.
At the same time, we read about object-oriented design as a way out of this mess. But you write your code in C which has no support for creating objects as in C++, Java or C#. What to do?
A solution is to create and write modular code in C, even if you don’t have to do so due to the C language as such. It’s about designing your system based on object oriented principles and implementing it by creating more loosely coupled pieces of code, or modules. A module represents a logical entity in the system or application, much like an object in a object oriented language, and one should try to make the module as SOLID as possible.
How to do it
The smallest possible module consists of two files. One header file and one source code file.
- The header file contains the module’s public interface in the form of function prototypes and types. This should be the only entity that the clients (users) of the module should depend upon. It should not contain any implementation specific information.
- The source code file contains the implementation of the public interface. This includes defining implementation specific types such as enums, structs, etc, e.g. to track the state of the module, that are not needed by the clients of the module.
Depending upon the requirements and prerequisites of the module, the module need to include support a single instance (e.g. a device driver for physical piece of a hardware) or multiple instances (e.g. a queue or a tree node). Another important thing to remember is to construct and destruct the module internal data just as done in an object oriented language.
Implementing a single instance a module
To implement a single instance module is simple. It’s just to add static variables in the source code file to handle the module internal data required to implement the functionality specified by the header file.
//In the source code file
static int x;
static int y;
Multiple instance modules
When implementing a multiple instance module the way to go is to use an abstract data type (ADT), aka a user-defined data type, to store the module internal data. Using a forward declared type in the header file while defining the type in the source code file, as shown below, allows us to hide the implementation specific part of the data type from the clients.
//In the header file
typedef struct FooStruct Foo;
void Foo_doSomething(Foo);
//In the source code file
struct FooStruct
{
//Internal implementation specific data needed for module Foo
};
Initializing and cleaning up the module
When implementing a module in C it is important remember to implement functions for initialization and cleaning up (removing/freeing) module internal data.
//In the single instance header file
void Foo_Init(); //Initialize the internal data/state by setting default values, allocating memory etc
void Foo_Free(); //Clean up the used data in memory
//In the single instance header file
typedef FooStruct Foo;
*Foo Foo_New(); //Creates a new instance of Foo and returns a pointer to it
void Foo_Free(*Foo); //Cleans up the object pointed to by the pointer
Some benefits of modularized code in C
One of the bigger benefits of modularized C code is that it makes it easier to test each module. Using TDD when implementing the module is a great way to create a modularized design. The clear separation of the interface and the implementation simplifies the use of stubs or mock implementations when testing a module.
Writing modularized code in C allows several different implementations to exist of the the same module which can be handy in some situations. As an example, imagine that you have a module that handles storing data. In some cases you might want to store the data in the file system, while in some cases you might which to store the data in a SQL database or simply keep it in memory. These three examples requires three different implementations but the rest of the system remain unaffected of which implementation is used.