I’m preparing a keynote on “Timeless Design Principles”, with the aim of demonstrating how the principles I try to instil in developers on my Codemanship courses could have been applied just as readily 30 years ago or even 50 years ago in programming languages of the time.
In 1989, C ruled the world. A common misconception among inexperienced developers is that design principles like S.O.L.I.D. and Tell, Don’t Ask only apply to OO languages like C++.
Nothing could be further from the truth, though. Let’s start with Tell, Don’t Ask.
Consider this simple C function that calculates a quote for a fitted carpet:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
float quote(struct Room *room, struct Carpet *carpet){ | |
float area = room->length * room->width; | |
if(carpet->roundUp){ | |
area = ceil(area); | |
} | |
return area * carpet->pricePerSqrMetre; | |
} |
The quote() function has to ask for the room’s dimensions, and then has to ask for the carpet’s price pr square metre and whether it should round up to the nearest square metre.
Although Room and Carpet aren’t classes, as far as I’m concerned this is still Feature Envy. Room and Carpet are completely unencapsulated. The carpet_quote module knows how the area of a room is calculated, and it knows how to calculate the price of the carpet in that room. If those details change, carpet_quote breaks. Or, more simply, carpet_quote knows too much.
A good first step to fixing that would be to move those two pieces of logic into their own functions.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
float area(const struct Room *room) { | |
return room->length * room->width; | |
} | |
float price(const struct Carpet *carpet, float area) { | |
if(carpet->roundUp){ | |
area = ceil(area); | |
} | |
return area * carpet->pricePerSqrMetre; | |
} | |
float quote(struct Room *room, struct Carpet *carpet){ | |
return price(carpet, area(room)); | |
} |
Now our quote() function knows a lot less. But the carpet_quote module still knows it all. So the next step would be to move the area() and price() functions to the modules where the Room and Carpet structs are declared.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "room.h" | |
float area(const struct Room *room) { | |
return room->length * room->width; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <math.h> | |
#include "carpet.h" | |
float price(const struct Carpet *carpet, float area) { | |
if(carpet->roundUp){ | |
area = ceil(area); | |
} | |
return area * carpet->pricePerSqrMetre; | |
} |
carpet_quote now knows less about the details, which are neatly encapsulated inside carpet and room.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "carpet_quote.h" | |
#include "room.h" | |
#include "carpet.h" | |
float quote(struct Room *room, struct Carpet *carpet){ | |
return price(carpet, area(room)); | |
} |
The data is still accessible from the outside, though. So we’ve got a little more work to do to complete this refactoring. At the moment, the Room and Carpet structs are declared in completeness in the header files room.h and carpet.h, so any module can create and set their field values directly.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#ifndef ENCAPSULATION_ROOM_H | |
#define ENCAPSULATION_ROOM_H | |
struct Room { | |
float width; | |
float length; | |
}; | |
float area(const struct Room *room); | |
#endif //ENCAPSULATION_ROOM_H |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#ifndef ENCAPSULATION_CARPET_H | |
#define ENCAPSULATION_CARPET_H | |
#include "bool.h" | |
struct Carpet { | |
float pricePerSqrMetre; | |
boolean roundUp; | |
}; | |
float price(const struct Carpet *carpet, float area); | |
#endif //ENCAPSULATION_CARPET_H |
In our client code, we instantiate these “objects” directly, setting their field values externally. This gives me the screaming heebie-jeebies. (Every bit a smuch as “data classes in Python, or JSON objects in JavaScript.)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
int main() { | |
struct Room room = {2.5, 2.5}; | |
struct Carpet carpet = {10.0, false}; | |
float total = quote(&room, &carpet); |
How can we hide the data of a Room and a Carpet inside their respective modules? Luckily, C gives a mechanism: partial declarations. We can partially declare a struct in a .h file, defining its type but omitting its data.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#ifndef ENCAPSULATION_ROOM_H | |
#define ENCAPSULATION_ROOM_H | |
typedef struct Room Room; | |
Room* new_room(float width, float length); | |
float area(struct Room *room); | |
#endif //ENCAPSULATION_ROOM_H |
Then we can declare the struct with all its data fields in the .c file, so that they can only be accessed internally. Then we add a factory method – essentially a “constructor” – for that type.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <stdlib.h> | |
#include "room.h" | |
struct Room { | |
float width; | |
float length; | |
}; | |
Room* new_room(float width, float length){ | |
Room* room = malloc(sizeof(Room)); | |
room->width = width; | |
room->length = length; | |
return room; | |
} | |
float area(struct Room *room) { | |
return room->length * room->width; | |
} |
Now the only way a client can get a handle on an instance of the struct is via its module, and it can’t access the data directly.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
int main() { | |
Room* room = new_room(2.5, 2.5); | |
Carpet* carpet = new_carpet(10.0, false); | |
float total = quote(room, carpet); |