Part I: Good

C Opaque Type as an Object

In the C language, object-oriented abstraction can be implemented using structures and functions. In C, a pointer to a structure can be defined without disclosing the elements of the structure. For example, in the header file Shape.h we define opaque type

typedef const struct __Shape * ShapeRef;

This declares a new type, ShapeRef, which is a pointer to a hidden structure __Shape. While the messages we can send to (or functions we can call with) this structure pointer are defined in a C header file, i.e. Shape.h, we are not allowed to see nor access directly the elements of this structure.

You can read more about Opaque data types and Opaque pointers at Wikipedia. You can also learn more about Opaque types at Apple.

Mutable vs Immutable Types

You may have noticed in the example on Opaque Types that the pointer was defined using the const keyword. The keyword const means that the contents of __Shape structure cannot be modified after it is created. This makes Shape an immutable type. This means that while we can define functions that reveal the internal state (i.e., structure elements) of Shape, we cannot define functions that change its internal state. Only by eliminating the keyword const in the type definition would we be able to define functions to change the internal state of the type. A type whose internal state can be modified after creation is called a mutable type. Thus, in the header file Shape.h we also define the opaque type

typedef struct __Shape * MutableShapeRef;

What are the advantages of an immutable type? Let's say you want to use an instance of type A as a variable inside type B. You decide to do this by placing a copy of the pointer for the instance of A inside B. Unbeknownst to you, however, another type, let's call it C, also has a copy of the pointer for the same instance of A. If A was mutable, then type C could change the value of A, and, in turn, change the instance variable A inside B, without B knowing what happened. There are two ways to handle this problem. If A is mutable, then type B has no choice but to make its own personal copy of the A instance. Depending on the size of A, this could be a memory and time consuming process. On the other hand, if A is immutable, then B can safely (and rapidly) copy only the pointer for A. As you might imagine, immutable types are highly useful in concurrent programs. General advice: don't define an type as mutable unless you really need it.

Read more about Mutable and Immutable types at Wikipedia.

Implementation of Shape

Now, looking inside the source Shape.c, we find the structure

struct __Shape {
    // Shape Type attributes  - order of declaration is essential
    float xPosition;
    float yPosition;
    float orientation;
};

Creation and Destruction of the Shape Type is handled with these function:

static struct __Shape *ShapeAllocate()
{
      struct __Shape *theShape = malloc(sizeof(struct __Shape)); 
      if(NULL == theShape) return NULL;
      return theShape;
}

ShapeRef ShapeCreate(float xPosition, float yPosition, float orientation)
{
     struct __Shape *newShape = ShapeAllocate(); 
     if(NULL == newShape) return NULL; 
     newShape->xPosition = xPosition; 
     newShape->yPosition = yPosition;           
     newShape->orientation = orientation;
     return newShape;
}

void ShapeFinalize(ShapeRef theShape)
{
     if(NULL == theShape) return;
     free((void *)theShape); 
}

Comparison and Accessors are handled with these functions:

bool ShapeEqual(ShapeRef theShape1, ShapeRef theShape2) 
{
     if(NULL == theShape1 || NULL == theShape2) return false; 
     if(theShape1 == theShape2) return true;
     if(theShape1->xPosition != theShape2->xPosition) return false; 
     if(theShape1->yPosition != theShape2->yPosition) return false; 
     if(theShape1->orientation != theShape2->orientation) return false; 
     return true;
}

float ShapeGetXPosition(ShapeRef theShape)
{
     if(NULL == theShape) return nan(NULL);
     return theShape->xPosition;
}
void ShapeSetXPosition(MutableShapeRef theShape, float xPosition) {
     if(NULL == theShape) return;
     theShape->xPosition = xPosition;
}

A shape can be translated and rotated. These methods are handled with these functions:

void ShapeTranslate(MutableShapeRef theShape, float xTranslation, float yTranslation) 
{
     if(NULL == theShape) return;
      theShape->xPosition += xTranslation; 
      theShape->yPosition += yTranslation;
}

void ShapeRotate(MutableShapeRef theShape, float angle) 
{
     if(NULL == theShape) return;
     theShape->orientation += angle;
}

Usage of Shape

    MutableShapeRef shape = ShapeCreateMutable(0.0, 0.0, 0.0);
    ShapeShow(shape);
    
    ShapeTranslate(shape, 10.0, 20.0);
    ShapeRotate(shape, 180.);
    ShapeShow(shape);
    ShapeFinalize(shape);

Inheritance

Let's examine how we can define a Square type that inherits from Shape. In source code we define the structure

struct __Square {
     // Shape Type attributes - order of declaration is essential 
     float xPosition;
     float yPosition;
     float orientation;
     // Square Type attributes
     float width; 
};

For this inheritance trick to work it is essential that the order of instance variable declarations be identical to those inside the Shape structure. Any additional instance variables must go after the variables matching Shape's structure.

In the header file we define the opaque types

typedef const struct __Square * SquareRef;
typedef struct __Square * MutableSquareRef;

Creation and Destruction of the Square Type is handled with these function:

static struct __Square *SquareAllocate() 
{
     struct __Square *theSquare = malloc(sizeof(struct __Square)); if(NULL == theSquare) return NULL;
     return theSquare;
}

SquareRef SquareCreate(float xPosition, float yPosition, float orientation, float width) 
{
     struct __Square *newSquare = SquareAllocate();
     if(NULL == newSquare) return NULL; 
     newSquare->xPosition = xPosition; 
     newSquare->yPosition = yPosition; 
     newSquare->orientation = orientation; 
     newSquare->width = width;
     return newSquare; 
}

void SquareFinalize(SquareRef theSquare) 
{
     if(NULL == theSquare) return;
     free((void *)theSquare); 
}

Comparison and Accessors are handled with these functions:

bool SquareEqual(SquareRef theSquare1, SquareRef theSquare2) 
{
     if(!ShapeEqual((ShapeRef) theSquare1, (ShapeRef) theSquare2)) return false;
     if(theSquare1->width != theSquare2->width) return false;
     return true;
}

float SquareGetXPosition(SquareRef theSquare) 
{
     return ShapeGetXPosition((ShapeRef) theSquare); 
}

float SquareGetWidth(SquareRef theSquare) 
{
     if(NULL == theSquare) return nan(NULL);
     return theSquare->width;
}

void SquareSetXPosition(MutableSquareRef theSquare, float xPosition) 
{
     ShapeSetXPosition((MutableShapeRef) theSquare, xPosition); 
}

void SquareSetWidth(MutableSquareRef theSquare, float width) 
{
     if(NULL == theSquare) return;
     theSquare->width = width; 
}

Notice how we type cast a Square into a Shape before calling Shape methods.

Usage of Square

    MutableSquareRef square = MutableSquareCreate(0.0, 0.0, 0.0, 10.0);
    ShapeShow((ShapeRef) square);
    SquareShow(square);
  
    ShapeTranslate((MutableShapeRef) square, 10.0, 20.0);
    ShapeRotate((MutableShapeRef) square, 180.);
    SquareShow(square);
    SquareFinalize(square);

Source code used in this section:

Shape.c
Shape.h
Square.c
Square.h
main.c