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. By 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:
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.
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 list 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 a thing, 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.
While I am writing this chapter, I am also thinking about one of my all-time favorite energy drink. That energy drink is called Jolt. Not sure how many people remember Jolt soda or Jolt energy drink, but I would not have made it through college without them. A can of Jolt can be modeled as an object with attributes (volume, caffeine amount, 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 objects, such as a can of Jolt, or virtual objects like 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 object 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 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. Let's see how we would use classes in Swift to model cans of Jolt and a cooler to keep our Jolt cold. We will begin by modeling the cans of Jolt as follows:
class Jolt { var volume: Double var caffeine: Double var temperature: Double var canSize: Double var description: String init(volume: Double, caffeine: Double, temperature: Double) { self.volume = volume self.caffeine = caffeine self.temperature = temperature self.description = "Jolt energy drink" self.canSize = 24 } func drinking(amount: Double) { volume -= amount } func temperatureChange(change: Double) { temperature += change } }
In this Jolt
class, we defined five properties. These properties are volume
(the amount of Jolt in the can), caffeine
(how much caffeine comes in a can), temperature
(the present temperature of the can), description
(the description of the product), and cansize
(the size of the can itself). We then define one initializer that will be used to initiate the properties of the object when we create an instance of the class. This initializer will ensure that all of the properties are properly initialized when the instance is created. Finally, we defined two actions for the can. These two actions are drinking
(called when someone drinks from the can) and temperatureChange
(called when the temperature of the can changes).
Now, let's see how we would model a cooler that we can use to keep our cans of Jolt cold because no one likes warm cans of Jolt:
class Cooler { var temperature: Double var cansOfJolt = [Jolt]() var maxCans: Int init(temperature: Double, maxCans: Int) { self.temperature = temperature self.maxCans = maxCans } func addJolt(jolt: Jolt) -> Bool { if cansOfJolt.count < maxCans { cansOfJolt.append(jolt) return true } else { return false } } func removeJolt() -> Jolt? { if cansOfJolt.count > 0 { return cansOfJolt.removeFirst() } else { return nil } } }
We modeled the cooler in a similar fashion to how we modeled the cans of Jolt. We began by defining the three properties of the cooler. The three properties are temperature
(the present temperature in the cooler), cansOfJolt
(the cans of Jolt in the cooler), and
maxCans
(the maximum number of cans the cooler can hold). We then used an initializer to initiate the properties when we create the instances of the Cooler
class. Finally, we defined the two actions for the cooler. They are addJolt
(used to add a can of Jolt to the cooler) or removeJolt
(used to remove a can of Jolt from the cooler). Now that we have our Jolt
and Cooler
classes, let's see how we would use these two classes together:
var cooler = Cooler(temperature: 38.0, maxCans: 12) for _ in 0...5 { let can = Jolt(volume: 23.5, caffeine: 280, temperature: 45) let _ = cooler.addJolt(can) } let jolt = cooler.removeJolt() jolt?.drinking(5) print("Jolt Left in can: \(jolt?.volume)")
In this example, we created an instance of the Cooler
class using the initializer to set the default properties. We then created six instances of the Jolt
class and added them to the cooler using a for-in
loop. Finally, we took a can of Jolt from the cooler and drank some of it. A refreshing drink of Jolt and the jolt of caffeine. What could be better?
This design seems to work well for our simplistic example; however, it really is not that flexible. While I really like caffeine, my wife doesn't; she prefers Caffeine Free Diet Coke. With our present cooler design, when she goes to add some of her Caffeine Free Diet Coke to the cooler, we would have to tell her that it is not possible because our cooler only accepts instances of Jolt. This would not be good, because this is not the way coolers work in the real world and also because I would not want to tell my wife that she can't have her Diet Coke (trust me no one wants to tell her she can't have her Diet Coke). So, how could we make this design more flexible?
The answer to this question is polymorphism. Polymorphism comes from the Greek words Poly (for many) and Morph (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. When we interact with multiple objects through a uniform interface, we are able to add additional object types that conform to that interface at any time. We can then use these additional types in our code with little to no changes.
With object-oriented programming languages, we can achieve polymorphism and code reuse with subclassing. Subclassing is when one class is derived from another superclass. For example, if we had a Person
class that modeled a typical person, we could then subclass the Person
class to create a Student
class. The Student
class would then inherit all of the properties and methods of the Person
class. The Student
class could override any of the properties and methods that it inherited and/or add its own additional properties and methods. We could then add additional classes that are also derived from the Person
superclass, and we could interact with all of these subclasses using the interface presented by the Person
class.
When one class is derived from another class, the original class, the one we are deriving the new class from, is known as the super or the parent class and the new class is known as the child or subclass. In our person-student example, the Person
was the super or parent class and the Student
was the sub or child class. In this book, we will be using the terms superclass and subclass.
Polymorphism is achieved with subclassing because we can interact with the instances of all the child classes though the interface that is presented by the superclass. As an example, if we had three child classes (Student
, Programmer
, and Fireman
) that were all subclasses of the Person
class, then we could interact with all three of the subclasses though the interface that is presented by the Person
class. If the Person
class had a method named running()
, then we can be assured that all the subclasses of the Person
class has a method named running()
(either the method from the Person
class or one from the subclass that overrides the method from the Person
class). Therefore, we can interact with all the subclasses using the running()
method.
Let's see how polymorphism can help us add drinks other than Jolt to our cooler. In our original example, we were able to hard code the can size in our Jolt
class because Jolt energy drinks were only sold in 24 oz cans (the sodas had different sizes, but the energy drink was only sold in 24 oz cans). The following enumeration defines the can sizes that our cooler will accept:
enum DrinkSize { case Can12 case Can16 case Can24 case Can32 }
This DrinkSize
enumeration lets us use 12, 16, 24, and 32 oz drink sizes in our cooler.
Now, let's look at our base or superclass that all of our drink types will derive from. We will name this superclass Drink
:
class Drink { var volume: Double var caffeine: Double var temperature: Double var drinkSize: DrinkSize var description: String init(volume: Double, caffeine: Double, temperature: Double, drinkSize: DrinkSize) { self.volume = volume self.caffeine = caffeine self.temperature = temperature self.description = "Drink base class" self.drinkSize = drinkSize } func drinking(amount: Double) { volume -= amount } func temperatureChange(change: Double) { self.temperature += change } }
This Drink
class is very similar to our original Jolt
class. We defined the same five properties that we had in our original Jolt
class; however, drinkSize
is now defined to be of the DrinkSize
type rather than Double
. We defined a single initializer for our Drink
class that initializes all the five properties of the class. Finally, we have the same two methods that were in the original Jolt
class, which are drinking()
and temperatureChange()
. One thing to take note of is, in the Drink
class, our description is set to Drink base class.
Now, let's create the Jolt
class that will be a subclass of the Drink
class. This class will inherit all the property and methods from the Drink
class:
class Jolt: Drink { init(temperature: Double) { super.init(volume: 23.5, caffeine: 280, temperature: temperature, drinkSize: DrinkSize.Can24) self.description = "Jolt energy drink" } }
As we can see in the Jolt
class, we do not need to redefine the properties and the methods from the Drink
superclass. We will add an initializer for our Jolt
class. This initializer only requires that the temperature of the can of Jolt be provided. All the other values are set to their default values for a can of Jolt.
Now, let's see how we would create the Cooler
class that will accept other drink types besides Jolt:
class Cooler { var temperature: Double var cansOfDrinks = [Drink]() var maxCans: Int init(temperature: Double, maxCans: Int) { self.temperature = temperature self.maxCans = maxCans } func addDrink(drink: Drink) -> Bool { if cansOfDrinks.count < maxCans { cansOfDrinks.append(drink) return true } else { return false } } func removeDrink() -> Drink? { if cansOfDrinks.count > 0 { return cansOfDrinks.removeFirst() } else { return nil } } }
This new Cooler
class is exactly like the original Cooler
class, except that we replace all the references to the Jolt
class with the references to the Drink
class. Since the Jolt
class is a subclass of the Drink
class, we can use it any place where an instance of the Drink
class is required. Let's see how this would work. The following code will create an instance of the
Cooler
class. Add six cans of Jolt to the cooler, remove one of the cans from the cooler, and then take a drink of Jolt:
var cooler = Cooler(temperature: 38.0, maxCans: 24) for _ in 0...5 { let can = Jolt(temperature: 45.1) let _ = cooler.addDrink(can) } let jolt = cooler.removeDrink() cooler.cansOfDrinks.count jolt?.drinking(5) print("Jolt Left in can: \(jolt?.volume)")
Notice that in this example, we used instances of the Jolt
class where instances of the Drink
class are required. This is polymorphism in action. Now that we have a cooler with our Jolt in it, we are ready to go on a trip. My wife of course wants to bring her Caffeine Free Diet Coke so she asks if she can put some in the cooler to keep it cold. Knowing that we do not want to deprive her of Diet Coke, we quickly create a CaffeineFreeDietCoke
class that we can use with the cooler. The code for this class is:
class CaffeineFreeDietCoke: Drink { init(volume: Double, temperature: Double, drinkSize: DrinkSize) { super.init(volume: volume, caffeine: 0, temperature: temperature, drinkSize: drinkSize) self.description = "Caffiene Free Diet Coke" } }
The CaffeineFreeDietCoke
class is very similar to the Jolt
class. They both are subclasses of the Drink
class, and they each define an initializer that initializes the class. The key is that they both are subclasses of the Drink
class, which means we can use instances of both classes in our cooler. Therefore, when my wife brings her six Caffeine Free Diet Cokes, we can put them in the cooler just like the cans of Jolt. The following code demonstrates this:
var cooler = Cooler(temperature: 38.0, maxCans: 24) for _ in 0...5 { let can = Jolt(temperature: 45.1) let _ = cooler.addDrink(can) } for _ in 0...5 { let can = CaffeineFreeDietCoke(volume: 15.5, temperature: 45, drinkSize: DrinkSize.Can16) let _ = cooler.addDrink(can) }
In this example, we created an instance of the cooler; we added six cans of Jolt and six cans of Caffeine Free Diet Coke to it. Using polymorphism, as shown here, allows us to create as many subclasses of the Drink
class as we need, and all of them can be used with the Cooler
class without changing the code of the Cooler
class. This allows our code to be extremely flexible.
So, what happens when we grab a can from the cooler? Obviously, if my wife grabs a can of Jolt, she will want to put it back and get a different can. But how will she know which can she grabbed?
To check whether an instance is of a particular type, we use the type check operator (is
). The type check operator will return true
if the instance is of the type or false
if it isn't. In the following code, we use the type check operator to continuously remove cans from the cooler until we find a can of Caffeine Free Diet Coke:
var foundCan = false var wifeDrink: Drink? while !foundCan { if let can = cooler.removeDrink() { if can is CaffeineFreeDietCoke { foundCan = true wifeDrink = can } else { cooler.addDrink(can) } } } if let drink = wifeDrink { print("Got: " + drink.description) }
In this code, we have a while
loop that continuously loops until the foundCan
Boolean is set to true
. Within the while
loop, we remove a drink from the cooler and then use the type check operator (is
) to see whether the can that we removed is an instance of the CaffeineFreeDietCoke
class. If it is an instance of the CaffeineFreeDietCoke
class, then we will set the foundCan
Boolean to true
and set the wifeDrink
variable to the instance of the drink we just removed from the cooler. If the drink is not an instance of the CaffeineFreeDietCoke
class, then we will put the can back in the cooler and loop back to grab another drink.
In the previous example, we showed how Swift can be used as an object-oriented programming language. We also used polymorphism to make our code very flexible and easy to expand; however, there are several drawbacks to this design. Before we move on to protocol-oriented programming, let's take a look at two of these drawbacks. Then, we will see how protocol-oriented programming can be used to make this design better.
The first drawback of our design is the initializers of the drink (Jolt
, CaffeineFreeDietCoke
, and DietCoke
) classes. When we initialize a subclass, we need to call the initializer of the superclass. This is a double-edged sword. While calling the initializer of our superclass gives us consistent initialization, it can also give us improper initialization if we are not careful. For example, let's say that we created another Drink
class named DietCoke
with the following code:
class DietCoke: Drink { init(volume: Double, temperature: Double, drinkSize: DrinkSize) { super.init(volume: volume, caffeine: 45, temperature: temperature, drinkSize: drinkSize) } }
If we look carefully, we will see that in the initializer of the DietCoke
class, we never set the description
property. Therefore, the description of this class will end up being Drink base class
, which is not what we want.
We need to be careful when we create subclasses like this to ensure that all of the properties are properly set and we don't just assume that the initializer of the superclass will properly set all of the properties for us.
The second drawback to our design is we are using reference types. While those who are familiar with object-oriented programming may not see this as a drawback and reference types are preferred in a lot of cases, in our design, it makes more sense to define the drink types as value types. If you are not familiar with how reference and value types work, we will be looking at them in depth in Chapter 2, Our Type Choices.
When we pass an instance of a reference type (that is, we pass to a function or a set in a collection like an array), we are passing a reference to the original instance. When we pass an instance of a value type, we are passing a new copy of the original instance. Let's see the issue that using reference types can cause if we are not careful by examining the following code:
var jolts = [Drink]() var myJolt = Jolt(temperature: 48) for _ in 0...5 { jolts.append(myJolt) } jolts[0].drinking(10) for (index,can) in jolts.enumerate(){ print("Can \(index) amount Left: \(can.volume)") }
In this example, we created an array that will contain instances of the Drink
class or instances of a type that is a subclass of the Drink
class. We then created an instance of the Jolt
class and used it to populate our array with six cans of Jolt. Next, we took a drink from the first can in our array and printed out the remaining volume of each can in our array. If we run this code, we would see the following results:
Can 0 amount Left: 13.5 Can 1 amount Left: 13.5 Can 2 amount Left: 13.5 Can 3 amount Left: 13.5 Can 4 amount Left: 13.5 Can 5 amount Left: 13.5
As we can see from the results, all of the cans in the array have the same amount of Jolt remaining. This is because we created a single instance of the Jolt
class and then, to the jolts
array, we added six references to this single instance. Therefore, when we took a drink from the first can in the array, we actually took a drink from all of the cans in the array.
A mistake like this to an experienced object-oriented programmer may seem out of the question; however, it's amazing how often it occurs with junior developers or developers who are not familiar with object-oriented programming. This error occurs more often with classes that have complex initializers. We can avoid this issue by using the Builder pattern that we will see in Chapter 6, Adopting Design Patterns in Swift or by implementing a copy method in our custom class that will make a copy of an instance.
One other thing to note about object-oriented programming and subclassing, as shown in the previous example, is that a class can only have one superclass. For example, the superclass for our Jolt
class is the Drink
class. This can lead to a single superclass that is very bloated and contains code that is not needed or wanted by all the subclasses. This is a very common problem in game development.
Now, let's look at how we would implement our drinks and cooler example using protocol-oriented programming.