Please see base link by Anna Widera.
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 typeCreature
to encapsulate common properties and behaviours.
class Creature { | |
let name: String | |
init(name: String) { | |
self.name = name | |
} | |
func fight() { | |
print(“👊”) | |
} | |
} |
view rawOO-Creature-base.swift hosted with ❤ by GitHub
Enemies on the ground should be able to walk and run in order to chase and fight the player. We can introduceLandCreature
simply by subclassingCreature
and adding extra capabilities.
class LandCreature: Creature { | |
func walk() { | |
print(“🚶🏻♀️”) | |
} | |
func run() { | |
print(“🏃🏻”) | |
} | |
} |
view rawOO-LandCreature-base.swift hosted with ❤ by GitHub
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.
class HellCreature: Creature { | |
func walk() { | |
print(“🚶🏻♀️”) | |
} | |
func run() { | |
print(“🏃🏻”) | |
} | |
func burn() { | |
print(“🔥”) | |
} | |
} |
view rawOO-HellCreature-base.swift hosted with ❤ by GitHub
At this point, we would probably decide that running and walking are fundamental capabilities of all characters, so we should move them into base classCreature
to remove redundancies. It’s good not to repeat the code, isn’t it?
After some refactoring:
class Creature { | |
let name: String | |
init(name: String) { | |
self.name = name | |
} | |
func walk() { | |
print(“🚶🏻♀️”) | |
} | |
func run() { | |
print(“🏃🏻”) | |
} | |
func fight() { | |
print(“👊”) | |
} | |
} | |
class LandCreature: Creature { } | |
class HellCreature: Creature { | |
func burn() { | |
print(“🔥”) | |
} | |
} |
view rawOO-Creature-canRunAndWalk.swift hosted with ❤ by GitHub
Ladies and gentlemen, meet Lucifer, theHellCreature
. 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 tofly()
. Easy-peasy.
class SkyCreature: Creature { | |
func fly() { | |
print(“🕊️”) | |
} | |
} |
view rawOO-SkyCreature-base.swift hosted with ❤ by GitHub
Let’s createrebelliousPilot
Surprisingly, Kanimoor can walk. What the hell?! Yes, we’ve just movedrun
andwalk
capabilities to the base class because they seemed common. Unfortunately, it’s no longer the case. Of course, we can overriderun()
andwalk()
inSkyCreature
withfatalError()
or no action at all. We can also… moverun()
andwalk()
back toLandCreature
andHellCreature
. 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:
class Creature { | |
let name: String | |
init(name: String) { | |
self.name = name | |
} | |
func fight() { | |
print(“👊”) | |
} | |
} | |
class LandCreature: Creature { | |
func walk() { | |
print(“🚶🏻♀️”) | |
} | |
func run() { | |
print(“🏃🏻”) | |
} | |
} | |
class HellCreature: Creature { | |
func walk() { | |
print(“🚶🏻♀️”) | |
} | |
func run() { | |
print(“🏃🏻”) | |
} | |
func burn() { | |
print(“🔥”) | |
} | |
} |
view rawOO-LandCreatureHellCreature-redundancy.swift hosted with ❤ by GitHub
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…
class Dragon: SkyCreature { } |
view rawOO-Dragon-base.swift hosted with ❤ by GitHub
Of course,Wyvern
is supposed to walk, run and burn as well. Once again, we can movewalk()
andrun()
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:
protocol Running { | |
func run() | |
} | |
extension Running { | |
func run() { | |
print(“🏃🏻”) | |
} | |
} |
view rawPO-extensions.swift hosted with ❤ by GitHub
From now on, every type which adopts the protocolRunning
will get the implementation ofrun()
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 toPersevering
andWalking
protocols will gain a nice ability of achieving somethingstepByStep()
.
extension Persevering where Self: Walking { | |
func stepByStep() { | |
walk() | |
walk() | |
} | |
} |
view rawPO-extensions-where.swift hosted with ❤ by GitHub
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 classCreature
(this time, without any capabilities). I will also extract a fundamental game action:fight()
to the protocolStrikeable
.
class Creature { | |
let name: String | |
init(name: String) { | |
self.name = name | |
} | |
} | |
protocol Strikeable { | |
func fight() | |
} | |
extension Strikeable { | |
func fight() { | |
print(“👊”) | |
} | |
} | |
extension Creature: Strikeable { } |
view rawPO-Creature-Strikeable.swift hosted with ❤ by GitHub
Declaringfight()
separately in Strikeable
allows me to supply other game elements with the ability to attack (these may not necessarily beCreatures
). The first level takes place on the ground, where player will fight againstLandCreatures
. Their basic actions (besides fighting) are walking and running. I will introduce two protocols to cover them:
protocol Walking { | |
func walk() | |
} | |
extension Walking { | |
func walk() { | |
print(“🚶🏻♀️”) | |
} | |
} | |
protocol Running { | |
func run() | |
} | |
extension Running { | |
func run() { | |
print(“🏃🏻”) | |
} | |
} |
view rawPO-WalkingRunning.swift hosted with ❤ by GitHub
With this, declaringLandCreature
is pretty straightforward:
class LandCreature: Creature, | |
Walking, | |
Running { } |
view rawPO-LandCreature.swift hosted with ❤ by GitHub
LandCreature
Woolfie
receives all of the following actions from the extensions of our protocols:
Let’s go to hell again (?)! Almost all pieces required to buildHellCreature
are ready at this point. We still needburn()
, so let’s add a protocol calledBurning
, coupled with a default implementation, of course:
protocol Burning { | |
func burn() | |
} | |
extension Burning { | |
func burn() { | |
print(“🔥”) | |
} | |
} |
view rawPO-Burning.swift hosted with ❤ by GitHub
Now, let’s bring inHellCreature
and invite Lucifer to the stage.
class HellCreature: Creature, | |
Walking, | |
Running, | |
Burning { } |
view rawPO-HellCreature.swift hosted with ❤ by GitHub
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 addingSkyCreature
, which will be able tofly()
andfight()
. WhileSkyCreature
gainsfight()
with fists by default, it seems there are other fighting methods that are more efficient in the air.
protocol Flying { | |
func fly() | |
} | |
extension Flying { | |
func fly() { | |
print(“🛩️”) | |
} | |
} | |
class SkyCreature: Creature, Flying { | |
func fight() { | |
print(„🏹”) | |
} | |
} |
view rawPO-Flying-SkyCreature.swift hosted with ❤ by GitHub
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?.
class Dragon: Creature, | |
Walking, | |
Running, | |
Flying, | |
Burning { } |
view rawPO-Dragon.swift hosted with ❤ by GitHub
What a great success! Our protocol-oriented creation is good to go!
By using protocols, we were able to compose all of theCreatures
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:
extension Creature { | |
var canFly: Bool { return self is Flying } | |
var canBurn: Bool { return self is Burning } | |
var canWalk: Bool { return self is Walking } | |
} |
view rawPO-canSomething.swift hosted with ❤ by GitHub
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
Conclusion
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.