Fundamental concepts of OOP in Swift and how to implement them

Dmytro Anokhin
7 min readNov 1, 2017

--

For every Object Oriented language three concepts stand out: encapsulation, inheritance, and polymorphism. The concepts are general for programming languages, but strongly associated with Object Oriented Programming. I decided to explore the concepts as they are in Swift/Objective-C and implement them in C.

Some concepts have different forms in programming languages. For each concept I provide short description of what the concepts is.

C

If you’re a C programmer you can skip this part. But it you never touched C code or feel a bit rusty, here’s a quick refresher on language constructions used in example.

C has two files:

  • .h — header file, contains function and data declarations, included by clients code;
  • .c — source code, implementation.

We will work with pointers. Pointer is a typed reference to location in memory. Pointers are declared using asterisk after the type.

int *pointer;

We can create a pointer to anything in memory.

There’s no classes in C, but we have structures.

struct Foo {
int member;
};

Structures can contain only data not functions. But we have pointers to functions.

struct Foo {
int member;
void (*function)(void);
};

Looks familiar if you used blocks in Objective-C.

C requires declaration before use.

struct Foo;
void function(void);

We can create aliases to types for our convenience.

typedef struct Foo *FooRef; // FooRef is now a pointer to Foo
FooRef foo;

We can access members of a structure using dot notation. Accessing pointer value is called dereferencing. For members of a structure we can use -> operator.

*pointer = 123;foo->member = 123;

In C we can allocate and deallocate memory directly, without any memory management mechanism. This is performed using malloc and free functions.

FooRef foo = (FooRef)malloc(sizeof(Foo));
free(foo);

In general C is very focused on working with memory and pointers. Some features are implemented by using well defined memory layout.

Encapsulation

Encapsulation is the mechanism to restrict access to certain data, variables or fields. By restricting access to data we prevent unexpected changes. Encapsulation instead exposes functions to operate on that data.

Encapsulation restricts direct access to data and exposes functions to operate on that data.

Here’s an example to demonstrate the concept. Say I’m working on a drawing app and my drawing are represented by the Canvas class. It contains specific information needed to render (such as size, pixel density, color space, etc.), whether the destination is a display, a printer, or a bitmap.

class Canvas {
/// width of the canvas in pixels
private(set) var width: Int

/// height of the canvas in pixels
private(set) var height: Int
init() {
width = 320
height = 240
}
}

Swift encapsulates data through properties. In Objective-C, property is a combination of instance variable and synthesized accessor method(s).

@interface Canvas : NSObject@property (nonatomic, readonly) int width;
@property (nonatomic, readonly) int height;
@end

In C we use forward declaration for data structure and function in header file and implement them in .c file.

// Canvas.htypedef struct Canvas * CanvasRef;extern int CanvasGetWidth(CanvasRef canvas);
extern int CanvasGetHeight(CanvasRef canvas);
extern CanvasRef CanvasCreate(void);
// Canvas.c
typedef struct Canvas {
int width;
int height;
} Canvas;
int CanvasGetWidth(CanvasRef canvas) {
return canvas->width;
}
int CanvasGetHeight(CanvasRef canvas) {
return canvas->height;
}
CanvasRef CanvasCreate(void) {
Canvas *canvas = (Canvas *)malloc(sizeof(Canvas));
canvas->width = 320;
canvas->height = 240;
return canvas;
};

All three languages provide encapsulation mechanisms. Naturally, encapsulation in Objective-C is very similar to C. With exception that the compiler will synthesize accessor methods for properties at compile time.

Encapsulation mechanism in Swift and Objective-C is weaker than C. This is general problem with any Object Oriented Language. What makes it weaker is necessary relationship between objects. Sometimes we need our inherited objects have access to parent data. This leads to a complex mix of access modifiers.

For Objective-C most concerning is dynamic nature of the language, partially inherited by Swift. Key-Value Coding, for instance, opens encapsulated data to unexpected access without any compiler protection.

Encapsulation in C is most strict. Everything is hidden in .c file, there is no way to access encapsulated data from the outside.

