Protocol-Oriented Programming

This article is for anyone who wants to learn more about Object-Oriented Programming and Protocol-Oriented Programming. The provided examples are written in Swift but they should be easy to understand for anyone 😉.

If you’re a developer then you most likely know about the Object-Oriented Programming (OOP) paradigm. The OOP paradigm has been used in many software projects over the last few decades and it has proven itself to be robust and useful – or has it? It’s true that OOP brings many benefits like encapsulation, polymorphism access control and so on which is probably why it became so popular – but there are many problems with OOP that should be addressed:

  • Inheritance: OOP brings you a strong feature called inheritance which sounds nice but often translates itself in huge spaghetti code. You’ll most likely end up with something called a God class which holds all the responsibilities. Joe Armstrong, the author of the Erlang programming language, had the following to say about OOP:

“The problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.”

– Joe Armstrong.

  • State: An object can have multiple owners because OOP works with reference types and not value types. This means that other owners can change the state of your object while you’re working with that object – which results in unexpected behaviour especially when you’re multithreading. One of my favourite examples about state/mutability is from Doug Gregor at the WWDC 2015:

“We create a home instance, we create a temperature instance.

We set our thermostat to a balmy 75 degrees Fahrenheit. Now we decide, oh, it’s getting close to dinnertime and I want to bake some salmon.

So I set my oven to 425 degrees and hit Bake. Walk away. Why is it so hot in here? What’s going on? You know what happened. We hit this case of unintended sharing.”

– Doug Gregor

  • Tight coupling: Most classes will probably be tight-coupled because of OOP. This makes writing tests for them really difficult.
  • Breakable code:  If you make a change in the base class you’ll have a high chance of new bugs appearing in your application. This happens because because the author of the subclass needs to know how the base class is implemented.

Apple came up with a solution for these problems by introducing the Protocol-Oriented Programming (POP) paradigm at the 2015 WWDC. This paradigm solves all the issues which OOP brings and it also comes with it’s own extra benefits.  It’s essential that you understand POP since it’s a great alternative for OOP. One extra bonus is that POP isn’t limited to Swift only – so let’s get started!

This article discusses an alternative approach on writing your code using the Protocol-Oriented programming paradigm. The following topics are discussed:

  1. What is Protocol-Oriented Programming?
  2. What is a protocol?
  3. The use case.
  4. The problem.
  5. The solution.
  6. The benefits.

If you only want to implement POP then skip straight to 5. The solution.

1. What is Protocol-oriented programming?

I could provide a complicated explanation on what Protocol-Oriented Programming is but it really comes down to this: With POP you’re communicating with an abstraction – not an implementation. That might sound confusing so I added a great example from Steve Jobs which explains POP perfectly:

“Objects are like people. They’re living, breathing things that have knowledge inside them about how to do things and have memory inside them so they can remember things. And rather than interacting with them at a very low level, you interact with them at a very high level of abstraction, like we’re doing right here. Here’s an example: If I’m your laundry object, you can give me your dirty clothes and send me a message that says, “Can you get my clothes laundered, please.” I happen to know where the best laundry place in San Francisco is. And I speak English, and I have dollars in my pockets. So I go out and hail a taxicab and tell the driver to take me to this place in San Francisco. I go get your clothes laundered, I jump back in the cab, I get back here. I give you your clean clothes and say, “Here are your clean clothes.” You have no idea how I did that. You have no knowledge of the laundry place. Maybe you speak French, and you can’t even hail a taxi. You can’t pay for one, you don’t have dollars in your pocket. Yet I knew how to do all of that. And you didn’t have to know any of it. All that complexity was hidden inside of me, and we were able to interact at a very high level of abstraction. That’s what objects are. They encapsulate complexity, and the interfaces to that complexity are high level.” – Steve Jobs

So you communicate with your objects through an abstraction – also known as an interface or protocol.  Every object (in this case laundry objects) has it’s own complex logic incapsulated inside of it and they all communicate to you through the interface. You don’t know about the complexity of each objects since all you do is communicate with the abstraction.  I made a diagram that visualises the example (the objects are the laundry cleaner objects/people from the Steve Jobs example):

 

2. What is a Protocol?

With protocols you define an interface you want a type to satisfy. When a type satisfies the protocol it’s said to conform to the protocol.

In other words: If you have a type (class, struct, enum) that implements the requirements from the protocol it’s said that your type conforms to the protocol. In Swift you define a protocol with the protocol syntax but in other languages like Java you’d use the interface syntax. In the example in the previous section all three objects conform to the interface/protocol.

3. The use case.

Let’s say you have some data which you want to print in a tabular format to your console in Xcode. This table displays the column on the first line and the rows on the consecutive lines under the column:

First name, Last name // COLUMN
Haris, Pekaric // ROW
Edin, Dzeko // ROW
Lionel, Messi // ROW
Zlatan, Ibrahimovic //ROW

The code for printing this table is probably wrapped in a method which takes certain arguments to construct the table:

func printTabular(column: String, row: String) {
    print(column)
    print(row)
}

The printTabular method is the method which prints the given data in a tabular format. The method requires a column and a row of type String as arguments:

printTabular(column: “First name, Last name”, row: “Haris, Pekaric”)

The method takes the given arguments and prints it to the console in a tabular format:

First name, Last name
Haris, Pekaric

So far so good! 😄

4. The problem.

In reality, we have lots of classes and structs in our codebase. At one point you will most likely want to print your types (classes and structs) in a tabular format. Let’s say we have the struct Person and the struct House:

Person

struct Person {
    var firstName: String
    var lastName: String
}

 

House

struct House {

    let name: String
    var persons = [Person]()

    init(name: String) {
        self.name = name
    }

