Most Recent

At WWDC 2015, in a very influential session titled Protocol-Oriented Programming in Swift, Dave Abrahams explained how Swift’s protocols can be used to overcome some shortcomings of classes. He suggested this rule: “Don’t start with a class. Start with a protocol”.

To illustrate the point, Dave described a protocol-oriented approach to a primitive drawing app. The example worked from a few of primitive shapes:

These are value types. That eliminates many of the problems of an object-oriented approach:

Instances aren’t shared implicitly

The reference semantics of objects add complexity when passing objects around. Changing a property of an object in one place can affect other code that has access to that object. Concurrency requires locking, which adds tons of complexity.

No problems from inheritance

Reusing code via inheritance is fragile. Inheritance also couples interfaces to implementations, which makes reuse more difficult. This is its own topic, but even OO programmers will tell you to prefer “composition over inheritance”.

No imprecise type relationships

With subclasses, it’s difficult to precisely identify types. e.g. with NSObject.isEqual(), you must be careful to only compare against compatible types. Protocols work with generics to precisely identify types.

To handle the actual drawing, a Renderer protocol was added that describes the primitive drawing operations:

This made it possible to define different renderers that worked easily with the given types. A main selling point was the ability to define a test renderer, which let you verify drawing by comparing strings:

I think that approach is pretty compelling. It’s much more testable. It also allows us to interpret the data differently by providing separate renderers. And value types neatly sidestep a number of problems that an object-oriented version would have.

But I think there’s a better way to write this code.

Despite the improvements, logic and side effects are still tightly coupled in the protocol-oriented version. Polygon.draw does 2 things: it converts the polygon into a number of lines and then renders those lines. So when it comes time to test the logic, we need to use TestRenderer—which, despite what the WWDC talk implies, is a mock.

We can separate logic and effects here by turning them into separate steps. Instead of the Renderer protocol, with move, line, and arc, let’s declare value types that represent the underlying operations.

With the value-oriented approach, we can take our Set<Path> and transform it directly. Say you wanted to flip the result horizontally. You calculate the size and then return a new Set<Path> with flipped coordinates.

In the protocol-oriented approach, it would be somewhat difficult to transform our drawing steps. To flip horizontally, you need to know the final width. Since that width isn’t known ahead of time, you’d need to write a Renderer that (1) saved all the calls to move, line, and arc and then (2) pass it another Render to render the flipped result.

(This theoretical renderer is creating the same boundary we created with the value-oriented approach. Step 1 corresponds to our .paths method; step 2 corresponds to draw(Set<Paths>).)

Easily inspect data while debugging

Say you have a complex Diagram that isn’t drawing correctly. You drop into the debugger and find where the Diagram is drawn. How do you find the problem?

If you’re using the protocol-oriented approach, you’ll need to create a TestRenderer (if it’s available outside your tests) or you’ll need to use a real renderer and actually render somewhere. Inspecting that data will be difficult.

But if you’re using the value-oriented approach, you only need to call paths to inspect this information. Debuggers can display values much more easily than effects.

The boundary adds another semantic layer, which opens up additional possibilities for testing, transformation, and inspection.

I’ve used this approach on a number of projects and found it immensely helpful. Even with a simple example like the one given here, values have a number of benefits. But those benefits become much more obvious and helpful when working in larger, more complex systems.

If you’d like to see a real world example, check out PersistDB, the Swift persistence library I’ve been working on. The public API presents Querys, Predicates, and Expressions. These are reduced to SQL.Querys, SQL.Predicates, and SQL.Expressions. And each of those is reduced to a SQL, a value representing some actual SQL.