Swift 3 Protocol-Oriented Programming - Second Edition

4 (1 reviews total)
By Jon Hoffman
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Object-Oriented and Protocol-Oriented Programming

About this book

One of the most important additions to the new features and capabilities of the Swift programming language was an overhaul of Protocols. Protocol-oriented programming and first class value semantics have now become two incredibly powerful concepts at the heart of Swift’s design.

This book will help you understand the difference between object-oriented programming and protocol-oriented programming. It will demonstrate how to work with protocol-oriented programming using real world use cases. You will gain solid knowledge of the different types that can be used in Swift and the differences between value and reference types. You will be taught how to utilize the advanced features of protocol-oriented programming to boost the performance of your applications.

By the end of the book, you will have a thorough understanding of protocol-oriented programming and how to utilize it to build powerful, practical applications.

Publication date:
November 2016
Publisher
Packt
Pages
218
ISBN
9781787129948

 

Chapter 1.  Object-Oriented and Protocol-Oriented Programming

This book is about protocol-oriented programming. When Apple announced Swift 2 at the World Wide Developers Conference (WWDC) in 2015, they also declared that Swift was the world's first protocol-oriented programming language. From its name, we may assume that protocol-oriented programming is all about the protocol; however, this would be a wrong assumption. Protocol-oriented programming is about so much more than just the protocol; it is actually a new way of not only writing applications, but also how we think about programming.

In this chapter, you will learn the following:

  • How Swift can be used as an object-oriented programming language

  • How Swift can be used as a protocol-oriented programming language

  • The differences between object-oriented programming and protocol-oriented programming

  • The advantages that protocol-oriented programming has over object-oriented programming

While this book is about protocol-oriented programming, we are going to start off by discussing how Swift can be used as an object-oriented programming language. Having a good understanding of object-oriented programming will help us understand protocol-oriented programming and also give us some insight into the issues protocol-oriented programming is designed to solve.

 

What is object-oriented programming?


Object-oriented programming is a design philosophy. Writing applications with an object-oriented programming language is fundamentally different than writing applications with older procedural languages, such as C and Pascal. Procedural languages use a set of instructions to tell the computer what to do step-by-step by relying on procedures (or routines). Object-oriented programming, however, is all about the object. This may seem like a pretty obvious statement given the name but essentially, when we think about object-oriented programming, we need to think about the object.

The object is a data structure that contains information about the attributes of the object, in the form of properties, and the actions performed by or to the object, in the form of methods. Objects can be considered things and in the English language they would normally be considered nouns. These objects can be real-world or virtual objects. If you take a look around, you will see many real-world objects and, virtually all of them can be modeled in an object-oriented way with attributes and actions.

As I am writing this chapter, I look outside and see a lake, numerous trees, grass, my dog, and the fence in our backyard. All of these items can be modeled as objects with both properties and actions.

I am also thinking about one of my all-time favorite energy drinks. That energy drink is called Jolt. I'm not sure how many people remember Jolt, but I would not have made it through college without it. A can of Jolt can be modeled as an object with attributes (volume, quantity of caffeine, temperature, and size) and actions (drinking and temperature change).

We could keep the cans of Jolt in a cooler to keep them cold. This cooler could also be modeled as an object because it has attributes (temperature, cans of Jolt, and maximum number of cans) and actions (adding and removing cans).

The object is what makes object-oriented programming so powerful. With an object, we can model real-world items, such as a can of Jolt, or virtual objects, such as characters in a video game. These objects can then interact within our application to model real-world behavior or the behavior we want in our virtual world.

Within a computer application, we cannot create an object without a blueprint that tells the application what properties and actions to expect from the object. In most object-oriented languages, this blueprint comes in the form of a class. A class is a construct that allows us to encapsulate the properties and actions of an object into a single type that models the entity we are trying to represent in our code.

We use initializers within our classes to create instances of the class. We usually use these initializers to set the initial values of the properties for the object, or to perform any other initialization that our class needs. Once we create the instance of a class, we can then use it within our code.

All of this explanation about object-oriented programming is fine, but nothing demonstrates the concepts better than the actual code. Before we can begin coding we will need to define the requirements. In this chapter, we will show how we could design vehicle types for a video game in both an object-oriented and a protocol-oriented way. Let's look at the requirements for the vehicle types.

 

Requirements for the sample code


When we develop applications we usually have a set of requirements that we need to work towards. Our sample projects in this chapter are no different. The following is a list of requirements for the vehicle types that we will be creating:

  • We will have three categories of vehicle: Sea, land, and air

  • A vehicle can be a member of multiple categories

  • Vehicles may move or attack when they are on a tile that matches any of the categories they are in

  • Vehicles will be unable to move to or attack on a tile that does not match any of the categories they are in

  • When a vehicle's hit points reach 0, the vehicle will be considered incapacitated

  • We will need to keep all active vehicles in a single array that we can loop through

For our projects in this chapter, we will be demonstrating the design with only a few vehicles, but we know that the number of vehicle types will grow as we develop the game. In this chapter, we will not be implementing a lot of the logic for the vehicles because our focus is the design and not the code that makes the vehicles move and attack.

Let's start off by looking at how we would design these vehicles in an object-oriented way, and how Swift functions as an object-oriented programming language.

 

Swift as an object-oriented programming language