Inheritance

Inheritance allows creating new objects based on existing, inheriting properties and behaviour.

Swift and Objective-C provide inheritance mechanisms. Usually we use inheritance to extend existing type for a specific case, specialize more generic type. In example DisplayCanvas extends Canvas to provide color profile information.

/// The canvas when rendering destination is a display
class DisplayCanvas: Canvas {
/// The color profile of a display
enum ColorSpace {
/// Standard RGB color space
case sRGB
/// Wide gamut RGB (DCI-P3) color space
case wideGamutRGB
}
private(set) var colorSpace: ColorSpace override init() {
colorSpace = .wideGamutRGB
super.init()
}
}

In C we can use composition to simulate inheritance.

// Canvas.htypedef enum ColorSpace {
sRGB = 0,
wideGamutRGB
} ColorSpace;
extern CanvasRef DisplayCanvasCreate(void);
// Canvas.m
typedef struct DisplayCanvas {
Canvas super;
ColorSpace colorSpace;
} DisplayCanvas;
CanvasRef DisplayCanvasCreate(void) {
DisplayCanvas *canvas
= (DisplayCanvas *)malloc(sizeof(DisplayCanvas));
canvas->super.width = 320;
canvas->super.height = 240;
canvas->colorSpace = wideGamutRGB;
return (CanvasRef)canvas;
};

Inheritance in Swift and Objective-C not only allows using existing objects to create new, it is also subtyping mechanism. Subtyping allows using inherited type where the base type can be used, substitute the base type. In 1994 computer scientists Barbara Liskov and Jeannette Wing formulated Liskov Substitution Principle:

Subtype Requirement: Let ϕ(x) be a property provable about objects x of type T. Then ϕ(y) should be true for objects y of type S where S is a subtype of T.

Barbara Liskov and Jeannette Wing

Inheritance allows creating new objects based on existing, inheriting properties and behaviour. We must be able to use the new object where the base object can be used.

For our example we must be able to use DisplayCanvas wherever we can use Canvas.

/// Draws canvas to a destination
func draw(_ canvas: Canvas) {
print("width: \(canvas.width), height: \(canvas.height)")
}
let canvas = DisplayCanvas()
draw(canvas)
width: 320, height: 240

Same works in C.

// Canvas.hextern void CanvasDraw(CanvasRef canvas);
// Canvas.m
void CanvasDraw(CanvasRef canvas) {
printf("width: %d, height: %d\n",
canvas->width, canvas->height);
}

// main.c
CanvasRef canvas = DisplayCanvasCreate();
CanvasDraw(canvas);
width: 320, height: 240

Because of the way C aligns structures in memory, we can cast pointers from one to the other if first elements are the same. If we would change order of members in our structure cast will not work.

typedef struct DisplayCanvas {
ColorSpace colorSpace;
Canvas super;
} DisplayCanvas;
// We can not safely cast DisplayCanvas to Canvas

Another approach to inheritance would be using union and enum in base structure to identify the type.

typedef struct DisplayCanvas {
ColorSpace colorSpace;
} DisplayCanvas;
typedef struct PrintCanvas {
int dpi;
} PrintCanvas;
typedef enum CanvasType {
display = 0,
print
} CanvasType;
typedef struct Canvas {
union {
DisplayCanvas display;
PrintCanvas printer;
} super;

CanvasType type;
} Canvas;

This way we can simulate type(of:) and isMemberOfClass: checks.

Polymorphism

Polymorphism allows different data types to be handled using a uniform interface.

Polymorphism allows values of different data types to be handled using a uniform interface.

We already used polymorphism in our drawing function, that handles different data types. Remember Liskov substitution principle — we are able to use inherited type wherever base type can be used.

What is more interesting is polymorphic behaviour of objects. In Swift and Objective-C we can make drawing routine a method and override it to implementing specific drawing.

class Canvas {
func draw() {
print("width: \(width), height: \(height)")
}
}
class DisplayCanvas: Canvas {
override func draw() {
print("width: \(width), height: \(height), color space: \(colorSpace)")
}
}

