Mastering Swift's Development Blog

Follow Us On Twitter
  • Jon Hoffman

Protocol-Oriented Programming with Protocol-Oriented Design

Updated: Sep 5


I am going to take a quick break from my posts on Protocol-Oriented Design Patterns, and write a quick post on what Protocol-Oriented Design means to us. Note added after I finished writing this post: I really did mean for this to be a quick, short, post but as you can see it really wasn’t that quick or short but there is so much that goes into a Protocol-Oriented design. I hope you enjoy this post and find it helpful.


There are a lot of blog posts dedicated to Protocol-Oriented programming and a lot of these take a very simplistic approach to explaining it. By that last statement I mean that these posts oversimplify the design by focusing on putting the requirements into a protocol and then having value types, like structs, adopt them. This does show the basics, and great for simple examples, but we can do so much more with the protocol in Swift and what are the design advantages of a protocol-oriented design. In fact we can do so much more that we were able to write a four-part series on the protocol.


To fully understand protocol-oriented design, it is essential that we understand protocol inheritance, composition and extensions. If you are not sure what those are or just want to brush up on the concepts, you can read about them in our Swift Protocols Part 2: Protocol Inheritance and Composition post and in our Swift Protocol 3: Protocol Extension post. So what is protocol-oriented design.



What is Protocol-Oriented Design

Protocol Oriented Design is the process of breaking up the application architecture into problem areas and then using protocol-oriented methodologies and concepts to design the solutions. That sounds like quite the mouth full and full of technobabble. To put it more simply, protocol-oriented design is the process of using protocol-oriented techniques to architect our application. Still not sure what protocol-oriented design is, well read on to see.


In the infamous “Crusty Talk” at WWDC 2015, David Abrahams said “Don’t start with the class. Start with the Protocol”. In our humble opinion, we would not say that is totally the right approach however, the protocol is at the center of protocol-oriented design. David Abrahams apparently didn’t totally agree with that statement either.


David Abrahams later made the following statement on Twitter “Use value types, then if you need polymorphism, make them conform to a protocol”. This clarification does ring truer to us and, once again in our humble opinion, is really the foundation of Protocol Oriented Design but even this statement oversimplifies it. The ability to break our requirements into reusable chunks, to us, is really at the heart of protocol-oriented design, and this is where protocol inheritance and composition come in. This is one of the areas we will focus on in this post.


The best way to understand this is to look at an example.


Requirements for Our Example

For the example, we are developing a video game that will have numerous animals that may attack. Here are the requirements for our animals:

  • We will have three categories of animals: land, sea and air.

  • An animal may be a member of one or more of these categories.

  • An animal will only be able to move to tiles that match one of the categories that they are in.

  • Animals will start off with a certain number of damage points and when their damage points reach 0 or less, then the animal will be dead.

  • For our small example here, we will create a Tiger (land animal) and Alligator (land and sea animal)

Now that we have our basic requirements, and before we look at how we would design this using a protocol-oriented approach, let’s see how we would design it with an object-oriented approach.


Animal Types Using an Object-Oriented Design

We will start off by creating a diagram of our design in order to give us a visual of what we will be building. This diagram shows our diagram:



This diagram shows that we will have one superclass named Animal and two subclasses named Alligatorand Tiger. We may think, that with the three categories, would need to create a larger class hierarchy where the middle layer contains the classes for the Land, Air and Sea animals however that is not possible with our requirements. The reason this is not possible is because animal types can be members of multiple categories and with a class hierarchy each class can have only one super class. This means that our Animal super class will need to contain the code required for each of the three categories. Let’s take a look at the code for the Animal super class.

class Animal {
    internal var landAnimal = false
    internal var seaAnimal = false
    internal var airAnimal = false
    internal var damagePoints = 0
    
    func isLandAnimal() -> Bool {
        return landAnimal
    }
    func isSeaAnimal() -> Bool {
        return seaAnimal
    }
    func isAirAnimal() -> Bool {
        return airAnimal
    }
    func doLandAttack() {}
    func doLandMovement() {}
    func doSeaAttack() {}
    func doSeaMovement() {}
    func doAirAttack() {}
    func doAirMovement() {}
    
    func takeDamage(damage: Int) {
        damagePoints -= damage
    }
    func damagePointsRemaining() -> Int {
        return damagePoints
    }
    func isAlive() -> Bool {
        return damagePoints > 0 ? true : false
    }
}

I am not going to go over this class line by line, just point out the major flaws with this design. The first is we are using Boolean values to define what type of animal this is (Land, Sea and/or Air) and if it can move/attack on air, sea or land. This can be error prone when we are defining our subclasses.