Swift provides full support for developing applications in an object-oriented way. Actually, prior to Swift 2, I considered Swift primarily an object-oriented language in the same way that I considered Java and C# to be object-oriented languages. In this section, we will be designing the vehicle types in an object-oriented way and seeing the drawbacks of this design.

Before we look at the code, let's create a very basic class diagram that shows how we would design the vehicle class hierarchy for an object-oriented design. I usually start off by doing a very basic diagram that simply shows the classes themselves without much detail. This helps me picture the class hierarchy in my mind. The following diagram shows the class hierarchy for the object-oriented design:

This diagram shows that we have one superclass named Vehicle and five subclasses named Tank, Amphibious, Submarine, Jet, and Transformer. With a class hierarchy, each of the subclasses will inherit all of the properties and methods from the superclass therefore, any common code and properties can be implemented in the Vehicle superclass and all of the subclasses will inherit them.

We may think that, with the three categories (land, air, and sea) in our requirements, we would want to create a larger class hierarchy where the middle layer would contain separate superclasses for land, air, and sea vehicles. This would allow us to separate the code for each category into its own superclass; however, that is not possible with the requirements. The reason this is not possible is that the vehicle types may be members of multiple categories and with a single inheritance language such as Swift, each class can have one and only one superclass. This means that if we created separate land and sea superclasses, for example, then the Amphibious class could be a subclass of either the land or the sea type but not both.

Since Swift is a single inheritance language, and we can have only one superclass for the vehicle classes, that superclass will need to contain the code required for each of the three categories. Having a single superclass such as this is one of the drawbacks of object-oriented design because the superclass can become very bloated.

We will start forming our object-oriented design by creating a TerrainType enumeration that will be used to define the different vehicle, attack, and movement types. The TerrainType enumeration is defined like this:

enum TerrainType { 
    case Land 
    case Sea 
    case Air 
} 

Now let's looks at how we would define the Vehicle superclass and the properties within this class:

class Vehicle { 
    fileprivate var vehicleTypes = [TerrainType]() 
    fileprivate var vehicleAttackTypes = [TerrainType]() 
    fileprivate var vehicleMovementTypes = [TerrainType]() 
     
    fileprivate var landAttackRange = -1 
    fileprivate var seaAttackRange = -1 
    fileprivate var airAttackRange = -1 
     
    fileprivate var hitPoints = 0 
} 

We start the Vehicle type off by defining seven properties. The first three properties are arrays of the TerrainType type. These three arrays will keep track of the vehicle type (vehicleTypes array), the types of terrain the vehicle can attack from (vehicleAttackTypes array), and the types of terrain the vehicle can move to (vehicleMovementTypes array).

The next three properties (landAttackRange, seaAttackRange, and airAttackRange integers) will contain the attack range of the vehicle for each of the different terrain types. Finally, the last property will keep track of the hit points of the vehicle.

The preference is for each of these properties, except for the hitPoints property, to be constants however, a subclass cannot set/change the value of a constant defined in a superclass. This means that we will need to rely on Swift's access control functionality to control the access to these properties.

We defined the properties as fileprivate variables because we need to set them from the subclasses however, we do not want external entities to change them. This access control was introduced in Swift 3, and allows access to the properties and methods from any code within the same source file that the item is defined in. In order for this to work, the subclass needs to be defined in the same physical file as the superclass, which is definitely not the ideal solution because this file could get very large. However, in this object-oriented design, it is the best option that we have to prevent these properties from being changed by instances of other types.

Since the properties are marked as fileprivate, we will need to create some getter methods that will retrieve the values of the properties. We will also create methods to see what types of terrain the vehicle can attack from and move to. Let's look at these methods:

     func isVehicleType(type: TerrainType) -> Bool {  
         return vehicleTypes.contains(type)  
     } 
     func canVehicleAttack(type: TerrainType) -> Bool { 
         return vehicleAttackTypes.contains(type)  
     } 
     func canVehicleMove(type: TerrainType) -> Bool { 
         return vehicleMovementTypes.contains(type) 
     } 
     
    func doLandAttack() {} 
    func doLandMovement() {} 
     
    func doSeaAttack() {} 
    func doSeaMovement() {} 
     
    func doAirAttack() {} 
    func doAirMovement() {} 
     
    func takeHit(amount: Int) { hitPoints -= amount } 
    func hitPointsRemaining() -> Int { return hitPoints } 
    func isAlive() -> Bool { return hitPoints > 0 ? true : false } 

The first method (isVehicleType method) accepts one parameter of the TerrainType type and will return true if the vehicleTypes array contains that terrain type. This will allow the external code to see if the vehicle is of a certain type. The next two methods also accept a parameter of the TerrainType type and will return true if the vehicleAttackTypes or vehicleMovementTypes arrays contain that terrain type. These two methods would be used to see whether a vehicle can move to or attack from a certain type of terrain.

The next six methods will define the attacks or movement for the different terrains. We will also look at a couple of additional methods that will see if the vehicle is alive, and deduct hit points when the vehicle takes a hit.

One big disadvantage to this design, as we noted earlier, is that all of the subclasses need to be in the same physical file as the Vehicle superclass. Given how large the vehicle classes can be, we probably don't want them all in the same source file. To avoid this, we could set the property's access controls to internal or public, but that would not prevent the properties from being changed by instances of other types. This is a major drawback of object-oriented design.

