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