Understanding the Foundation of Protocol-oriented Design

When we develop applications, we usually have a set of requirements that we need to develop against. With that in mind, let’s define the requirements for the animal types that we will be creating in this article:

  • We will have three categories of animals: land, sea, and air.
  • Animals may be members of multiple categories. For example, an alligator can be a member of both the land and sea categories.
  • Animals may attack and/or move when they are on a tile that matches the categories they are in.
  • Animals will start off with a certain number of hit points, and if those hit points reach 0 or less, then they will be considered dead.

POP Design

We will start off by looking at how we would design the animal types needed and the relationships between them. Figure 1 shows our protocol-oriented design:

Figure 1: Protocol-oriented design

In this design, we use three techniques: protocol inheritance, protocol composition, and protocol extensions.

Protocol inheritance

Protocol inheritance is where one protocol can inherit the requirements from one or more additional protocols. We can also inherit requirements from multiple protocols, whereas a class in Swift can have only one superclass.

Protocol inheritance is extremely powerful because we can define several smaller protocols and mix/match them to create larger protocols. You will want to be careful not to create protocols that are too granular because they will become hard to maintain and manage.

Protocol composition

Protocol composition allows types to conform to more than one protocol. With protocol-oriented design, we are encouraged to create multiple smaller protocols with very specific requirements. Let’s look at how protocol composition works.

Protocol inheritance and composition are really powerful features but can also cause problems if used wrongly.

Protocol composition and inheritance may not seem that powerful on their own; however, when we combine them with protocol extensions, we have a very powerful programming paradigm. Let’s look at how powerful this paradigm is.

Protocol-oriented design — putting it all together

We will begin by writing the Animal superclass as a protocol:

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

In the Animal protocol, the only item that we are defining is the hitPoints property. If we were putting in all the requirements for an animal in a video game, this protocol would contain all the requirements that would be common to every animal. We only need to add the hitPoints property to this protocol.

Next, we need to add an Animal protocol extension, which will contain the functionality that is common for all types that conform to the protocol. Our Animal protocol extension would contain the following code:

extension Animal {
    mutating func takeHit(amount: Int) {
        hitPoints -= amount
    }
    func hitPointsRemaining() -> Int {
        return hitPoints
    }
    func isAlive() -> Bool {
        return hitPoints > 0 ? true : false
    }
}

The Animal protocol extension contains the same takeHit()hitPointsRemaining(), and isAlive() methods. Any type that conforms to the Animal protocol will automatically inherit these three methods.

Now let’s define our LandAnimalSeaAnimal, and AirAnimal protocols. These protocols will define the requirements for the landsea, and air animals respectively:

protocol LandAnimal: Animal {
    var landAttack: Bool { get }
    var landMovement: Bool { get }

    func doLandAttack()
    func doLandMovement()
}
protocol SeaAnimal: Animal {
    var seaAttack: Bool { get }
    var seaMovement: Bool { get }
    
    func doSeaAttack()
    func doSeaMovement()
}
protocol AirAnimal: Animal {
    var airAttack: Bool { get }
    var airMovement: Bool { get }
    
    func doAirAttack()
    func doAirMovement()
}

These three protocols only contain the functionality needed for their particular type of animal. Each of these protocols only contains four lines of code. This makes our protocol design much easier to read and manage. The protocol design is also much safer because the functionalities for the various animal types are isolated in their own protocols rather than being embedded in a giant superclass. We are also able to avoid the use of flags to define the animal category and, instead, define the category of the animal by the protocols it conforms to.

In a full design, we would probably need to add some protocol extensions for each of the animal types, but we do not need them for our example here.

Now, let’s look at how we would create our Lion and Alligator types using protocol-oriented design:

struct Lion: LandAnimal {
    var hitPoints = 20
    let landAttack = true
    let landMovement = true
    func doLandAttack() {
         print(“Lion Attack”)
    }
    func doLandMovement() {
         print(“Lion Move”)
    }
}

struct Alligator: LandAnimal, SeaAnimal {
    var hitPoints = 35
    let landAttack = true
    let landMovement = true
    let seaAttack = true
    let seaMovement = true

    func doLandAttack() {
        print(“Alligator Land Attack”)
    }
    func doLandMovement() {
        print(“Alligator Land Move”)
    }
    func doSeaAttack() {
        print(“Alligator Sea Attack”)
    }
    func doSeaMovement() {
        print(“Alligator Sea Move”)
    }
}

Notice that we specify that the Lion type conforms to the LandAnimal protocol, while the Alligator type conforms to both the LandAnimal and SeaAnimal protocols. As we saw previously, having a single type that conforms to multiple protocols is called protocol composition and is what allows us to use smaller protocols, rather than one giant monolithic superclass.

Both the Lion and Alligator types originate from the Animal protocol; therefore, they will inherit the functionality added with the Animal protocol extension. If our animal type protocols also had extensions, then they would also inherit the function added by those extensions. With protocol inheritance, composition, and extensions, our concrete types contain only the functionality needed by the particular animal types that they conform to.

Since the Lion and Alligator types originate from the Animal protocol, we can use polymorphism. Let’s look at how this works:

var animals = [Animal]()

animals.append(Alligator())
animals.append(Alligator())
animals.append(Lion())

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

In this example, we create an array that will contain Animal types named animals. We then create two instances of the Alligator type and one instance of the Lion type that are added to the animals array. Finally, we use a for-in loop to loop through the array and print out the animal type based on the protocol that the instance conforms to.

Related Posts

Comments are closed.

© 2024 Software Engineering - Theme by WPEnjoy · Powered by WordPress