Design Principles, Best Practices and Design Patterns for C (or Procedural Programming in general)?
Information hiding - as espoused by Parnas (Software Fundamentals).
Careful management of headers and visibility:
- Everything in a source file that can be hidden from the outside world should be; only the documented external interface should be exposed.
- Everything that is exposed is declared in a header.
- That header is used where the functionality is needed (and where it is defined).
- The header is self-contained - when you need it, you use it, and you don't have to fret about 'what other headers do I also have to include' because the header ensures it works by including anything it needs to make it work.
The header is self-protected - so it does not matter if it is included multiple times.
#ifndef HEADER_H_INCLUDED #define HEADER_H_INCLUDED ...rest of header contents, including other #include lines if necessary #endif /* HEADER_H_INCLUDED */
Design sets of functions to work on 'objects' (usually structures) - and use those functions rather than poking around the innards of the structure in the code that is using it. Think of it as self-imposed encapsulation.
My three advices:
- Write unit tests. They will help you zero in on a design that suites your problem as you go along. Much better than relying (solely) on pre-meditated thinking.
- Have a memory leak detector (there are all sort of libraries out there) installed and running from day one. Have this library print out all leaks as soon as the program/tests exits. This will allow you to catch a leak as soon as you introduce it, thereby making its fixing much less painful.
- Write OOP code in C. Not that difficult. While it is possible to emulate method overriding, I suggest that you start with emulation of simple objects. Even this simple mechanism can give you great mileage.
Here's an example:
typedef struct Vector {
int size;
int limit;
int* ints;
} Vector;
Vector* Vector_new() {
Vector* res = (Vector*) malloc(sizeof(Vector));
res->limit = 10;
res->size = 0;
res->ints = (int*) malloc(sizeof(int) * res.limit);
return res;
}
void Vector_destroy(Vector* v) {
free(v->ints);
free(v);
}
void Vector_add(Vector* v, int n) {
if(v->size == v->limit) {
v->limit = v->limit * 2 + 10;
v->ints = realloc(v->ints, v->limit);
}
v->ints[v->size] = n;
++v->size;
}
int Vector_get(Vector* v, int index) {
if(index >= 0 && index < v->size)
return v->ints[index];
assert false;
}
There is a good, free, online book, titled Object-Oriented Programming With ANSI-C, which covers the topic of writing object-oriented code in C. A google search for "object-oriented C" also yields a number of other good examples and resources.
If your project is safety-critical, MISRA-C is a good set of rules. It is intended mostly for embedded c, but it can be useful in other areas as well.
I consider myself an OO coder, and I do a lot of work with embedded-C. The best advice I can give, especially for large projects, is not to overdo it. Creating a complete OO framework on top of ANSI C can be very tempting, but it takes a great deal of time and effort to get it right. The fancier you get, the more time you will spend debugging your framework instead of working on the real project. Approach the task with a clear head, and a good, solid grasp of YAGNI. Best of luck!