Another disadvantage of the object-oriented design is that we need to provide methods so the vehicle can attack from and move to each of the different terrain types, even though most vehicles will not be able to attack from and move to all of the different terrain types. Even though there is no code in the method implementations, the external code can still call any of the attack or movement methods. For example, even though our Submarine type is going to be a sea-only type, the external code will be able to call the movement and attack methods for land and air types. Superclasses that are bloated, such as this, are a major disadvantage of single-inheritance, object-oriented programming languages such as Swift.

Note

In this example, we are only defining a very small subset of the functionality that would be needed for our vehicle types in a video game. Imagine how big the Vehicle superclass could be if all of the functionality was being implemented.

Let's take a look at how we would subclass the Vehicle class by seeing how we would create the Tank, Amphibious, and Transformer classes. We will start with the Tank class:

class Tank: Vehicle { 
     
    override init() { 
        super.init() 
        vehicleTypes = [.Land] 
        vehicleAttackTypes = [.Land] 
        vehicleMovementTypes = [.Land] 
         
        landAttackRange = 5 
 
        hitPoints = 68 
    } 
     
    override func doLandAttack() { print("Tank Attack") } 
    override func doLandMovement() { print("Tank Move") } 
} 

The Tank class is a subclass of the Vehicle class, and we begin this class by overriding the default initializer. In the initializer, we set several inherited properties. Not how we add the Land value, the vehicleTypes, vehicleAttackTypes, and vehicleMovementTypes arrays. This specifies that the Tank type is a land vehicle and can attack from and move to land tiles. Using arrays to keep track of the type of vehicle the class is and the types of terrain the vehicle can move to and attack from is another disadvantage of the object-oriented design. Even for the most experienced developer, it is very easy to enter the wrong value into the arrays causing unexpected behavior.

In the Tank class, we also override the doLandAttack() and doLandMovement() methods from the Vehicle superclass since the TankTank class is a land vehicle. We do not override the other attack and movement methods from the Vehicle superclass because the tank should not be moving to or attacking from the sea or air terrains. Even though we do not override these methods, they are still a part of the Tank class because they are inherited from the Vehicle superclass, and there isn't any way to prevent external code from calling these methods. This is another disadvantage of object-oriented design.

Now let's look at the Amphibious and Transformer classes. These classes are very similar to the Tank class, except they can move to and attack from multiple terrain types. Here is the Amphibious class that can move and attack on both land and sea terrains:

class Amphibious: Vehicle { 
     
    override init() { 
        super.init() 
        vehicleTypes = [.Land, .Sea] 
        vehicleAttackTypes = [.Land, .Sea] 
        vehicleMovementTypes = [.Land, .Sea] 
         
        landAttackRange = 1 
        seaAttackRange = 1 
         
        hitPoints = 25 
    } 
    override func doLandAttack() {  
        print("Amphibious Land Attack")  
    } 
    override func doLandMovement() {  
        print("Amphibious Land Move")  
    } 
    override func doSeaAttack() {  
        print("Amphibious Sea Attack")  
    } 
    override func doSeaMovement() {  
        print("Amphibious Sea Move")  
    } 
} 

Now let's see the Transformer class that can move to and attack from all three terrain types:

class Transformer: Vehicle { 
     
    override init() { 
        super.init() 
        vehicleTypes = [.Land, .Sea, .Air] 
        vehicleAttackTypes = [.Land, .Sea, .Air] 
        vehicleMovementTypes = [.Land, .Sea, .Air] 
         
        landAttackRange = 7 
        seaAttackRange = 10 
        airAttackRange = 12 
         
        hitPoints = 75 
    } 
     
    override func doLandAttack() {  
        print("Transformer Land Attack")  
    } 
    override func doLandMovement() {  
        print("Transformer Land Move")  
    } 
    override func doSeaAttack() {  
        print("Transformer Sea Attack")  
    } 
    override func doSeaMovement() {  
        print("Transformer Sea Move")  
    } 
    override func doAirAttack() {  
        print("Transformer Air Attack")  
    } 
    override func doAirMovement() {  
        print("Transformer Air Move")  
    } 
} 

Now that we have created the vehicle types, let's take a look at how we would use them. One of the original requirements was to be able to keep instances of all the vehicle types in a single array. This will allow us to loop through all active vehicles and perform any actions needed. For this we will use polymorphism.

Polymorphism comes from the Greek words poly (for many) and morph (for forms). In computer science, we use polymorphism when we want to use a single interface to represent multiple types within our code. Polymorphism gives us the ability to interact with multiple types in a uniform manner. With object-oriented programming languages, we can achieve polymorphism through subclassing, where we interact with the various subclasses using the interface provided by the superclass.

Let's see how we would use polymorphism to keep all instances of the various vehicle types in a single array and interact with them. Since all of the vehicle types are subclasses of the Vehicle superclass, we can create an array of vehicle types, and store instances of any type that is a subclass of the Vehicle superclass, as shown here:

var vehicles = [Vehicle]() 
 
var vh1 = Amphibious() 
var vh2 = Amphibious() 
var vh3 = Tank() 
var vh4 = Transformer() 
 
vehicles.append(vh1) 
vehicles.append(vh2) 
vehicles.append(vh3) 
vehicles.append(vh4) 

Now that we have an array of vehicle types, we can interact with each instance through the interface presented by the vehicle type. The following code illustrates this:

