I know SOLID principles were written for object oriented languages.
I found in the book: "Test driven development for embedded C" by Robert Martin, the following sentence in the last chapter of the book:
"Applying the Open-Closed Principle and the Liskov Substitution Principle makes more flexible designs."
As this is a book of C (no c++ or c#), there should be a way of way of implementing this principles.
There exists any standard way for implementing this principles in C?
Open-closed principle states that a system should be designed so that it's open to extension while keeping it closed from modification, or that it can be used and extended without modifying it. An I/O subsystem, as mentioned by Dennis, is a fairly common example: in a reusable system the user should be able to specify how data is read and written instead of assuming that data can only be written to files for example.
The way to implement this depends on your needs: You may allow the user to pass in an open file descriptor or handle, which already enables the use of sockets or pipes in addition to files. Or you may allow the user to pass in pointers to the functions that should be used for reading and writing: this way your system could be used with encrypted or compressed data streams in addition to what the OS permits.
The Liskov substitution principle states that it should always be possible to replace a type with a subtype. In C you don't often have subtypes, but you can apply the principle in the module level: code should be designed so that using an extended version of a module, like a newer version, should not break it. An extended version of a module may use a struct
that has more fields than the original, more fields in an enum
, and similar things, so your code shouldn't assume that a struct that is passed in has a certain size, or that enum values have a certain maximum.
One example of this is how socket addresses are implemented in the BSD socket API: there an "abstract" socket type struct sockaddr
that can stand for any socket address type, and a concrete socket type for each implementation, such as struct sockaddr_un
for Unix domain sockets and struct sockaddr_in
for IP sockets. Functions that work on socket addresses have to be passed a pointer to the data and the size of the concrete address type.
First, it helps to think about why we have these design principles. Why does following the SOLID principles make software better? Work to understand the goals of each principle, and not just the specific implementation details required to use them with a specific language.
Notice how each principle drives an improvement in a certain attribute of the system, whether it be higher cohesion, looser coupling, or modularity.
Remember, your goal is to produce high quality software. Quality is made up of many different attributes, including correctness, efficiency, maintainability, understandability, etc. When followed, the SOLID principles help you get there. So once you've got the "why" of the principles, the "how" of the implementation gets a lot easier.
EDIT:
I'll try to more directly answer your question.
For the Open/Close Principle, the rule is that both the signature and the behavior of the old interface must remain the same before and after any changes. Don't disrupt any code that is calling it. That means it absolutely takes a new interface to implement the new stuff, because the old stuff already has a behavior. The new interface must have a different signature, because it offers the new and different functionality. So you meet those requirements in C just the same way as you'd do it in C++.
Let's say you have a function int foo(int a, int b, int c)
and you want to add a version that's almost exactly the same, but it takes a fourth parameter, like this: int foo(int a, int b, int c, int d)
. It's a requirement that the new version be backward compatible with the old version, and that some default (such as zero) for the new parameter would make that happen. You'd move the implementation code from old foo into new foo, and in your old foo you'd do this: int foo(int a, int b, int c) { return foo(a, b, c, 0);}
So even though we radically transformed the contents of int foo(int a, int b, int c)
, we preserved its functionality. It remained closed to change.
The Liskov substitution principle states that different subtypes must work compatibly. In other words, the things with common signatures that can be substituted for each other must behave rationally the same.
In C, this can be accomplished with function pointers to functions that take identical sets of parameters. Let's say you have this code:
#include <stdio.h>
void fred(int x)
{
printf( "fred %d\n", x );
}
void barney(int x)
{
printf( "barney %d\n", x );
}
#define Wilma 0
#define Betty 1
int main()
{
void (*flintstone)(int);
int wife = Betty;
switch(wife)
{
case Wilma:
flintstone = &fred;
case Betty:
flintstone = &barney;
}
(*flintstone)(42);
return 0;
}
fred() and barney() must have compatible parameter lists for this to work, of course, but that's no different than subclasses inheriting their vtable from their superclasses. Part of the behavior contract would be that both fred() and barney() should have no hidden dependencies, or if they do, they must also be compatible. In this simplistic example, both functions rely only on stdout, so it's not really a big deal. The idea is that you preserve correct behavior in both situations where either function could be used interchangeably.
The closest thing I can think of off the top of my head (and it's not perfect, so if someone has a much better idea they are welcome to one-up me) is mostly for when I'm writing functions for some sort of library.
For Liskov substitution, if you have a header file that defines a number of functions, you do not want the functionality of that library to depend on which implementation you have of the functions; you ought to be able to use any reasonable implementation and expect your program to do its thing.
As for the Open/Closed principle, if you want to implement an I/O library, you want to have functions that do the bare minimum (like read
and write
). At the same time, you may want to use those to develop more sophisticated I/O functions (like scanf
and printf
), but you aren't going to modify the code that did the bare minimum.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With