24 January 2019
Protocol-oriented programming vs object-oriented in Swift: translating complicated world into simple code
Protocol-oriented programming has been making strides in the Swift community in recent years. It is more of an extension rather replacement of the object-oriented paradigm – a prelude to evolution rather than a revolution. But it still provides tons of benefits for both developers and organizations. Find out all about them in this example-rich introduction to protocol-oriented programming with Swift.
Object-oriented programming is one of the most widely known of all programming paradigms. But it’s not all there is out there. In recent years, the Swift community has been adopting more and more of a protocol-oriented approach. It is neither something all-new and shiny, nor a silver bullet for all problems. Still, it might serve a useful role in structuring the code. In this particular area, I will discuss protocol-oriented programming with Swift today.
Object-oriented approach in Swift
Let’s imagine… (Have you noticed that almost all programming articles start like this?) you were asked to write a game – a quick and easy one, with just two levels. (This is how all the quick and easy projects start, don’t they?). Level one will take place on the ground. Level two will be… the underground hell.
Let’s be object-oriented first. We need to find some similarities and hierarchies and simply model them. We will also need a couple characters: one for the player, one for the land creatures (enemies on the surface) and one to represent the hell monsters. Let’s start with a very basic type
Creature to encapsulate common properties and behaviours.
Enemies on the ground should be able to walk and run in order to chase and fight the player. We can introduce
LandCreature simply by subclassing
Creature and adding extra capabilities.
So far so good. Let’s move on to some hotter areas… Monsters hidden in the deepest pits of hell are very dangerous, as they burn everything they see. When they cannot reach the target with flames, they need to walk or run up to the victim.
At this point, we would probably decide that running and walking are fundamental capabilities of all characters, so we should move them into base class
Creature to remove redundancies. It’s good not to repeat the code, isn’t it?
After some refactoring:
Ladies and gentlemen, meet Lucifer, the
HellCreature. He can burn, walk and fight.
Victory!??? The game is ready. It’s pretty fun to play. It is, I must say, a great success.
At this point, your boss would probably come up with an amazing idea of adding premium level to the game. Yes, the one with the rebellious pilot. Ooooookeeeey, it can’t be that hard, can it?
The rebellious pilot enters the game.
We need a new type of enemy:
SkyCreature. It should be able to
Surprisingly, Kanimoor can walk. What the hell?! Yes, we’ve just moved
walk capabilities to the base class because they seemed common. Unfortunately, it’s no longer the case. Of course, we can override
fatalError() or no action at all. We can also… move
walk() back to
HellCreature. It is the end of the project, after all. One little code duplication has never killed anybody, hasn’t it?
So we might end up with a structure like this:
Finally, Kanimoor can no longer run or walk.
Once again, the game is completed. Well… almost. In the New Year’s edition, there will be yet another extra level to play… against the DRAGON!
I think you can already smell the troubles we are going to encounter…
Wyvern is supposed to walk, run and burn as well. Once again, we can move
run() to the base class… or copy and paste here and there…
Protocol-oriented programming with Swift – introduction
This time, let’s assume we have the opportunity to start the entire project all over again (it is quite a nice perspective, isn’t it?) and take a look at how we can use protocols to structure the whole universum better. To do that, first we have to find out what protocol-oriented programming with Swift is and what it offers. The documentation states that:
„A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality.“
Protocols are similar to interfaces in other languages. Yet, in Swift there are a few unique and very useful features protocols have. First, they can have a default implementation of the required methods. For example:
From now on, every type which adopts the protocol
Running will get the implementation of
run() for free. Of course, you might want to override it sometimes with another implementation of running. When it is not the case, the default should be sufficient and helpful in avoiding code duplication.
Default implementations can also be provided for a selected part of the adopters only. In the following example, every type which will conform to
Walking protocols will gain a nice ability of achieving something
Types can adopt multiple protocols, as they can do multiple things. At the same time, they can only be one thing (inherit only one superclass). Another very important thing is the fact that protocols may be adopted by both reference types (classes) and value types (structs and enumerations), whereas base classes and inheritance are restricted to reference types only. We didn’t touch upon the difference between value and reference semantics here, but I really suggest you check this as well. It is another key feature in Swift that is really worth knowing and using wisely.
Extensions let us model an application’s structure retroactively, instead of forcing us to make all the decisions upfront.
With this fundamental protocol-oriented programming with Swift introduction out of the way, we can start implementing the first two levels of the game. I think it will be useful for the base character class to at least have a name so I will introduce the class
Creature (this time, without any capabilities). I will also extract a fundamental game action:
fight() to the protocol
fight() separately in
Strikeable allows me to supply other game elements with the ability to attack (these may not necessarily be
Creatures). The first level takes place on the ground, where player will fight against
LandCreatures. Their basic actions (besides fighting) are walking and running. I will introduce two protocols to cover them:
With this, declaring
LandCreature is pretty straightforward:
Woolfie receives all of the following actions from the extensions of our protocols:
Let’s go to hell again (?)! Almost all pieces required to build
HellCreature are ready at this point. We still need
burn(), so let’s add a protocol called
Burning, coupled with a default implementation, of course:
Now, let’s bring in
HellCreature and invite Lucifer to the stage.
The Rebellion again – protocols to the rescue
This time, we can accept the game’s success much more easily. We might even be thrilled to go back to the project!
Rebellion in the air zone (third level) will require adding
SkyCreature, which will be able to
fight() with fists by default, it seems there are other fighting methods that are more efficient in the air.
It was quick and clean, wasn’t it? Excited by this quick success, let’s add (by our own choice!) an extra bonus level with a?.
What a great success! Our protocol-oriented creation is good to go!
By using protocols, we were able to compose all of the
Creatures in the game by adding a set of suitable features, instead of creating a stiff hierarchy of classes. In many cases, it’s more flexible to define objects by what they are able to do, rather than what they are.
Here is another useful hint for those of you who are already thinking of delving into the world of protocol-oriented programming. With protocols, we can add convenient properties to check whether a given object can perform a particular task:
What’s awesome about it is that you don’t need to update these values when any of them stops conforming to the protocol. These are computed properties, so the results will alter automatically.
Where is the catch? The risks of protocol-oriented programming with Swift
Just as the object-oriented programming carries the risk of creating a very complex class hierarchy, the protocol-oriented paradigm may cause the structure to grow too much horizontally. Excessive granulation caused by creating many tiny protocols will make maintaining and using the application hard and annoying. For sure, it also requires keeping order in the project so that you don’t lose track of what conformances were added to which classes. At the end of the day, it’s like with any other technique: a balance needs to be found and the problem should drive the solution, not the other way around.
See also: WieBetaaltWat/Splitser success story
In the object-oriented paradigm, we focus on what an object is, while the protocol-oriented approach allows us to focus more on what an object can do, its abilities and behaviours. Our simple game was meant to emphasise differences in structuring decisions during the development process using both of these paradigms.
The object-oriented approach may sound very straightforward, because it is all about finding relevant nouns and creating a hierarchy for them. However, even in this simple game we struggled with so many important decisions that had to be made early in the project. This is why it is not quite as easy as it may appear at first glance.
Protocol extensions and default implementations at first may seem similar to base classes or abstract classes in other languages, but in Swift they play a bigger role. Why?
- Types can conform to more than one protocol.
- They can also obtain default behaviours from multiple protocols.
- Unlike multiple inheritance in other programming languages, protocol extensions do not introduce any additional states.
- Protocols can be adopted by classes, structs and enums, while base classes and inheritance are restricted to class types only.
- Protocols allow retroactive modeling with extensions added to existing types.
What else can I say? „At its heart, Swift is protocol-oriented“ so learn as much as possible from the standard library – it is a great source of knowledge! Observe how the Swift team uses protocols to structure the code, separate concerns, share common algorithms etc. to get yourself inspired. Most importantly, give protocol-oriented programming with Swift a chance and find out yourself how it can improve your workflow.