Your Friendly iOS Coder

Dabby [ダビー]

Protocols: The Good, the Nice, and the Weird

Over the past few weeks, I have been doing some work involving rewriting a ton of obj-c code into swift. Ever since I watched the WWDC video on Protocol Oriented Programming, using protocols and value types (a.k.a structs) in place of classes has always been my go to. This week I encountered an interesting problem that spawned as a result of entangling protocols (which, in my opinion, were designed to provide functionality mainly for value types) with class inheritance. Before I get deeper into my observations, It might be a good time to give an intro to protocols.

Protocols: What are they?

From Apple’s documentation on the Swift programming language, they give the following definition:

A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of those requirements. Any type that satisfies the requirements of a protocol is said to conform to that protocol.

So, what does this mean? I will use an example to explain. Let’s say we were creating a hero vs villains type game, and we had these two structs representing different types of villains: Ghost, and Vampire. (Please excuse my lack of a better example, you can tell I’m clearly not a gamer 😅)

1
2
3
4
5
6
7
8
9
struct Ghost {
    var opacity: CGFloat
    var color: UIColor
}

struct Vampire {
    var canTurnIntoBat: Bool
    var canHandleSunlight: Bool
}

Now, lets say I wanted to add functionality to move these villains around the game world. I would have something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Ghost {
    var opacity: CGFloat
    var color: UIColor

    func move() {
        // Move Ghost
    }
}

struct Vampire {
    var canTurnIntoBat: Bool
    var canHandleSunlight: Bool

    func move() {
        // Move Vampire
    }
}

This is all good so far, but what if I wanted to keep track of all the villains, and move them when particular events occurred in the game? Should I have two separate collections for Ghost and Vampire types? If you think about this, you will notice that it becomes a poor design choice if I decided to add another villain into the picture, as I would have to keep another collection for that type. This is where protocols come in (albeit in their simplest use case). I would want the ability to group all my villain types based on shared behaviour. With that in mind, I can do something along these lines:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protocol Villain {
    func move()
}

struct Ghost: Villain {
    var opacity: CGFloat
    var color: UIColor

    func move() {
        print("Moving Ghost")
    }
}

struct Vampire: Villain {
    var canTurnIntoBat: Bool
    var canHandleSunlight: Bool

    func move() {
        print("Moving Vampire")
    }
}

Note that this is not an inheritance relationship. This reads as: define a Ghost structure that conforms to the Villain protocol, and similarly for Vampire. Now, if I wanted to move all the villains in my game, I could do something like this:

1
2
3
4
5
6
7
8
9
10
11
let villains: [Villain] = [
    Ghost(opacity: 0.5, color: .red),
    Vampire(canTurnIntoBat: true, canHandleSunlight: false)
]

// Will Print:
// Moving Ghost
// Moving Vampire
for villain in villains {
    villain.move()
}

Not only is this a better solution if we added more villains, but it makes the code much simpler to read.

For those of you who are conversant with Java, this might sound like an interface, where you create a set of methods, and classes that implement said interface, must implement all the methods in the interface. You wouldn’t be wrong if you thought so, but protocols are packed with some more functionality:

  • You can specify properties (ivars) that structs/classes that wish to conform to protocol must have
  • If a function in a protocol causes a side effect on the structure that conforms to it, it must be marked as mutating. This is really handy for value types, because a lot of bugs are created due to functions with side effects
  • Finally, you can combine multiple protocols at any point using protocol<> syntax. This enables you to specify that a particular parameter must conform to multiple protocols.

The Good

Protocols in swift were made with a lot of things in mind. One of the major ones I feel is type safety. Protocols in swift are dispatched statically (compile time) instead of dynamically (run time). Please remember this, as this will play a major role in explaining the reason for the weird behaviour I came across. The fact that protocols are dispatched statically prevents us from doing some unsafe type casts, as the compiler is now equipped beforehand to know if a cast is going to fail or not. In our case, (5 as Villain).move() will generate a compile time error, as Int can’t be converted to type Villain. This is really good to know as this prevents a lot of crashes that are caused in obj-c where failed casts could return nil.

The Nice

One nice thing that comes from using protocols is the capability it provides for us to write more testable code. Now I must admit, I was never too keen on testing, but this changed when I recently founded a startup, created the software without writing a single test, then proceeded to get burned when I attempted to write the version 2, but more on this in a later post.

Because protocols offer us the ability to view types as more generic structures that conform to a certain set of behaviours, this enables us to easily mock out complex parts of our code during testing, thereby enabling us to write better unit tests.

Lets assume we were working on an app that needed some form of persistent storage. We can have the following Storage protocol that defines functions for storing and retrieving a dictionary of values.

1
2
3
4
5
6
typealias PList = [String: Any]

protocol Storage {
    func store(data: PList)
    func retrieveData() -> PList?
}

