Introduction To Protocol Oriented Programming

Protocol Oriented Programming has gained a lot of traction and become a buzz word in the Swift community over the last couple of years. Some love it and some hate it, but what is it really? What problems is it supposed to solve? And how does it relate to our beloved Object Oriented Programming?

What Is Protocol Oriented Programming?

Protocol Oriented Programming is not, although its name might suggest otherwise, a competitor or substitute for Object-Oriented Programming. It’s a way to think about a very specific problem set, that will help you create a flexible, maintainable and easy-to-read code. It should be thought of more as a complement to the Object Oriented approach (in fact, Object Oriented Programming already incorporates a number of the central ideas in Protocol Oriented Programming).

We will start by looking into how protocols (or interfaces, as they’re called in many other languages) can help us with encapsulation and information hiding. Take a look at this example of a Car and a Carrier class:

class Car {
var position: CGPoint
var isLocked = true

init(position: CGPoint) {
self.position = position
}

public func move(x: CGFloat, y: CGFloat) {
self.position.x += x
self.position.y += y
}

public func lock() {
self.isLocked = true
}

public func unlock() {
self.isLocked = false
}
}

class Carrier {
var position: CGPoint
var loadedCars = [Car]()

init(position: CGPoint) {
self.position = position
}

public func move(x: CGFloat, y: CGFloat) {
self.position.x += x
self.position.y += y
self.loadedCars.forEach { (car) in
car.move(x: x, y: y)
}
}

public func itsWeirdThatACarrierCanDoThis() {
self.loadedCars.forEach { (car) in
car.unlock()
}
}
}

Pay special attention to the last method in our Carrier class. That’s weird, right? A carrier should not be able to unlock the cars it’s transporting. This is a side effect of the Carrier class knowing exactly what it’s transporting, and therefore gaining access to all public methods in those objects. We could implement a Vehicle super class to the Car and put the move logic there, but if the Vehicle class also contains other methods, we may be stuck with the same kind of problem.

Now let’s look at a Protocol Oriented solution to this problem. We start by dividing our classes into their smallest, manageable building blocks, and put them into protocols that collect properties and methods that fit well together. For this particular situation we could split the functionality into three different protocols — Moveable, Lockable and Loadable.

protocol Moveable: AnyObject {
var position: CGPoint { get }
func move(x: CGFloat, y: CGFloat)
}

protocol Lockable: AnyObject {
var isLocked: Bool { get }
func lock()
func unlock()
}

protocol Loadable: AnyObject {
func load(_ thing: Moveable)
func unload(_ thing: Moveable)
}

This seems like it could do the trick. We’ve factored out all the properties and methods that should be publicly available and turned them into protocols that handle functionality that is well separated. Note that both Moveable.position and Lockable.isLocked are read-only properties. We choose this design because the both of these properties may be of interest to other instances, but only the object itself should be able to manipulate them (or choose how others manipulate them, through method implementations). We are going to solve the implementation of this by making them both Computed Properties, and declaring a private backing storage for them.

Now that we’re done specifying the functionality in the protocols, we refactor our classes to conform to the appropriate protocols and we hide everything that is not supposed to be available from outside the classes:

class Car: Moveable, Lockable {
private var _position: CGPoint
private var _isLocked = true

public var isLocked: Bool {
return self._isLocked
}

public var position: CGPoint {
return self._position
}

init(position: CGPoint) {
self._position = position
}

public func move(x: CGFloat, y: CGFloat) {
self._position.x += x
self._position.y += y
}

public func lock() {
self._isLocked = true
}

public func unlock() {
self._isLocked = false
}
}

class Carrier: Moveable, Loadable {
private var _position: CGPoint
private var loadedStuff = [Moveable]()

public var position: CGPoint {
return self._position
}

init(position: CGPoint) {
self._position = position
}

public func move(x: CGFloat, y: CGFloat) {
self._position.x += x
self._position.y += y
self.loadedStuff.forEach { (thing) in
thing.move(x: x, y: y)
}
}

public func load(_ thing: Moveable) {
self.loadedStuff.append(thing)
}

public func unload(_ thing: Moveable) {
self.loadedStuff = self.loadedStuff.filter({ $0 !== thing })
}

// This method no longer works,
// because a Moveable object has no unlock() method.
//
// public func itsWeirdThatACarrierCanDoThis() {
// self.loadedStuff.forEach { (thing) in
// thing.unlock()
// }
// }

}

Notice that when we let the Carrier deal with objects that implement the Moveable protocol, it lost access to all methods and properties that does not concern moving something around. What makes this approach even better is that it brings extensibility to our code. Our carrier no longer has a clue of exactly what it’s moving around, so as long as we let new classes implement the Moveable protocol, we can haul cars, boats, furniture… Pretty much anything that makes sense to move, all without changing a single line of code in our Carrier class.

You may also remember from previous articles that structs and Bools are Value Types. Our Computed Properties are able to return the backing storage variable directly because of the fact that a Value Type will be copied every time it’s passed off somewhere, meaning that we will never have any alias problems with our positions.

Are There Any More Advantages To Protocols?

I’m glad you asked.
Protocols are not all about information hiding. Our second example shows how protocols (together with generics) can make your programs more flexible, as well as reduce the amount of code you need to write. Take a look at this:

class Arithmetic {
static func add(a: Int, b: Int) -> Int {
return a + b
}
}

This is a very contrived example, but I think it will get the point across.
What you see is a method performing a simple addition and returning the result, and it’s not doing anything wrong per se.

However, when we look closer we realize that we’re going to need to write a lot of these to cover all the different types of numbers that you may want to add together. Floats will need a method, Doubles will need one, etc. We want to implement a single function that accepts several different types and provides a correct answer, but we also want to specify some constraints on what those types can be (it wouldn’t, for example, make sense to try to add two Strings in this situation). Let’s try to refine this method definition in our next example:

class Arithmetic {
static func add<Type: Numeric>(a: Type, b: Type) -> Type {
return a + b
}
}

Beautiful! We now have a function that’s using a protocol as a generic constraint. This allows us to handle any number of types with the same method, the condition being that the type must implement the Numeric protocol and thereby allow us to perform arithmetic operations on it.

Hopefully, this article brought some new insights and maybe sparked a few ideas about how to use Protocol Oriented Programming in your own projects. Feel free to comment if you have questions, and follow to get notifications about future articles.