for (index, vehicle) in vehicles.enumerated() { 
    if vehicle.isVehicleType(type: .Air) { 
        print("Vehicle at \(index) is Air") 
        if vehicle.canVehicleAttack(type: .Air) { 
            print("---Can do Air attack") 
        } 
        if vehicle.canVehicleMove(type: .Air) { 
            print("---Can do Air movement") 
        } 
    } 
    if vehicle.isVehicleType(type: .Land){ 
        print("Vehicle at \(index) is Land") 
        if vehicle.canVehicleAttack(type: .Land) { 
            print("---Can do Land attack") 
        } 
        if vehicle.canVehicleMove(type: .Land) { 
            print("---Can do Land movement") 
        } 
    } 
    if vehicle.isVehicleType(type: .Sea) { 
        print("Vehicle at \(index) is Sea") 
        if vehicle.canVehicleAttack(type: .Sea) { 
            print("---Can do Sea attack") 
        } 
        if vehicle.canVehicleMove(type: .Sea) { 
            print("---Can do Sea movement") 
        } 
    } 
} 

In this code, we loop through the vehicles array and use the isVehicleType(type:) method to determine if the vehicle is of a certain type, and then call the appropriate movement and attack methods. Note that we do not use an if-else or a switch statement here because any vehicle may be a member of multiple types, and we want to recheck the type, even if the vehicle matched a previous type.

This code works pretty well, but, as we will see in the protocol-oriented design section, we can make the code a lot cleaner, safer, and easier to read.

Before we look at the protocol-oriented design, let's review the drawbacks of object-oriented design that we pointed out in this section.

 

Drawbacks to object-oriented design


As we went through our object-oriented design, we pointed out several issues with such an approach. In this section, we will review these issues so we understand how protocol-oriented design addresses them.

Two of the drawbacks that we saw are directly related to each other, and are the result of Swift being a single-inheritance language. An object-oriented design with a single-inheritance language, such as Swift, can lead to bloated superclasses because we may need to include functionality that is needed by only a few of the subclasses. This leads to the second drawback related to Swift being a single-inheritance language, which is the inheritance of functionality that a type does not need.

Another drawback in our design is we could not create constants in our superclass that can be set by the subclasses. In our design, there were several properties that we would like to set in the initializer of our subclasses and then never change. It would be ideal if we could make these constants; however, a constant defined in one class cannot be set in a subclass of that type.

The final drawback that we saw was the inability to set a property or method to be accessible only by subclasses of that type. To get around this, we used the fileprivate access control to say that only code defined in the same source file could access the properties however, this workaround is not an ideal solution because we may not want to put all of the subclasses in the same source file as the superclass.

Now that we have looked at the object-oriented design, let's redesign our vehicle types in a protocol-oriented way.

 

Swift as a protocol-oriented programming language


As we did with the object-oriented design, let's start off by creating a very basic diagram that shows how we would design the vehicle types in a protocol-oriented way. Just like the object-oriented diagram, this will be a very basic diagram that simply shows the types themselves without much detail:

As we can see, the protocol-oriented design is quite a bit different from the object-oriented design. In our object-oriented design, we started the design with the superclass which became the focus of the design, since all of the subclasses inherited the functionality and properties from this superclass.

In the protocol-oriented design, we start the design with the protocol. The protocols and protocol extensions are also the focus of the protocol-oriented design however, as we will see throughout this book, the protocol-oriented design is about so much more than just the protocol.

In this new design, we use three techniques that make protocol-oriented programming significantly different from object-oriented programming. These techniques are protocol inheritance, protocol composition, and protocol extensions.

Protocol inheritance is where one protocol can inherit the requirements from one or more other protocols. This is similar to class inheritance in object-oriented programming however, instead of inheriting functionality we are inheriting requirements. One advantage that protocol inheritance has over class inheritance in Swift is that protocols can inherit the requirements from multiple protocols. In our example, the LandVehicle, SeaVehicle, and AirVehicle protocols inherit the requirements from the Vehicle protocol.

Protocol composition allows types to conform to more than one protocol. In our example, there are some types (Tank, Submarine, and Jet structures) that conform to a single protocol; however there are also two types (Amphibious and Transformer structures) that take advantage of protocol composition by conforming to multiple protocols.

Protocol inheritance and composition are extremely important to protocol-oriented design because they allow us to create smaller and more specific protocols. This allows us to avoid the bloated superclass that we may end up getting with object-oriented designs. We do need to be careful not to create protocols that are too granular because they will become hard to maintain and manage.

Protocol extensions allow us to extend a protocol to provide method and property implementations to conforming types. This gives us the ability to provide common implementations to all the conforming types, eliminating the need to provide an implementation in each individual type or the need to create a class hierarchy. While protocol extensions may not seem too exciting, once you see how powerful they really are, they will transform the way you think about application design.

We will cover these techniques in much greater detail in later chapters. For now, just knowing what these techniques are will help us understand the basics of our design. Let's begin our implementation by creating the Vehicle protocol. The Vehicle protocol, for our example, will define a single property name hitPoints that will keep track of the vehicle's remaining hit points:

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

If you recall from our object-oriented design, we had three methods defined as takeHit(amount:), hitPointsRemaining(), and isAlive(). The implementation for these methods would be the same for every vehicle type, which makes them great candidates to be implemented in a protocol extension. The following code shows how we would create a Vehicle protocol extension, and how we would implement these three methods within the extension:

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

