4

In Robert Martin's seminal 1996 article "The Open-Closed Principle" he presents an example in C which does not follow the principle (the DrawAllShapes() method is not closed for modification):

enum ShapeType {circle, square};
struct Shape
{
 ShapeType itsType;
};
struct Circle
{
 ShapeType itsType;
 double itsRadius;
 Point itsCenter;
};
struct Square
{
 ShapeType itsType;
 double itsSide;
 Point itsTopLeft;
};
//
// These functions are implemented elsewhere
//
void DrawSquare(struct Square*)
void DrawCircle(struct Circle*);
typedef struct Shape *ShapePointer;
void DrawAllShapes(ShapePointer list[], int n)
{
 int i;
 for (i=0; i<n; i++)
 {
   struct Shape* s = list[i];
   switch (s->itsType)
   {
     case square:
       DrawSquare((struct Square*)s);
       break;
     case circle:
       DrawCircle((struct Circle*)s);
       break;
    }
   }
}

He then presents an OOP implementation of the above behaviour, using inheritance and polymorphism in C++ which better follows the open-closed principle (OCP).

My question is how would the C code above be refactored to adhere to the OCP using only the procedural capabilities of C. Or more generally, can code in procedural languages strictly follow the OCP?

2
  • 2
    Uncle Bob's OOP example uses runtime polymorphism. You would have to simulate that polymorphism in C. One way to do that would be with pointer functions. Commented Nov 12, 2019 at 22:37
  • 1
    See here for an example. Commented Nov 12, 2019 at 22:43

3 Answers 3

6

Think about what the open-closed principle entails: You have to write your methods in a way that you don't really know what the input is exactly, but you have to do something that depends on what exactly the input is.