Another flaw with this design is every animal subclass receives the functionality for each type of animal. As an example, we are able to call the doAirAttack() function on a sea animal. In our subclasses if we define an animal as a sea animal, we will need to know that we only need to override the doAirMovemement() anddoAirAttack() functions. This can be error prone as well, if we either define the type of animal wrong or if we override the wrong functions.


Now let’s look at how we would implement the Tiger and Alligator types

class Tiger: Animal {
    override init() {
        super.init()
        landAnimal = true
        damagePoints = 25
    }
    override func doLandAttack() {
        print("Lion Attack")
    }
    override func doLandMovement() {
        print("Lion Move")
    }
}

class Alligator: Animal {
    override init() {
        super.init()
        landAnimal = true
        seaAnimal = true
        damagePoints = 40
    }
    override func doLandAttack() {
        print("Alligator Land Attack")
    }
    override func doLandMovement() {
        print("Alligator Land Move")
    }
    override func doSeaAttack() {
        print("Alligator Sea Attack")
    }
    override func doSeaMovement() {
        print("Alligator Sea Move")
    }
}

This design and code definitely works and is what object-oriented developers are used to doing. We are also use to being careful when we create subclasses to make sure we are overriding the correct functions and setting the correct variables but with protocol-oriented design we have a better solution.


Animal Types Using an Object-Oriented Design

Just like the object-oriented design we will start off by creating a diagram of our design in order to give us a visual of what we will be building. This diagram shows our diagram:


As we can see, the protocol-oriented design is quite different than the object-oriented design. In this design we are using three techniques that make protocol-oriented design quite different from object-oriented design. These techniques are protocol inheritance, protocol composition and protocol extensions.


With protocol inheritance a protocol can inherit the requirements from one or more other protocols. In this design the LandAnimal, SeaAnimal and AirAnimal all inherit the requirements from the Animal protocol.


With protocol composition a type can adopt one or more protocol. This is a key difference from the object-oriented design and enables us to divide the requirements between LandAnimal, SeaAnimal and AirAnimalprotocols rather than putting all requirements into one superclass.


Protocol extensions are arguably one of the most important parts of the protocol-oriented programming paradigm. They enable us to provide a default implementation for common functionality to all types that conform to a protocol. This helps eliminate a lot of duplicate code.


Now let’s look at how we would implement this design. With a protocol-oriented design we will start off with the protocol.

protocol Animal {
    var damagePoints: Int { get set }
}

In our Animal protocol, we are only defining the damagePoints property. We may think that we are missing a lot of the common functionality that we saw in the Animal superclass in the previous section. This functionality would include the functions takeDamage(), damagePointsRemaining() and isAlive() but we do not want to define them within the protocol itself instead we want to implement them in the Animal protocol extension like this.

extension Animal {
    mutating func takeDamage(_ damage: Int) {
        damagePoints -= damage
    }
    func damagePointsRemaining() -> Int {
        return damagePoints
    }
    func isAlive() -> Bool {
        return damagePoints > 0 ? true : false
    }
}

We will now create the LandAnimal, SeaAnimal and AirAnimal protocols. These protocols will use protocol inheritance to inherit the requirements from the Animal protocol which means any type that adopts these protocols will are receive the functionality defined in the Animal protocol extension.

protocol LandAnimal: Animal {
    func doLandMovement()
    func doLandAttack()
}

protocol SeaAnimal: Animal {
    func doSeaMovement()
    func doSeaAttack()
}

protocol AirAnimal: Animal {
    func doAirMovement()
    func doAirAttack()
}

One of the biggest advantages that we get with this design, of the object-oriented design, is the ability to break out the requirements into separate protocols. With this, each animal only receives the requirements to implement the functionality for their type of animal. Where in the object-oriented design, every animal received the functionality for aid, land and sea.


Now let’s implement the Tiger and Alligator types using our protocol-oriented approach.

struct Tiger: LandAnimal {
    var damagePoints = 25

    func doLandMovement() {
        print("Tiger Move")
    }
    func doLandAttack() {
        print("Tiger Attack")
    }
}

struct Alligator: LandAnimal, SeaAnimal {
    var damagePoints = 40
    
    func doLandMovement() {
        print("Alligator Land Move")
    }
    func doLandAttack() {
        print("Alligator Land Attack")
    }
    func doSeaMovement() {
        print("Alligator Sea Move")
    }
    func doSeaAttack() {
        print("Alligator Sea Attack")
    }
}

If we had an array of animal types, you may be wondering how we would be able to tell which animals could fly and which could swim. For this we could attempt to typecast the instance using the as? Keyword. Let’s briefly look at this.

var animals = [Animal]()
animals.append(Alligator())
animals.append(Alligator())
animals.append(Tiger())

