Protocol Oriented Programming

There was a really great talk at WWDC this year around Protocol-Oriented Programming in Swift. It did get me thinking though about how this is different from what we have today in ObjC, or even in a language like C

There is also a really good blog post talking about some of this by Marcel Weiher that makes for some good reading as well.

Ok, so the heart, as I understood the talk, is about thinking of your types as in the form of protocols instead of base classes. The fundamental idea is to get rid of one of the really nasty problems of OOP – implicit data sharing. That's great because that problem sucks.

It turns out, we can do this today in ObjC with one caveat – default protocol implementations. This is a feature that is new with Swift 2.0 and apparently wasn't worth bringing back to ObjC.

This code is inspired heavily by the Crustacean demo app that goes along with the WWDC video.

ObjC

Let's start with the renderer protocol:

@protocol KSRenderer <NSObject>
- (void)moveTo:(CGPoint)position;
- (void)lineTo:(CGPoint)position;
- (void)arcAt:(CGPoint)center
       radius:(CGFloat)radius
   startAngle:(CGFloat)startAngle
     endAngle:(CGFloat)endAngle;
@end

Ok. that looks simple enough.

Our sample KSTestRenderer will look like this:

@interface KSTestRenderer : NSObject<KSRenderer>
@end

@implementation KSTestRenderer
- (void)moveTo:(CGPoint)position
{
    printf("  moveTo(%f, %f)\n", position.x, position.y);
}

- (void)lineTo:(CGPoint)position
{
    printf("  lineTo(%f, %f)\n", position.x, position.y);
}

- (void)arcAt:(CGPoint)center
       radius:(CGFloat)radius
   startAngle:(CGFloat)startAngle
     endAngle:(CGFloat)endAngle
{
    printf("  arcAt(center: (%f, %f), radius: %3.2f,"
           " startAngle: %3.2f, endAngle: %3.2f)\n",
          center.x, center.y, radius, startAngle, endAngle);
}
@end

Alright, all looking good so far. Now it's time for all of the shape code.

@protocol KSDrawable
- (void)draw:(id<KSRenderer>)renderer;
@end

@interface KSPolygon : NSObject<KSDrawable>
@property (copy, nonatomic) NSArray<NSValue *> *corners;
@end

@implementation KSPolygon
- (instancetype)init
{
    if (self = [super init]) {
        _corners = [[NSMutableArray<NSValue *> alloc] init];
    }

    return self;
}

- (void)draw:(id<KSRenderer>)renderer
{
    printf("polygon:\n");
    [renderer moveTo:[_corners.lastObject pointValue]];
    for (NSValue *value in _corners) {
        [renderer lineTo:[value pointValue]];
    }
}
@end

@interface KSCircle : NSObject<KSDrawable>
@property (assign) CGPoint center;
@property (assign) CGFloat radius;
@end

@implementation KSCircle
- (void)draw:(id<KSRenderer>)renderer
{
    printf("circle:\n");
    [renderer arcAt:_center radius:_radius startAngle:0.0f endAngle:M_PI * 2];
}
@end

@interface KSDiagram : NSObject<KSDrawable>
@property (copy, nonatomic) NSArray<id<KSDrawable>> *elements;
- (void)add:(id<KSDrawable>)other;
@end

@implementation KSDiagram
- (instancetype)init
{
    if (self = [super init]) {
        _elements = [[NSMutableArray alloc] init];
    }

    return self;
}

- (void)add:(id<KSDrawable>)other
{
    [(NSMutableArray *)_elements addObject:other];
}

- (void)draw:(id<KSRenderer>)renderer
{
    for (id<KSDrawable> drawable in _elements) {
        [drawable draw:renderer];
    }
}
@end

Finally, we get to the usage code:

KSCircle *circle = [[KSCircle alloc] init];
circle.center = CGPointMake(187.5f, 333.5f);
circle.radius = 93.75f;

KSPolygon *triangle = [[KSPolygon alloc] init];
triangle.corners = @[ [NSValue valueWithPoint:CGPointMake(187.5f, 427.25f)],
                      [NSValue valueWithPoint:CGPointMake(268.69f, 286.625f)],
                      [NSValue valueWithPoint:CGPointMake(106.31f, 286.625f)] ];

KSDiagram *diagram = [[KSDiagram alloc] init];
[diagram add:circle];
[diagram add:triangle];

KSTestRenderer *renderer = [[KSTestRenderer alloc] init];
[diagram draw:renderer];

The output of this code is:

circle:
  arcAt(center: (187.500000, 333.500000), radius: 93.75, startAngle: 0.00, endAngle: 6.28)
polygon:
  moveTo(106.309998, 286.625000)
  lineTo(187.500000, 427.250000)
  lineTo(268.690002, 286.625000)
  lineTo(106.309998, 286.625000)

Of course, one of the "big wins" was the ability retro-fit a class to make apply conformance to some protocol. This is done in ObjC through a class extension.

First, we'll update the protocol so that it has something that can be opted-into. Note that this is where ObjC and Swift deviate. In ObjC we'll have to mark the selector as optional because we cannot define a default implementation for it.

@protocol KSRenderer <NSObject>
- (void)moveTo:(CGPoint)position;
- (void)lineTo:(CGPoint)position;
- (void)arcAt:(CGPoint)center
       radius:(CGFloat)radius
   startAngle:(CGFloat)startAngle
     endAngle:(CGFloat)endAngle;

@optional
- (void)circleAt:(CGPoint)center
          radius:(CGFloat)radius;
@end