Now, any type that conforms to the Vehicle protocol, or any type that conforms to a protocol that inherits from the Vehicle protocol, will automatically receive these methods. It is important to note that protocols that inherit requirements from another protocol also inherit the functionality provided by the protocol's extensions as well.

Now let's take a look at how we would define the LandVehicle, SeaVehicle, and AirVehicle protocols:

protocol LandVehicle: Vehicle { 
    var landAttack: Bool {get} 
    var landMovement: Bool {get} 
    var landAttackRange: Int {get} 
     
    func doLandAttack() 
    func doLandMovement() 
} 
 
protocol SeaVehicle: Vehicle { 
    var seaAttack: Bool {get} 
    var seaMovement: Bool {get} 
    var seaAttackRange: Int {get} 
     
    func doSeaAttack() 
    func doSeaMovement() 
} 
 
protocol AirVehicle: Vehicle { 
    var airAttack: Bool {get} 
    var airMovement: Bool {get} 
    var airAttackRange: Int {get} 
     
    func doAirAttack() 
    func doAirMovement() 
} 

There are a couple of things to note about these protocols. The first is they all inherit the requirements from the Vehicle protocol, which also means they inherit the functionality from the Vehicle protocol extension.

Another thing to note about these protocols is that they only contain the requirements needed for their particular type of vehicle. If you recall, the Vehicle superclass from our object-oriented design contained the requirements for all vehicle types. Dividing the requirements up into three separate protocols makes the code much safer, and easier to maintain and manage. If we do need some common functionality we can add a protocol extension to any or all of the protocols.

We defined the properties for these protocols with only the get attribute, which means we will be defining the properties as constants within the types that conform to these protocols. This is a really big advantage in using protocol-oriented design because it prevents code from changing the values once they are set, introducing errors that are hard to trace.

Now let's look at how we can create types that conform to these protocols. We will create the Tank, Amphibious, and Transformer types (as we did in our object-oriented design) starting with the Tank type:

struct Tank: LandVehicle { 
    var hitPoints = 68 
    let landAttackRange = 5 
    let landAttack = true 
    let landMovement = true 
     
    func doLandAttack() { print("Tank Attack") } 
    func doLandMovement() { print("Tank Move") } 
} 

There are several differences between the Tank type defined here and the Tank type defined in our object-oriented design. In order to see these differences, let's look at the Tank type that was defined in our object-oriented design:

class Tank: Vehicle {    
    override init() { 
        super.init() 
        vehicleTypes = [.Land] 
        vehicleAttackTypes = [.Land] 
        vehicleMovementTypes = [.Land] 
         
        landAttackRange = 5 
 
        hitPoints = 68 
    }    
    override func doLandAttack() { print("Tank Attack") } 
    override func doLandMovement() { print("Tank Move") } 
} 

The first thing that we can see is that the Tank type from our object-oriented design is a class, which is a reference type, while the Tank type defined in this section is a structure, which is a value type. We could have defined the Tank type as a class but using a structure has some advantages. We will discuss the different type choices in depth in Chapter 2, Our Type Choices.

The big differences between the two types is that in the Tank type defined in this section is we are able to use the default initializer that the structure provides, and we define the properties as constants; therefore, they can't be changed once they are set. In the Tank type from our object-oriented design, we had to override the initializer and then set the properties within the initializer. The properties in the object-oriented design were also variables, which may be changed once they are set.

One thing that we do not see when we look at the two Tank types is that the Tank type from the protocol-oriented design contains only the functionality for land vehicles. The Tank type from the object-oriented design inherits the functionality and properties for the sea and air types from the Vehicle superclass, even though it does not need that functionality.

Now let's see how we would create the Amphibious and Transformer types:

struct Amphibious: LandVehicle, SeaVehicle { 
    var hitPoints = 25 
    let landAttackRange = 1 
    let seaAttackRange = 1 
    let landAttack = true 
    let landMovement = true 
    let seaAttackRange = 1 
    let seaAttack = true 
    let seaMovement = true 
     
    func doLandAttack() { print("Amphibious Land Attack") } 
    func doLandMovement() { print("Amphibious Land Move") } 
    func doSeaAttack() { print("Amphibious Sea Attack") } 
    func doSeaMovement() { print("Amphibious Sea Move") } 
} 
struct Transformer: LandVehicle, SeaVehicle, AirVehicle { 
    var hitPoints = 75 
    let landAttackRange = 7 
    let landAttack = true 
    let landMovement = true 
    let seaAttack = true 
    let seaMovement = true 
    let airAttack = true 
    let airMovement = true 
     
    func doLandAttack() { print("Transformer Land Attack") } 
    func doLandMovement() { print("Transformer Land Move") } 
    func doSeaAttack() { print("Transformer Sea Attack") } 
    func doSeaMovement() { print("Transformer Sea Move") } 
    func doAirAttack() { print("Transformer Sea Attack") } 
    func doAirMovement() { print("Transformer Sea Move") } 
} 

We can see that the Amphibious and Transformer types are very similar to the Tank type, however they use protocol composition to conform to multiple vehicle types. This allows, for example, the Amphibious type to contain the functionality for both land and sea vehicles.