Lets say that in our app, we decide to store all this information in UserDefaults as opposed to using CoreData. We can then have the UserDefaults class conform to our Storage protocol as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
extension UserDefaults: Storage {
    var appKey: String {
        return "Test-App-Key"
    }

    func store(data: PList) {
        UserDefaults.standard.set(data, forKey: appKey)
    }

    func retrieveData() -> PList? {
        guard let storedData = UserDefaults.standard.value(forKey: appKey) as? PList else {
            return nil
        }

        return storedData
    }
}

Using this, we can define a save function, and invoke it using UserDefaults as follows:

1
2
3
4
5
6
func save(data: PList, in storage: Storage) {
    storage.store(data: data)
}

let data: [String: Any] = ["val1" : true, "val2": [1, 2, 3], "val3": 0.5]
save(data: data, in: UserDefaults.standard)

Note that, in this case, our save function is really simple in that it just redirects to the store function in the Storage protocol, but we can imagine a more complex situation where saving the data could throw an error. Then, we would like to handle that in our save function by returning true if the save was successful, or false otherwise. Now, if we wanted to test our save function, we could create a mock for the Storage protocol. This would enable us to focus only on testing our save function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class SaveTest: XCTestCase {
    struct StorageMock: Storage {
        var onStore: (() -> Void)?
        var onRetrieve: (() -> Void)?

        func store(data: PList) {
            onStore?()
        }

        func retrieveData() -> PList? {
            onRetrieve?()
            return nil
        }
    }

    func testSave() {
        let didStoreExpectation = expectation(description: "Should invoke storage store function")
        let storage = StorageMock(onStore: {
            didStoreExpectation.fulfill()
        }, onRetrieve: nil)

        save(data: ["val1": "Testing"], in: storage)
        waitForExpectations(timeout: 0.1, handler: nil)
    }
}

SaveTest.defaultTestSuite.run()

I hope you can agree with me that using protocols this way makes testing a cinch.

The Weird

Now, for the moment you have all been waiting for! Last week, I encountered some really weird behaviour. In order to properly explain what happened, consider the next block of code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
protocol Moveable {
    var point: CGPoint { get set }
    mutating func move(byOffset offset: CGPoint)
}

extension Moveable {
    mutating func move(byOffset offset: CGPoint) {
        print("default move invoked")
        point.x += offset.x
        point.y += offset.y
    }
}

class GameObject: Moveable {
    var point: CGPoint

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

class Bird: GameObject {
    func move(byOffset offset: CGPoint) {
        print("Flapping wings")
    }
}

func teleport(moveable: Moveable, to location: CGPoint) {
    let offset = CGPoint(x: location.x - moveable.point.x, y: location.y - moveable.point.y)
    var mutableMoveable = moveable // allow mutation
    mutableMoveable.move(byOffset: offset)
}

var bird: Bird = Bird(point: CGPoint.zero)
teleport(moveable: bird, to: CGPoint(x: 2.0, y: 3.0))

So here is the question, which of the move functions will get called? The one defined in the extension or the one defined in the Bird class. If you guessed the one in the Bird class, you guessed the same as I did!… which means you are incorrect 😅. It’s the one in the extension that gets called.

But Why?

Remember what I mentioned over here about protocols being statically dispatched? It turns out that if you have a base class conforming to a given protocol via extensions, the derived classes of the base class are unable to override the protocol behaviour defined in the extension. This is because, at compile time, the only information known about a given parameter is whatever type it is defined as. In our case, the compiler doesn’t know that we passed a Bird to the teleport function. Hence, it uses the implementation of move() defined in the extension. So, how do we fix this?

Well, first thing to realize is that the protocol oriented programming being preached by Apple is directly applicable in the value type land, but when you entangle them with complex inheritance behaviours, you could get some weird behaviour, since polymorphism is a run time feature, and protocol conformance is a compile time feature. After looking around on the internet, I found a nice post that gave different approaches to solving this.

In my opinion, the best way to do this is to get rid of the inheritance relationship all together, and just use a struct to represent the Bird class instead. Like this:

1
2
3
4
5
6
7
struct Bird: Moveable {
    var point: CGPoint

	func move(byOffset offset: CGPoint) {
		print("Flapping wings")
	}
}

If you really need to use a class in place of a struct, then explicitly implement the behaviour in the class. That way, subclasses can override the behaviour where necessary.

The End

On a final note, I hope this post made you a bit more comfortable with using protocols in Swift. The major takeaway from this is that if you are thinking about creating an inheritance hierarchy with classes, try using structs and protocols instead. Also, if you already have a complex inheritance hierarchy, and you decide to use protocols with said hierarchy, you will have to be aware of any differences in behaviour due to the fact that protocols are statically dispatched and polymorphism is done dynamically.

Thanks for taking the time 🙏🏿

Find me on twitter or contact me if you have any questions.

Resources

Source code