Next, and again because Swift has a feature that ObjC doesn't, we need to update the draw: selector on the KSCircle class to handle selectively calling this optional circleAt:radius:.

- (void)draw:(id<KSRenderer>)renderer
{
    printf("circle:\n");
    if ([renderer respondsToSelector:@selector(circleAt:radius:)]) {
        [renderer circleAt:_center radius:_radius];
    }
    else {
        [renderer arcAt:_center radius:_radius
             startAngle:0.0f endAngle:M_PI * 2];
    }
}

Lastly, a new extension to the KSTestRenderer is in order to retro-fit the class with new functionality.

@interface KSTestRenderer (Hacky)
@end

@implementation KSTestRenderer (Hacky)
- (void)circleAt:(CGPoint)center radius:(CGFloat)radius
{
    printf("  circleAt(center: (%f, %f), radius: %3.2f)\n",
           center.x, center.y, radius);
}
@end

Now the output of our program is this:

circle:
  circleAt(center: (187.500000, 333.500000), radius: 93.75)
polygon:
  moveTo(106.309998, 286.625000)
  lineTo(187.500000, 427.250000)
  lineTo(268.690002, 286.625000)
  lineTo(106.309998, 286.625000)

As you can see, the way in which a circle is rendered is now different because of our class extension. Making a CGContextRef would be as straight forward as applying the protocol extension to it and implementation the methods as done in Swift, except… that is a Swift-only feature too that extensions can be applied to non-ObjC types.

C

Things are a little more interesting in the C is unable to extend classes without creating a new type. Though… I'm not sure that is necessarily a bad thing (more on that later).

Anyhow, here's the full (very rough) code sample for C

struct Renderer {
    virtual void moveTo(CGPoint position) = 0;
    virtual void lineTo(CGPoint position) = 0;
    virtual void arcAt(CGPoint center, CGFloat radius, CGFloat startAngle, CGFloat endAngle) = 0;

    virtual void circleAt(CGPoint center, CGFloat radius) {
        arcAt(center, radius, 0.0f, M_PI * 2);
    }
};

struct TestRenderer : public Renderer {
    void moveTo(CGPoint position) {
        printf("  moveTo(%f, %f)\n", position.x, position.y);
    }

    void lineTo(CGPoint position) {
        printf("  lineTo(%f, %f)\n", position.x, position.y);
    }

    void arcAt(CGPoint center, CGFloat radius, CGFloat startAngle, CGFloat endAngle) {
        printf("  arcAt(center: (%f, %f), radius: %3.2f, startAngle: %3.2f, endAngle: %3.2f)\n",
               center.x, center.y, radius, startAngle, endAngle);
    }
};

struct Drawable {
    virtual void draw(Renderer &renderer) = 0;
};

struct Polygon : public Drawable {
    std::vector<CGPoint> corners;

    void draw(Renderer &renderer) {
        printf("polygon:\n");
        renderer.moveTo(corners.back());
        for (auto p : corners) { renderer.lineTo(p); }
    }
};

struct Circle : public Drawable {
    CGPoint center;
    CGFloat radius;

    void draw(Renderer &renderer) {
        printf("circle:\n");
        //renderer.arcAt(center, radius, 0.0f, M_PI * 2);
        renderer.circleAt(center, radius);
    }
};

struct Diagram : public Drawable {
    std::vector<Drawable *> elements;
    void add(Drawable *other) {
        elements.push_back(other);
    }

    void draw(Renderer &renderer) {
        for (auto e : elements) {
            e->draw(renderer);
        }
    }
};

struct TestRendererExtension : public TestRenderer {
    void circleAt(CGPoint center, CGFloat radius) {
        printf("  circle(center: (%f, %f), radius: %3.2f)\n", center.x, center.y, radius);
    }
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Circle circle;
        circle.center = CGPointMake(187.5f, 333.5f);
        circle.radius = 93.75f;

        Polygon triangle;
        triangle.corners.push_back(CGPointMake(187.5f, 427.25f));
        triangle.corners.push_back(CGPointMake(268.69f, 286.625f));
        triangle.corners.push_back(CGPointMake(106.31f, 286.625f));

        Diagram diagram;
        diagram.add(&circle);
        diagram.add(&triangle);

        TestRenderer renderer;
        diagram.draw(renderer);
    }
    return 0;
}

The above code shows the default implementation of circleAt that can be applied to all types that inherit this base class.

NOTE: A protocol (or interface) is really just a definition of functionality with no member data.

In the C

TestRendererExtension renderer;
diagram.draw(renderer);

Wrapping Up

I think the important takeaway from the Swift talk is not really about a "new paradigm" of programming, but rather showing a better way to compose software using techniques that we already use day-to-day. It makes it easier to do the better thing (get rid of accidental data sharing between types) and reducing the boiler-plate code required to do it.

There is one thing I'm worried about though: the class extension seems to be creating a very similar problem in that we are getting rid of unintentional data sharing between type hierarchies and replacing it with potentially unintentional functional changes in our programs.

Imagine a more complicated set of protocols and types interacting and along comes a protocol extension for a type that overrides a default function for the protocol and now your output went from:

circle:
  arcAt(center: (187.500000, 333.500000), radius: 93.75, startAngle: 0.00, endAngle: 6.28)

To this:

circle:
  circleAt(center: (187.500000, 333.500000), radius: 93.75)

To me, this is smelling a lot like the fundamental issue we saw with data sharing between types – unintended side-effects. For this reason alone, I think I prefer the C

Protocol Oriented Programming