for (index, animal) in animals.enumerated() {
    if let _ = animal as? AirAnimal {
        print("Animal at \(index) is Air animal")
    }
    if let _ = animal as? LandAnimal {
        print("Animal at \(index) is Land animal")
    }
    if let _ = animal as? SeaAnimal {
        print("Animal at \(index) is Sea animal")
    }
}

In this code we start off by creating an array of animals. We then loop through them using a for loop. For this example we want to get the index of where the animal is in the array, but normally we would just need a normal for loop. We then try to typecast the animal instance to each of the animal types. If you run this code the results would be:

Animal at 0 is Land animal
Animal at 0 is Sea animal
Animal at 1 is Land animal
Animal at 1 is Sea animal
Animal at 2 is Land animal

As we can see, the animals at index 0 and 1 are both Land and Sea animals while the animal at index 2 is only a Land animal


What About the Enum

When we talk about preferring value types over reference types, one thing that we cannot ignore, with Swift, is the enum. The enum in swift is far more powerful than it is in most other languages. In fact, an enum can adopt a protocol, be extended using extensions and can be treated generally as a type like a struct or class. Let’s take a quick look at this.


In this example we will be creating animal types again therefore we will start off with our protocol:

protocol Animal {
    func speak()
}

Now let’s create three types that will adopt this protocol

struct Dog: Animal {
    func speak() {
        print("Bark")
    }
}
struct Cat: Animal {
    func speak() {
        print("Meow")
    }
}
struct Lion: Animal {
    func speak() {
        print("roar")
    }
}

Finally let’s create an enum that will contain a case for each of our animal types.

enum AnimalTypePet {
    case dog
    case cat
    case lion
}

This is generally how enums work in most languages, where they just contain a group of constants or cases, but with Swift we can do so much more. For example if we wanted to have the ability to return an instance of the correct animal type, we could simply add a function to the enum like this:

enum AnimalTypePet {
    case dog
    case cat
    case lion

    func getAnimal() -> Animal {
        switch self {
        case .dog:
            return Dog()
        case .cat:
            return Cat()
        case .lion:
            return Lion()
        }
    }
}

With this function added we could get an instance of the correct animal type, based on the case selected like this:

var myPet = AnimalTypePet.dog
myPet.getAnimal().speak()

The results of the code would be “Bark” since that is how a dog speaks. We would not even have to add the function to the enum itself, because enums can be extended with extensions. We could add this function through an extension like this:

extension AnimalTypePet {
    func getAnimalType() -> Animal {
        switch self {
        case .dog:
            return Dog()
        case .cat:
            return Cat()
        case .lion:
            return Lion()
        }
    }
}

You may be asking yourself “If this post is on Protocol-Oriented design, where is the protocol?”. Well enums, like classes and structs can adopt a protocol. We could create a protocol like this:

protocol AnimalType {
    func getAnimal() -> Animal
}

And then have our original AnimalTypePet enum adopt it like this:

enum AnimalTypePet: AnimalType {
    case dog
    case cat
    case lion

    func getAnimal() -> Animal {
        switch self {
        case .dog:
            return Dog()
        case .cat:
            return Cat()
        case .lion:
            return Lion()
        }
    }
}

We could then create other animal type enums and use the interface provided by the protocol to interact with them.


We are only scratching the surface on what an enum can do in Swift as that deserves a whole post to itself. The major take away from this section is not to forget about the enum when you are thinking about value types however be mindful to use enums as they are intended to be used.


What about Generics

Generics are a big part of Swift itself and should be consider as part of our Protocol-Oriented designs but this post is already quite long so for now I will refer you to our post on Swift Generics and also, more importantly, to our post on Associated types with Protocols.


Conclusion

If we put the code for our object-oriented design next to the code for our protocol-oriented design, we would see how much cleaner the protocol-oriented approach is. The protocol-oriented code would also have better performance and would be safer in a multi-threaded environment because it uses value types rather than reference types but these topics are for a different post because this one is already very long.


If you are still primarily using classes with a class hierarchy in your design, you might want to ask yourself why. Remember what David Abrahams said “Use value types, then if you need polymorphism, make them conform to a protocol”.


We really only scratched the surface of what protocol-oriented design is in this post and we plan on writing additional posts on the subject, hopefully not ones that are as long as this one. We hope you found this post useful in explaining what the basics of Protocol-Oriented Design is to us.


masteringSwift.jpeg

Mastering Swift 5.3

The sixth edition of this bestselling book, updated to cover through version 5.3 of the Swift programming language

Amazon

Packt

pop.jpeg

Protocol Oriented Programming

Embrace the Protocol-Oriented design paradigm, for better code maintainability and increased performance, with Swift.

Amazon

Packt