let canvas: Canvas = DisplayCanvas()
canvas.draw()
width: 320, height: 240, color space: wideGamutRGB

To replicate this in C we can use approach called dynamic dispatch. You probably already familiar with term because Swift and Objective-C implement it.

Dynamic Dispatch is the process of selecting which implementation of a polymorphic operation (method or function) to call at run time.

wikipedia

We can implement dynamic dispatch using function pointers:

// Canvas.ctypedef struct Canvas {
int width;
int height;
// Pointer to draw function implementation
void (*draw)(struct Canvas *);
} Canvas;
// Draw function is exposed in .h and wraps function call
void CanvasDraw(CanvasRef canvas) {
canvas->draw(canvas);
}
// Base implementation of drawing
void _CanvasDraw(struct Canvas *canvas) {
printf("width: %d, height: %d\n",
canvas->width, canvas->height);
}
// Overriden implementation
void _DisplayCanvasDraw(struct DisplayCanvas *canvas) {
printf("width: %d, height: %d, color space: %d\n",
canvas->super.width, canvas->super.height,
canvas->colorSpace);
}
CanvasRef DisplayCanvasCreate(void) {
DisplayCanvas *canvas
= (DisplayCanvas *)malloc(sizeof(DisplayCanvas));
// When creating canvas we provide function implementation
canvas->super.draw
= (void (*)(struct Canvas *)) &_DisplayCanvasDraw;
// ... return (CanvasRef)canvas;
};

// main.c
CanvasRef canvas = DisplayCanvasCreate();
CanvasDraw(canvas);
width: 320, height: 240, color space: 1

This is very fast implementation because we have direct access to a fuction pointer. But the pointer takes space and with growing number of functions our structure will occupy more memory.

Most common solution is using special data structure Virtual Table to store pointers to functions. This approach is used by both Objective-C (dispatch table) and Swift (witness table).

// Canvas.ctypedef struct CanvasVTable {
void (*draw)(struct Canvas *canvas);
} CanvasVTable;
typedef struct DisplayCanvasVTable {
void (*draw)(struct DisplayCanvas *canvas);
} DisplayCanvasVTable;
const CanvasVTable kCanvasVTable = {
_CanvasDraw
};
const DisplayCanvasVTable kDisplayCanvasVTable = {
_DisplayCanvasDraw
};

We define two virtual tables and for base and inherited structure. We need to replace pointer to a function with pointer to a virtual table.

typedef struct Canvas {
int width;
int height;
const CanvasVTable *vtable;
} Canvas;
CanvasRef DisplayCanvasCreate(void) {

// ...

canvas->super.vtable = (CanvasVTable *) &kDisplayCanvasVTable;
// ... return (CanvasRef)canvas;
};
void CanvasDraw(CanvasRef canvas) {
canvas->vtable->draw(canvas);
}

Alternatively, we can add separate vtable in every structure to be able to execute overridden function.

Swift and Objective-C implement dynamic method resolution by traversing class hierarchy and performing lookup in witness/dispatch tables.

Because of proper encapsulation, all the changes I made in Canvas.c — I never had to modify clients code.

From what you can see, encapsulation, inheritance, and polymorphism, three fundamental concepts of Object Oriented Programming, are not that hard to implement.

In C, inheritance and polymorphism are not that convenient for sure, but encapsulation is even more strict.

In Swift and Objective-C, polymorphism is implemented through inheritance and classes naturally encapsulate data. I tempted to think together they make Object Oriented Programming work.

Demo project in C is on github: https://github.com/dmytro-anokhin/canvas

Cheers!

Thank you for reading. If you like the article, stay tuned and follow me. Hope to see you next time 😊

Show authors more ❤️ with 👏’s

--

--

Dmytro Anokhin
Dmytro Anokhin

Written by Dmytro Anokhin

iOS Developer, here to share best practices learned through my experience. You can find me on Twitter: https://twitter.com/dmytroanokhin

Responses (1)