Cf. a simple real-life example: You feel hungry (important abstraction: you don't directly care what you eat), so you go to a place where they serve food. Waiter shows up and asks you what you want. Consider the following two alternative algorithmic dialogs:

  • Is this a hamburger restaurant?
  • No.
  • Is this a seafood restaurant?
  • No.
  • Is this a pizza restaurant?
  • No.
  • Is this a bakery?
  • No
  • Is this a steak house?
  • Yes.
  • Great! I would like a prime-rib steak, medium rare please!

Over:

  • Would you please bring me something to eat?
  • Sure!

The problem with following the first logic is that... if you forget to ask for all types, you may end up not eating at all. So if that's the typical dialog, a new Chinese restaurant shows up, and you will never eat in there, until you "extend" your algorithmic dialog to include this possibility.

Regardless of the "artificiality" of the example, it shows that the open-closed principle has a very important consequence... You assign all tasks that are responsibilities of an object to the object itself. So your question boils down to:

How to abstract methods

Various languages have their own techniques. Because this is an extremely fundamental concept of OOP, OOP-tailored languages have this built-in. Abstractions are something like abstract classes / interfaces / etc. and implementations "fill-in" the details. In procedural languages, it comes down to "emulating" method calls.

As a result, the short answer to your question, regarding C, is, as suggested by Robert Harvey, the function pointer, which is the abstraction of a method that reveals what it does, but not how.

The long answer to your question is probably covered, in detail, in another answer regarding how to emulate OOP in C.

3

To do the job in C, you'd probably want to create a struct containing a pointer to a function. In other words, you'd explicitly/manually define a vtable, and include it as the first item in a union that included all the shapes:

void drawCircle(union Shape *);
void drawSquare(union Shape *);

struct Square { 
    void (*draw)();

    Position center;
    Size size;
};

struct Circle { 
    void (*draw)();

    Position center;
    Size size;
};

union Shape {
    Circle circle;
    Square square;
};


void drawShapes(Shape *shapes, int count) { 
    for (int i=0; i<count; i++)
        shapes[i]->draw(&shapes[i]);
}

So, we have an array of Shape objects. The first element in each of those is a pointer to a function that can draw that particular kind of shape. So, when/if we add a new type of shape, we need to define a function to draw that kind of shape, and when we create a shape "object" for that shape, we need to initialize its draw member to point to the drawing function for that shape.

When we call that function, it basically do something on the order of:

void drawSquare(void *data) { 
     struct Square *real_data = (struct Square *)data;

     // draw a square with the specified location/size
}

void drawCircle(void *data) {
    struct Circle *real_data = (struct circle *)data;

    // draw a circle with the specified location/size
}

This gets us sort of halfway to the open/closed principle. To add a new shape, we obviously need to add a new function to draw that sort of shape. We also need to add a struct to hold the sort of data needed to define that shape. That struct needs to contain a pointer to a function to do the drawing that will receive a pointer to a Shape. And, of course, we have to add that new type of shape to the Shape union, so we can pass the right data to the drawing function. If we're bothered by that, we can skip the Shape union entirely, and just pass each function a pointer to void, and define a vtable type (by whatever name we prefer) to contain only the pointer(s) to function(s) for the interface. That adheres a bit more closely to the open/closed principle, at the expense of (a little) type safety.

Our drawShapes and all the code for drawing a Circle or a Square can remain closed--adding a new shape doesn't affect them. Of course, adding more pointers to functions (rather than just the one draw) doesn't really change this a whole lot--we can still write functions generally on the order of drawShapes that work with shapes of arbitrary types, as long as each defines the same interface in the form of pointers to functions (with matching types and such).

In the other direction, this still has a lot of manual work that leaves a lot of room for mistakes. It's particularly easy to initialize some of the pointers so they refer to the wrong functions, so when we use them, we don't get what we expect. We also have to manually initialize the pointer to function in each Shape object we create, to have it point at the correct drawing function (and as we add more simulated virtual functions, that problem becomes ever worse).

1

In Open/Close, you have two aspects:

  • Close: you shall not change the source code. This is common business in C; you have plenty of libraries that you are not supposed to change. And use of opaque pointers can hide to the outside world the structure of the data that your are using.

  • Open: you shall be able to extend what you have easily and without changing the original code. This is something much more difficult to do in C and there is no language support for any such thing. It is feasible but requires discipline. And it requires a programme and data structures that allow it.

It's feasible in C - example:

In the case of the Shape you would typically emulate an abstract shape by adding some members to the Shape structure:

  • a void *data member that would point to a structure with the private shape data members.
  • a function pointer for each polymorphic shape handling, for example a drawing function DrawXxxx() that you currently use in the switch part. In future, instead of wondering what shape it is and do a large switch, you'd just call the drawing function via its pointer.
  • a ShapeXxxFactory() factory function would setup new ShapeXxxx by initializing the structure, aranging for the data pointer and the function pointers to be correctly initialized.

You can encapsulate elements by having a header limited to what has to be publicly known (factory function, destructor, ...), and hiding in a ShapeXccc.c compilation unit the private details:

  • the functions supposed to be used via function pointers can be encapsulated as static function, only a locally visible to the ShapeXxxFactory() function.
  • the data pointer would be an opaque pointer, and the real structure of the data would be defined in a local structure. So only the shape-specific functions can cast data to its real type.

Another trick is to write a couple of "template" functions that implement some general behavior of any shape, but that would invoke some shape-specific function pointers to do specific parts in a similar way as with template method. Again, no need for an enum; no need for a switch.

Other approaches

Another way to handle the extensibility is the plugin approach. It's similar from the philosophical point of view: you would dynamically load some functions and point to the right function dynamically.

Conclusion and known uses

So the Open/Close is feasible in C. The only question is how easy shall be "easy" in the principle's definition. In C all this is depending on good will and discipline, and is relatively error prone compared to C++: it's easy to make a mistake in the function pointer, or in the casting.

THis kind of design is used for example:

  • In the WinAPI, when you create a new type of windows and define a function that will process the events that are directed at that windows.
  • In Windows COM objects, that expose a binary interface but let the implementation of this interface be done in any suitable language, including in C.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.