top of page

Mastering Swift's Development Blog

Follow Us On Twitter
  • Writer's pictureJon Hoffman

Swift Protocols Part 2: Protocol Inheritance and Composition

Updated: Sep 4, 2022


Protocols are the core on which the Swift programming language is built upon. Understanding how they work and what makes them so powerful is essential to understanding the language itself. The protocol is so important to the Swift language that we have created this four-part series on it. In this post we will dive deep into protocol inheritance and composition, while really looking at how and when to use each of them. Here are links to the other posts in this series:



Protocol Inheritance

Protocol inheritance is where one protocol inherits the requirements from one or more other protocols. This is quite similar to class inheritance in Object Oriented Programming but instead of inheriting functionality, like class inheritance, protocols inherit requirements. Let’s look at this by starting with a FullName protocol and adding an Age protocol. We will then create a Person protocol which will inherit the requirements from both protocols.

protocol FullName {
    var firstName: String { get set }
    var lastName: String { get set }
    
    func getName() -> String
}

protocol Age {
    var age: Int { get set }
}

protocol Person: FullName, Age {

}

When a protocol inherits from another protocol, the syntax is the same as when a type adopts a protocol. The list of protocols that are being inherited are listed after the definition of the protocol itself therefore in the above example the Person protocol is inheriting the requirements from the FullName and Age protocols as well as having the requirements defined within the protocol itself. This means that any type that adopts the Person protocol must implement the requirements defined in the FullName and Age protocols.


Let’s expand this example out a little bit to demonstrate why protocol inheritance can be so useful. Let’s add the following two protocols to our example.

protocol BasketballSkillRatings {
    var dribbling: Int { get set }
    var shooting: Int { get set }
}

protocol BaseballSkillRatings {
    var hitting: Int { get set }
    var throwing: Int { get set }
}

We can now use protocol inheritance to create BasetballPlayer and BaseballPlayer protocols like this:

protocol BasketballPlayer: Person, BasketballSkillRatings {

}

protocol BaseballPlayer: Person, BaseballSkillRatings {

}

Notice how we use the Person protocol as well as the skill protocols when we are defining both the BasketballPlayerand BaseballPlayer protocols. This enables us to create reusable protocols that can be combined to form more useful protocols. This prevents us from having to redefine requirements within various different protocols. One word of warning, while protocol inheritance does enable us to make more granular protocols and then combine them as we showed in this example, do not overdo the granularity. Making too many small protocols can become a maintenance nightmare.


For example, we would not want to define the FullName protocol like this:

protocol LastName {
    var lastName: String { get set }
}
protocol FirstName {
    var firstName: String { get set }
}
protocol FullName: LastName, FirstName {
    
    func getName() -> String
}

Once again, defining our protocols at this level of granularity could end up being a maintenance nightmare. Think about if we had to make a change to one of these protocols and we had a hierarchy that was six or seven layers deep. We would need to untangle which protocol was inheriting requirements from the protocol we want to change to know where we need to make the changes. Protocol extensions can help us with this, and we look at them in the third part of this series but I would warn you, as a generally rule, that we don’t make our protocols too granular.


I would also warn against making protocols that only have requirements it inherits from other protocols like the Person, BasketballPlayer and BaseballPlayer examples. If you find yourself doing this, you may want to think about using protocol composition.


Protocol Composition

Protocol composition enables our types to adopt more than one protocol. In our previous example we had the BasketballPlayer and BaseballPlayer protocols that inherited requirements from the Person protocol and the two skills protocols. Depending on our design, we may actually want to create the BasketballPlayer and BaseballPlayer concrete types instead using protocol composition. The following example shows how we would create the BasketballPlayer type using protocol composition

struct BasketballPlayer: Person, BasketballSkillRatings {
    var firstName: String
    var lastName: String
    var age: Int
    var dribbling: Int
    var shooting: Int
    
    func getName() -> String {
        return firstName + " " + lastName
    }
}

Notice that protocol composition looks just like protocol inheritance where the list of protocols to adopt are listed after the type definition. In this example we adopt all the requirements from the Person and BasketballSkillRatings protocols.


There really is not a good rule on when to use protocol inheritance and when to use protocol composition, it really depends on your architecture and what works best in your design. One thing I would say, is if you are using protocol inheritance to create larger protocols and you find that you have only one type inheriting from a single protocol, then you may want to see if you are able to use protocol composition instead. Another good rule, is if you have a protocol that only inherits requirements from other protocols, without defining any of its own, then you may want to use protocol composition instead. Let’s take a closer look at this with another example where we use protocol inheritance and composition together.


Using Protocol Inheritance and Composition together


To see how we can use protocol inheritance and composition together, let’s look at how we would model animals for a video game. For this video game, we will need to know the type of animal which is defined by how it moves (walk, swim or fly) and the ways that the animal can attack (bite, claw or fire breathing). We will start off by looking at the animal type hierarchy and how we could design it. This image illustrates a possible option for our design.






In this image we see that the design starts off with a base Animal protocol. This will contain requirements that all animal types would need to adopt. These would be things like how much damage it can take, max speed and nocturnal. We then add three other protocols, SeaAnimal, LandAnimal and AirAnimal, who all adopt the Animal protocol requirements. These protocols would contain requirements that are specific to the sea, land, and air animals. We would do something very similar for our attack types as well. This image illustrates a possible design for our attack types.





In this design we start off with a base Attack protocol. Like the Animal protocol, the Attack protocol would define requirements that are common for all types of attacks. We then define five protocols, which define the five different types of attack, which all inherit from the attack protocol.


With these protocols were now able to begin building our animal types. We could define a few animal types like this:

struct Bear: LandAnimal, ClawAttack, TeethAttack {}
struct Alligator: LandAnimal, SeaAnimal, TeethAttack {}
struct Dragon: LandAnimal, AirAnimal, TeethAttack, ClawAttack, FireAttack {}

In this example we are using protocol inheritance to create a protocol hierarchy for both our animal types and attack types. We then use protocol composition to define the requirements for the animals themselves.


We could create Bear, Alligator and Dragon protocols which inherit the requirements that each animal type needs and then just have the concrete types inherit from one protocol, however we would recommend against doing that. Using protocol composition instead, like what we do in this example, makes our code very easy to read and understand. It also helps us avoid having to make changes to multiple files if we simply want to add or remove one of the protocols to/from an animal.


One of the things that makes the code very easy to read is how the protocols are named. By using the suffix of Animal and Attack, depending on what protocol is being inherited, we know right away if we are inheriting an animal type or an attack type. Take care when naming your protocols to ensure that your code is easy to read because the easier it is to read, the easier it is for you and others to maintain.

To check if an instance of a type conforms to a specific protocol we can use the ‘is’ operator. The ‘is’ operator returns true if an instances is of a type that conforms to a specific protocol. Let’s assume that we have an instance of the Alligator type named gator, in order to see if this instance conforms to the SeaAnimal protocol we could use the following code:

if gator is SeaAnimal {
            // do something
}                      

Protocol inheritance and protocol composition make up one of the pillars of protocol-oriented design. We will be introducing another pillar in part 3 of this series which is protocol extensions. Here is a list of the other posts in this series.




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

bottom of page