Tell, Don’t Ask in C

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:

float quote(struct Room *room, struct Carpet *carpet){
float area = room->length * room->width;
if(carpet->roundUp){
area = ceil(area);
}
return area * carpet->pricePerSqrMetre;
}

view raw
carpet_quote.c
hosted with ❤ by GitHub

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.

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));
}

view raw
carpet_quote.c
hosted with ❤ by GitHub

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.

#include "room.h"
float area(const struct Room *room) {
return room->length * room->width;
}

view raw
room.c
hosted with ❤ by GitHub

#include <math.h>
#include "carpet.h"
float price(const struct Carpet *carpet, float area) {
if(carpet->roundUp){
area = ceil(area);
}
return area * carpet->pricePerSqrMetre;
}

view raw
carpet.c
hosted with ❤ by GitHub

carpet_quote now knows less about the details, which are neatly encapsulated inside carpet and room.

#include "carpet_quote.h"
#include "room.h"
#include "carpet.h"
float quote(struct Room *room, struct Carpet *carpet){
return price(carpet, area(room));
}

view raw
carpet_quote.c
hosted with ❤ by GitHub

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.

#ifndef ENCAPSULATION_ROOM_H
#define ENCAPSULATION_ROOM_H
struct Room {
float width;
float length;
};
float area(const struct Room *room);
#endif //ENCAPSULATION_ROOM_H

view raw
room.h
hosted with ❤ by GitHub

#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

view raw
carpet.h
hosted with ❤ by GitHub

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.)

int main() {
struct Room room = {2.5, 2.5};
struct Carpet carpet = {10.0, false};
float total = quote(&room, &carpet);

view raw
main.c
hosted with ❤ by GitHub

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.

#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

view raw
room.h
hosted with ❤ by GitHub

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.

#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;
}

view raw
room.c
hosted with ❤ by GitHub

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.

int main() {
Room* room = new_room(2.5, 2.5);
Carpet* carpet = new_carpet(10.0, false);
float total = quote(room, carpet);

view raw
main.c
hosted with ❤ by GitHub

 

Author: codemanship

Founder of Codemanship Ltd and code craft coach and trainer

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s