    mutating func add(_ person: Person) {
        self.persons.append(person)
    }
}

We have a House struct with the property persons. We want to print the person array in a tabular format to the console. To achieve this we need to change our printTabular method to accept House as an argument instead of a String:

func printTabular(_ house: House) {
    print(house.persons)
}

There are multiple problems with this solution:

1: The old implementation of printTabular was generic: It could print any String data. The updated printTabular method isn’t generic and is only able to work with House objects.

2: For every type you want to export (Cars, Animals, and so on), you’ll have to create a new method which takes the type as an argument and and also creates the table format for that specific type. The table format is how the strings are presented in the table: uppercased, lowercased, camel-cased and so on. An example:

func printTabular(_ car: Car) {
    print(car.name.lowercased())
}

func printTabular(_ animal: Animal) {
    print(animal.name.uppercased())
}

This implementation is bad because it’s against DRY (Don’t repeat yourself)! 

3: The printTabular method should only be worried about presenting the table data and not handle the format of the data. When we look at printTabular(_ car: Car) and printTabular(_ animal: Animal) above we can see that the name is either uppercased or lowercased.

4: You don’t want a method to do more things than its intended to do (Separation of concerns). The printTabular method should only print the data and not format it.

So without protocols you’ll most likely end up with multiple methods for multiple types. Each method will specify how the rows and columns for that type are formatted – which is a bad design pattern.

5. The solution.

The solution is to implement POP – the printTabular will communicate with an abstraction/protocol so that the method doesn’t need to know about any specific types like House. With the use of a protocol we maintain a single printTabular method which is able to present tabular data for any conforming type. Every type will specify how the rows and columns should be formatted.

To implement POP we use the following steps.

  1. Define the protocol.
  2. Conform to the protocol.
  3. Implement the protocol.
  4. Update code to use the protocol.

1. Define the protocol

Let’s get started by defining the protocol:

protocol TabularPrintable {
    var column: String { get }
    var row: String { get }
}

The protocol lists two requirements: A column and a row property of the type String. Every type that wants to conform to this protocol has to implement these properties and specify their implementation of column and row.

2. Conform to the protocol

In our case we want to make the House object TabularPrintable so we will make it conform to the TabularPrintable protocol:

In our code we tell the House object to conform to the TabularPrintable protocol:

struct House: TabularPrintable {

}

The House struct now says that it will satisfy the TabularPrintable protocol. The code won’t compile at this point because  House doesn’t conform to the protocol since the requirements of the protocol aren’t implemented yet. We will implement the requirements in the next step.

3. Implement the protocol.

Since the TabularPrintable protocol requires the conforming type to implement its methods and properties House now has to implement the column and row properties.

struct House: TabularPrintable {

    let name: String
    var persons = [Person]()

    // Implementing required property from the protocol
    var column: String {
        return “First name, Last name”
    } 

    // Implementing required property from the protocol
    var row: String {
        var row = “”

        for person in persons {
            row += “\(person.firstName), \(person.lastName) \n”
        }

        return row
    }

    init(name: String) {
        self.name = name
    }

    mutating func add(_ person: Person) {
        self.persons.append(person)
    }
}

Since House now implements all the requirements from the TabularPrintable protocol its said that House conforms to the TabularPrintable protocol.

4. Update code to use protocol.

So far we only created the protocol and made House conform to it but we still can’t print House to the console. We need to change the printTabular method so it accepts types that conform to the  TabularPrintable protocol:

func printTabular(_ tabularPrintable: TabularPrintable) {
    print(tabularPrintable.column)
    print(tabularPrintable.row)
}

The printTabular method takes a TabularPrintable as the argument. This means that any type that conforms to TabularPrintable can be passed into this method.

The method can access the column and row property without knowing which specific type has been passed in as an argument (House? Animal? Car?). These properties are exposed because it’s guaranteed that every TabularPrintable will have these properties available due to the protocol. Notice that the name property of House isn’t available since this property isn’t defined in the TabularPrintable protocol.

We can now print the House object:

var house = House(name: “MyHouse”)
house.add(Person(firstName: “Haris”, lastName: “Pekaric”))
house.add(Person(firstName: “Edin”, lastName: “Dzeko”))

printTabular(house)

It works 🎉!

The diagram below illustrates the entire POP flow in detail. The diagram is the same as the one in the Steve Jobs example but updated for this example.

The method communicates with the interface and not with the type. You can add and remove types without modifying the method since the method only communicates with the interface and not with a specific implementation/type. This is also known as “Program to interfaces, not implementations”.

6. The benefits.

1. Because of POP the printTabular method became more generic: It’s able to print any type that conforms to the protocol.

2. You won’t have to change the printTabular method if you later decide to use totally different structs for your printTabular method. The printTabular is only concerned about the interface and not about the implementation. This is a huge benefit which I’ll discuss in a more advanced POP blog post.

3. The printTabular method only does what it’s required to do: print the data. It doesn’t care about the format of the data e.g. uppercase the entire row or lowercase it. With POP every type specifies its own custom format for its rows and columns (separation of concerns).

4. The benefit of POP is that you’re using an interface which means you don’t have to use classes anymore. This brings a huge benefit since a struct is a value type and not a reference type.

5. POP goes well with dependency inversion. I’ll discuss this in a more advanced POP blog post.

You could also solve this problem using the OOP approach by defining a base class and have subclasses inherit from the base class. In the end it’s up to you which paradigm you’ll implement: there is no silver bullet for architecting your code and it all depends on your specific use case. However, my personal experience is that 90% of the time you’d be better off with POP.

Don’t forget to leave a comment below if you liked this article and/or have feedback/questions!

About the author

Avatar
Haris Pekaric

Designing and developing Swift apps with ❤️.

Add comment