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
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
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
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
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.
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
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
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
Using this, we can define a
save function, and invoke it using
UserDefaults as follows:
1 2 3 4 5 6
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
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
I hope you can agree with me that using protocols this way makes testing a cinch.
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
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.
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
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.
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 🙏🏿