Now let's see how we would use these new types. As with our object-oriented design, we have the requirement to be able to keep instances of all of the vehicle types in a single array. This allows us to loop through all active vehicles and perform any actions needed. For this, we will use polymorphism just as we did with our object-oriented design however with our protocol-oriented design, we will use the requirements provided by the protocols to interact with the instances of the vehicle types. Let's see how we would do this by creating an array and putting several instances of the vehicle types into it:

var vehicles = [Vehicle]() 
 
var vh1 = Amphibious() 
var vh2 = Amphibious() 
var vh3 = Tank() 
var vh4 = Transformer() 
 
vehicles.append(vh1) 
vehicles.append(vh2) 
vehicles.append(vh3) 
vehicles.append(vh4) 

This code looks exactly like the code from our object-oriented design. In this code, we create an array that will store instances of types that conform to the Vehicle type. With protocol inheritance, this means the array will also accept types that conform to protocols that inherit the Vehicle protocol. In our example, this means that the array will accept instances of types that conform to the LandVehicle, SeaVehicle, AirVehicle, and Vehicle protocols.

The array, in this example, is defined to contain instances of types that conform to the Vehicle protocol. This means that we can use the interface defined by the Vehicle protocol to interact with the types in the array. Looking at the Vehicle protocol, that really is not very useful however, we can use another really nice feature in the Swift language to help us. This feature is pattern matching, and in our case, we will be using the type-casting pattern. The type-casting pattern matches a value if the type at runtime is the same as the type specified. Let's see how this works:

for (index, vehicle) in vehicles.enumerated() { 
    if let Vehicle = vehicle as? AirVehicle { 
        print("Vehicle at \(index) is Air") 
    } 
    if let Vehicle = vehicle as? LandVehicle { 
        print("Vehicle at \(index) is Land") 
    } 
    if let Vehicle = vehicle as? SeaVehicle { 
        print("Vehicle at \(index) is Sea") 
    } 
} 

In this code, we use a for loop statement to loop through the vehicles array. We use an as? type-casting pattern to see if the instances conform to the various protocols (AirVehicle, LandVehicle, and SeaVehicle protocols), and if so, we print out a message.

Accessing the vehicle types in this manner is very similar to how we accessed them in the object-oriented example however, what if we only wanted to get one type of vehicle rather than all vehicles? We are able to do this with another type-casting pattern with the where keyword. The following example shows how to do this:

for (index, vehicle) in vehicles.enumerated()  
    where vehicle LandVehicle { 
    var vh = vehicle as! LandVehicle 
    if vh.landAttack { 
        print("---Can do Land attack") 
    } 
    if vh.landMovement { 
        print("---Can do Land movement") 
    } 
} 

In this example, we use the where keyword to filter the results of the for loop to retrieve only instances that conform to the LandVehicle protocol. We can then typecast any instance that is returned from the for loop as an instance that conforms to the LandVehicle protocol and interact with it using the interface provided by the protocol.

Now that we have finished redesigning, let's summarize what protocol-oriented programming is and how it is different from object-oriented programming.

 

Summarizing protocol-oriented programming and object-oriented programming


We just saw how Swift can be used as both an object-oriented programming language and a protocol-oriented programming language, and what the differences between the two programming paradigms are. In the example presented in this chapter, there were two major differences between the two designs.

The first major difference that we saw is that with protocol-oriented programming we should start with the protocol rather than a superclass. We can then use protocol extensions to add functionality to the types that conform to that protocol. With object-oriented programming, we started with a superclass. When we redesigned our example, we converted the Vehicle superclass to a Vehicle protocol, and then used a protocol extension to add the common functionality needed for all types that conform to the Vehicle protocol.

In the protocol-oriented example, we used protocol inheritance and protocol composition to allow us to create protocols with very specific requirements. This allowed us to create concrete types that only contained the functionality needed. In the object-oriented design, the concrete types inherited all of the functionality provided by the Vehicle superclass.

The second big difference that we saw was the use of value types (structures) rather than reference types (classes) for our vehicle types. Apple's documentation states that developers should prefer value types over reference types where appropriate. In our example, it was appropriate to use value types for the vehicle types. In Chapter 2, Our Type Choices, we will discuss the differences between value and reference types in depth.

Both the object-oriented design and the protocol-oriented design used polymorphism to let us interact with different types using a single interface. With the object-oriented design, we used the interface provided by the superclass to interact with all the subclasses. In the protocol-oriented design, we used the interface provided by the protocols and the protocol extensions to interact with the types that conform to the protocols.

Now that we have summarized the differences between object-oriented programming design and protocol-oriented programming design, let's take a closer look at these differences.

 

Differences between object-oriented programming and protocol-oriented programming


I mentioned at the beginning of the chapter that protocol-oriented programming is about so much more than just the protocol, and that it is a new way of not only writing applications, but also thinking about programming. In this section, we will examine the differences between our two designs to see what that statement really means.

As a developer, our primary goal is always to develop an application that works properly, but we should also be focused on writing clean and safe code. In this book, we will be talking about clean and safe code a lot, so let's look at what we mean by these terms.

Clean code is code that is very easy to read and understand. It is important to write clean code because any code that we write will need to be maintained by someone, and that someone is usually the person who wrote it. There is nothing worse than looking back at code you wrote and not being able to understand what it does. It is also a lot easier to find errors in code that is clean and easy to understand.

By safe code, we mean code that is hard to break. There is nothing more frustrating for us developers than making a small change in our code and then have errors pop up throughout the code base. By writing clean code, our code will be inherently safer because other developers will be able to look at the code and understand exactly what it does.

