At WWDC 2015, Apple introduced the concept of Protocol-Oriented Programming(POP) in Swift. This is an alternative methodology of programming to Object-Oriented Programming(OOP) that is used in most modern languages today. Both POP & OOP are concerned with eliminating code duplication, decoupled code, and re-usability but approach it in fundamentally different way.
What is Object-Oriented Programming?
Object-Oriented Programming focuses first and foremost on objects as the main building blocks of your project. These objects have a special ability called inheritance, which allows them to adopt the properties and methods of whatever object they inherit from. Object-Oriented inheritance uses a cascading model where you subclass more generic or abstract classes to add increasingly specific and concrete functionality. This allows you to share common functionality built into the parent class to be shared, without duplication, across its children subclasses. There really is nothing wrong with this approach – it has been in use for a long time with great results.
However, there are several issues we run into with OOP. One, our subclass’s are often many layers deep in a hierarchy and sometimes inherit functionality we would prefer to be implemented differently. We might be able to override that functionality, but it can have adverse effects if we don’t truly understand the original implementation. Often we don’t have access to the source code of the object if it is not our own, making it difficult to accurately understand any potential side-effects. In other cases the function might not be able to overridden at all. Another issue is we can only inherit from one class. This can lead to classes who try to do too many things by accounting for everything a potential subclass might need (breaking the Single Responsibility Principle). Finally objects are passed around by reference. This makes them inherently unsafe to use in multi-threaded environments and force us to build a bunch of protection around them to ensure the object’s state does not change out from under us.
What is Protocol-Oriented Programming?
Protocol-Oriented Programming uses more of a decorator pattern, where instead of a deep hierarchy of inherited properties and functionality, we build an object or struct up from different protocols. You can conform to as many protocols as you want so it is very easy for each protocol adhere to the SRP. Swift also has a feature where you can extend protocols to create default implementation for some of the methods. If you choose to write your own implementation you don’t have to worry about how it might affect things deeper in the hierarchy as you do with inheritance.It acknowledges different objects might need to respond to the same method but implement them differently. This is especially useful when we want call the same method on anything that conforms to your protocol but allow them to implement it in a way that makes sense.
The main benefit of POP in Swift is it makes it much easier to work with structs. Structs are passed by value, meaning a new copy is made anytime a struct is passed around. This means they are inherently thread safe, saving you from having a bunch of protection for multi-threading as you do with objects. This can lead to faster, cleaner, and more efficient code.
The Object Oriented Way
So lets take a look at what this might look like. Recently I took my kids to the circus and I must still have it on the mind, as I will be using circus animals to illustrate these concepts.
First lets look at how we might model a circus animal under the OOP paradigm.
A circus animal is first and foremost an animal. So we create the Animal class. For simplicity is has only one property: species, which describes what type of animal it is. I decided to make AnimalType an enum with the various species of animal I want to have. Of course we have to create an initializer as all properties must be set by the time the object is initialized in Swift.
Circus animals must be able to perform, but not all animals can or should perform so that functionality should not be in the Animal class. This is the perfect time for a subclass, let’s subclass Animal to make the CircusAnimal class:
For our purposes the main thing a CircusAnimal needs to be able to do that an Animal can’t is perform. So we make CircusAnimal a subclass of Animal and give it a perform function. For clarity I have created a new type Act, which is just a string. You need an Act to perform right? 😉
Alright now we have a generic CircusAnimal defined, let’s create a CircusElephant:
Our CircusElephant should be able to do everything a CircusAnimal can, so we make it a subclass of CircusAnimal. At this point we know the species of the animal is going to be elephant so we can set that in the initializer. Really, the only thing this class gets us is an automatically AnimalTyped CircusAnimal, but it is nice for clarity and ease of creating multiple CircusElephants.
Finally we can use our CircusElephant class to make an object who we will call Elle:
Elle is an amazing elephant whose Act is to balance on a ball. She knows how to perform() her act because she is a CircusElephant who is a child of CircusAnimal.
This all looks pretty good!
Then we realize our Circus doesn’t only have animals who perform(), but people as well. We could just add a human case to our AnimalType to accommodate this and create a subclass of CircusAnimal called CircusPerformer whose species = AnimalType.human. Then we can call perform() on both a CircusAnimal & CircusPerformer without issue. But what if the way a human performs is different than an elephant? What if we have a something that is not animal that performs…like a robot? Now we have to duplicate code.
Well we can solve this in OOP with a protocol: /*We are starting to head in a Protocol Oriented direction already*/
This is nice! Now anything that wants to perform() just needs to conform to the Performer protocol. I have also made Act and associatedtype so that each Performer can determine what Type of Act they can perform().
We should make our CirusAnimal a Performer – I like to put my protocol conformance in an extension for clarity: /*This is a little strange here since that leaves our CircusAnimal Class empty*/
This is good. Our CircusAnimal gets to determine what Type of Act it can perform() and how it performs it. The Performer protocol can also be applied to a new class, like CircusRobot easily.
Now this is an oversimplified example, but looking at it we realize we could use structs for our CirucusElephant instead of an object. Two CircusElephants with the exact same values as each other are completely interchangeable. This is a good case for a struct. Generally a struct is more efficient than an object and much safer.
The Protocol Oriented Way
Structs cannot inherit so we need a different approach – Protocol-Oriented Programming. Instead of starting to build out our code with objects, we start with protocols:
We will use the same AnimalType enum. Animal can be made into a protocol with one property of species. All properties declared in a protocol must be variables and explicitly state if they are get only or get and set.
We can use the same Performer protocol from before:
A CircusAnimal is really just a combination of the Animal and Performer protocols so we can represent those two together with a typealias:
Now we can create our CircusElephant struct that conforms to our CircusAnimal protocol:
This looks pretty good! We have a struct version Elle who can do everything the object version can.
But then we start creating other CircusAnimals like the CircusTiger and CircusLion and we realize they all perform() in the same way. Wouldn’t be nice if we didn’t have to duplicate the implementation for perform() for each one?
Say hello to protocol extensions. Protocol extensions allow us to add common functionality to a protocol:
Now all Performer’s know how to perform() the same way…but that is not how our CircusAnimals perform(). They specifically need to have the AnimalType and they need the Act to be of Type String. The great thing about protocol extensions are they are easy and safe to overwrite. You never have to worry about if you need to call super or not.
Let’s change our CircusAnimal so it can have an Act of type String first. We can’t add that to a typealias, so we need to make CircusAnimal a legitimate protocol:
To accomplish this we make CircusAnimal a protocol that adopts Animal & Performer. All it does is explicitly type Act as a String. /*Act’s Type can still be overwritten by something that conforms to CircusAnimal if there were a case where String doesn’t cut it. But if it is not a String it must be StringLiteralConvertible or perform() must be implemented differently for that Performer.*/
Now we can make a new extension for Performer which is only for CircusAnimals with the where clause:
This default implementation of perform() will only be used when the Performer is also a CircusAnimal. In that situation we know it will have a species and the Act will be a String.
Now we can take of the duplicate code of out our CircusElephant struct so we end up with this:
And if we want to overwrite the implementation of perform() for a different animal we simply add the perform() function in that struct and it will use its own implementation of perform() instead of the implementation defined in the extension.
Hopefully this shows how flexible POP can make your code. It makes adhering to SRP much easier, and I find it makes my code easier to read and understand. This is a simple introduction to the concept and I am just getting started with it myself. I am still mostly seated in the OOP way of thinking but trying to move in the POP direction. When I sit down to create something new, I still think about what class or classes I need to make first. In POP we should be thinking about what protocols we might need. I am not there yet but I am excited to continue exploring this way of programming.
Please add your comments and thoughts below. Let me know about your experience and any errors or oversights I have made here.