When Apple announced Swift 2 at the World Wide Developers Conference (WWDC) in 2016, they also declared that Swift was the world’s first protocol-oriented programming (POP) language. From its name, we might assume that POP is all about protocol; however, that would be a wrong assumption. POP is about so much more than just protocol; it is actually a new way of not only writing applications but also thinking about programming.
This article is an excerpt from the book Mastering Swift, 6th Edition by Jon Hoffman.
In this article, we will discuss a protocol-oriented design and how we can use protocols and protocol extensions to replace superclasses. We will look at how to define animal types for a video game in a protocol-oriented way.
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 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 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 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.
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 LandAnimal
, SeaAnimal
, and AirAnimal
protocols. These protocols will define the requirements for the land
, sea
, 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.
Upgrade your knowledge and become an expert in the latest version of the Swift programming language with Mastering Swift 5.3, 6th Edition by Jon Hoffman.
Jon Hoffman has over 25 years of experience in the field of information technology. He has worked in the areas of system administration, network administration, network security, application development, and architecture. Currently, Jon works as an Enterprise Software Manager for Syn-Tech Systems.