Now let's briefly look at the difference between protocols/protocol extensions and superclasses. We will be covering this a lot more in Chapter 4, All About the Protocol, and Chapter 5, Let's Extend Some Types.

Protocol and protocol extensions compared with superclasses

In the object-oriented programming example, we created a Vehicle superclass from which all of the vehicle classes were derived. In the protocol-oriented programming example, we used a combination of a protocol and a protocol extension to achieve the same result; however, there are several advantages in using protocols.

To refresh our memory of the two solutions, let's look at the code for both the Vehicle superclass and the Vehicle protocol and protocol extension. The following code shows the Vehicle superclass:

class Vehicle { 
 fileprivate var vehicleTypes = [TerrainType]() 
    fileprivate var vehicleAttackTypes = [TerrainType]() 
    fileprivate var vehicleMovementTypes = [TerrainType]() 
     
    fileprivate var landAttackRange = -1 
    fileprivate var seaAttackRange = -1 
    fileprivate var airAttackRange = -1 
     
    fileprivate var hitPoints = 0    
 
 func isVehicleType(type: TerrainType) -> Bool {  
    return vehicleTypes.contains(type)  
  } 
 func canVehicleAttack(type: TerrainType) -> Bool { 
    return vehicleAttackTypes.contains(type)  
  } 
 func canVehicleMove(type: TerrainType) -> Bool { 
    return vehicleMovementTypes.contains(type)  
  } 
     
 func doLandAttack() {} 
 func doLandMovement() {} 
     
 func doSeaAttack() {} 
 func doSeaMovement() {} 
     
 func doAirAttack() {} 
 func doAirMovement() {} 
     
 func takeHit(amount: Int) { hitPoints -= amount } 
 func hitPointsRemaining() -> Int { return hitPoints } 
 func isAlive() -> Bool { return hitPoints > 0 ? true : false } 
}

The Vehicle superclass is a complete type that we can create instances of. This can be a good or a bad thing. There are times, such as in this example, when we should not be creating instances of the superclass; we should only be creating instances of the subclasses. For this, we can still use protocols with object-oriented programming; however, we will need to use protocol extensions to add the common functionality, and that leads us down the protocol-oriented programming path.

Now let's look at how we used protocols and protocol extensions with protocol-oriented programming to create the Vehicle protocol and the Vehicle protocol extension:

protocol Vehicle { 
    var hitPoints: Int {get set} 
} 
extension Vehicle { 
    mutating func takeHit(amount: Int) { hitPoints -= amount } 
    func hitPointsRemaining() -> Int { return hitPoints } 
    func isAlive() -> Bool { return hitPoints > 0 ? true : false } 
} 

We then created three additional protocols, one for each type of vehicle, and used protocol inheritance to inherit the requirements and functionality from the Vehicle protocol into these three protocols. The following are the LandVehicle, SeaVehicle, and AirVehicle protocols:

protocol LandVehicle: Vehicle { 
    var landAttack: Bool {get} 
    var landMovement: Bool {get} 
    var landAttackRange: Int {get} 
     
    func doLandAttack() 
    func doLandMovement() 
} 
 
protocol SeaVehicle: Vehicle { 
    var seaAttack: Bool {get} 
    var seaMovement: Bool {get} 
    var seaAttackRange: Int {get} 
     
    func doSeaAttack() 
    func doSeaMovement() 
} 
 
protocol AirVehicle: Vehicle { 
    var airAttack: Bool {get} 
    var airMovement: Bool {get} 
    var airAttackRange: Int {get} 
     
    func doAirAttack() 
    func doAirMovement() 
} 

The code in both of these solutions is pretty safe and easy to understand; however, the protocol-oriented design is safer. By separating the implementation from the definition and dividing the requirements into small, more specific protocols, we were able to eliminate the need for a bloated superclass, and also prevented types from inheriting functionality they do not need.

There are three clear advantages that protocols/protocol extensions have over superclasses. The first advantage is that types can conform to multiple protocols; however, they can only have one superclass. What this means is that we can create numerous protocols that contain very specific functionality rather than creating a single monolithic superclass. We can see this in our example where the Vehicle superclass contained the functionality for land, sea, and air vehicles; however while, in the protocol-oriented design, we were able to create three protocols, one for each type of vehicle.

The second advantage that protocol/protocol extensions have is that we can use protocol extensions to add functionality without needing the original code. What this means is that we can extend any protocol, even the protocols that are a part of the Swift language itself. To add functionality to our superclass, instead, we need to have the original code. We could use extensions to add functionality to a superclass, which means that all the subclasses will also inherit that functionality. However, generally, we use extensions to add functionality to a specific class rather than adding functionality to a class hierarchy.

The third advantage that protocols/protocol extensions have is that protocols can be adopted by classes, structures, and enumerations, while class hierarchies are restricted to class types. Protocols/protocol extensions give us the option to use value types where appropriate.

Implementing vehicle types

The implementations of vehicle types were slightly different between the object-oriented example and the protocol-oriented example however the difference is pretty significant. We will look at the differences between these two examples, but first, let's take a look at the code again to remind us how we implemented the vehicle types. We will look at how we implemented the Tank type in the object-oriented example first:

class Tank: Vehicle { 
     
    override init() { 
        super.init() 
        vehicleTypes = [.Land] 
        vehicleAttackTypes = [.Land] 
        vehicleMovementTypes = [.Land] 
         
        landAttackRange = 5 
 
        hitPoints = 68 
    } 
     
    override func doLandAttack() { print("Tank Attack") } 
    override func doLandMovement() { print("Tank Move") } 
} 

This class is a subclass of the Vehicle superclass, and it implements a single initializer. While this is a pretty simple and straightforward implementation, we really need to fully understand what the superclass expects in order to implement the type properly. For example, if we do not fully understand the Vehicle superclass, we may forget to set the landAttackRange property. In our example, forgetting to set this property will cause the instances of the Tank type unable to attack properly.

Now let's look at how we implemented a vehicle type in the protocol-oriented programming example:

struct Tank: LandVehicle { 
    var hitPoints = 68 
    let landAttackRange = 5 
    let landAttack = true 
    let landMovement = true 
     
    func doLandAttack() { print("Tank Attack") } 
    func doLandMovement() { print("Tank Move") } 
} 

The Tank type from the protocol-oriented design conforms to the LandVehicle protocol and uses the default initializer provided by the structure. The code in the protocol-oriented design is a lot safer and easier to understand. The reason we say that the protocol-oriented example is safer and easier to understand is because of the way properties and initializer are implemented in both the examples.

In the object-oriented programming example, all of the properties are defined in the superclass as variables. We will need to look at the code or the documentation for the superclass to see what properties are defined and how they are defined. If we forget to set something in a subclass, the compiler will happily compile the application and not warn us.

With protocols, we also need to look at the protocol itself or the documentation for the protocol to see which properties to implement. If we forget to implement any of the requirements, the compiler will warn us and refuse to compile until we properly set everything. We also have the ability to define any of the properties as constants, whereas with the object-oriented design we had to define them as variables.

 

The winner is...


As we were reading through this chapter, and seeing all of the advantages that protocol-oriented programming has over object-oriented programming, we may think that protocol-oriented programming is clearly superior to object-oriented programming. However, this assumption may not be totally correct.

Object-oriented programming has been around since the 1970s, and is a battle tested programming paradigm. Protocol-oriented programming is the new kid on the block, and was designed to correct some of the issues with object-oriented programming. I have personally used the protocol-oriented programming paradigm in a couple of projects and I am very excited about its possibilities.

Object-oriented programming and protocol-oriented programming have similar philosophies, such as creating custom types that can model real-world or virtual objects and polymorphism to use a single interface to interact with multiple types. The difference is in how these philosophies are implemented.

In my opinion, the code base in a project that uses protocol-oriented programming is much safer and easier to read as compared to a project that uses object-oriented programming. This does not mean that I am going to stop using object-oriented programming altogether; I can still see plenty of need for class hierarchy and inheritance.

Remember, when we are designing our application, we should always use the right tool for the right job. We would not want to use a chainsaw to cut a piece of 2 x 4 timber, but we also would not want to use a circular saw to cut down a tree. Therefore, the winner is the developer, where we have the choice of using different programming paradigms rather than being limited to only one.

 

Summary


In this chapter, we saw how Swift can be used as an object-oriented programming language and as a protocol-oriented programming language. While these two programming paradigms have similar philosophies, they implement these philosophies differently.

With object-oriented programming, we would use classes as our blueprints when we create objects. With protocol-oriented programming, we have the choice of using classes, structures, and enumerations. We can even use other types, as we will see in Chapter 2, Our Type Choices.

With object-oriented programming, we can implement polymorphism using class hierarchies. With protocol-oriented programming, we can use a combination of protocols and protocol extensions to implement polymorphism. We will look at protocols in depth in Chapter 4, All About the Protocol.

With object-oriented programming, we are able to implement functionality in our superclasses that is inherited by the subclasses. The subclasses do have the ability to override the functionality provided by their superclass. With protocol-oriented programming, we use protocol extensions to add functionality to types that conform to our protocols. These types can also shadow the protocol's default functionality if they choose to. We will look at protocol extensions in depth in Chapter 5, Let's Extend Some Types.

While object-oriented programming has been around since the 1970s it is beginning to show some wear and tear. In this chapter, we looked at the problems and design issues that protocol-oriented programming was designed to solve.

Now that we have seen an overview of protocol-oriented programming, it is time to look at the areas that make up protocol-oriented programming in greater detail. By achieving a deeper understanding of the different areas, we will be able to better implement protocol-oriented programming in our applications. We will start off by looking at the various type choices that we have with the Swift programming language and how we should use each of them.

About the Author

  • Jon Hoffman

    Jon Hoffman has over 20 years' experience in the field of Information Technology. Over those 20 years, Jon has worked in the system administration, network administration, network security, application development, and architecture arenas. Currently, he works as an Enterprise Software Manager at Syntech Systems. He has developed extensively for the iOS platform since 2008. This includes several apps that he has published in the App Store, apps that he has written for third parties, and numerous enterprise applications. Some of Jon's other interests are playing basketball, kayaking, and working out with his daughters. Jon also really enjoys Tae Kwon Do, where he and his oldest daughter earned their black belts together early in 2014 and are currently 3rd-degree Black Belts.

    Browse publications by this author

Latest Reviews

(1 reviews total)
This book has been a good read so far. I'm glad the examples work (bothers me when they don't)
Book Title
Access this book, plus 7,500 other titles for FREE
Access now