With object-oriented programming, we usually begin our design by thinking about the objects and the class hierarchy. Protocol-oriented programming is a little different. Here, we begin our design by thinking about the protocols. However, as we stated at the beginning of this chapter, protocol-orientated programming is about so much more than just the protocol.
As we go through this section, we will briefly discuss the different items that make up protocol-oriented programming with regards to our current example. We will then discuss these items in depth over the next couple of chapters to give you a better understanding of how to use protocol-oriented programming as a whole in our applications.
In the previous section, when we looked at Swift as an object-oriented programming language, we designed our solution with a class hierarchy, as shown in the following diagram:
To redesign this solution with protocol-oriented programming, we would need to rethink a couple areas of this design. The first area that we would want to rethink is the Drink
class. Protocol-oriented programming states that we should begin with a protocol rather than a superclass. This means that our Drink
class would become a Drink
protocol. We would then use protocol extensions to add common code for our drink types that will conform to this protocol. We will go over the protocols in Chapter 4, All about the Protocol, and we will cover the protocol extensions in Chapter 5, Let's Extend Some Types.
The second area that we would want to rethink is the use of reference (class) types. With Swift, Apple has stated that it is preferable to use value types over reference types where appropriate. There is a lot to consider when we decide whether to use reference or value types, and we will go over this in depth in Chapter 2, Our Type Choices. In this example, we will use value (structure) types for our drink types (Jolt
and CaffeineFreeDietCoke
) and a reference (class) type for our Cooler
type.
The decision to use value types for our drink types and a reference type for our Cooler
type, in this example, is based on how we would use the instances of these types. The instance of our drink types will only have one owner. For example, when a drink is in the cooler, the cooler owns it. But then, when a person takes the drink out, the drink is removed from the cooler and given to a person who would then own it.
The Cooler
type is a little different from the drink types. While the drink types will have only one owner interacting with it at a time, instances of the Cooler
type may have several parts of our code interacting with it. For example, we may have one part of our code adding drinks to the cooler while we have instances of several people taking drinks from the cooler.
To summarize it, we use a value type (structure) to model our drink types because only one part of our code should be interacting with an instance of the drinks type at any one time. However, we use a reference type (class) to model our cooler because multiple parts of our code will be interacting with the same instance of the Cooler
type.
We are going to stress this many times in this book: one of the main differences between reference and values types is how we pass the instances of the type. When we pass an instance of a reference type, we are passing a reference to the original instance. This means that the changes made are reflexed in both the references. When we pass an instance of a value type, we are passing a new copy of the original instance. This means that the changes made in one instance are not reflexed in the other.
Before we examine protocol-oriented programming further, let's take a look at how we would rewrite our example in a protocol-oriented programming manner. We will start by creating our Drink
Protocol:
protocol Drink { var volume: Double {get set} var caffeine: Double {get set} var temperature: Double {get set} var drinkSize: DrinkSize {get set} var description: String {get set} }
Within our Drink
protocol, we defined the five properties every type that conforms to this protocol must provide. The DrinkSize
type is the same DrinkSize
type that we defined in the object-oriented section of this chapter.
Before we add any types that conform to our Drink
protocol, we want to extend the protocol. Protocol extensions were added to the Swift language in version 2, and they allow us to provide functionality to conforming types. This lets us define the behavior for all types that conform to a protocol rather than adding the behavior to each individual conforming type. Within the extension for our Drink
protocol, we will define two methods: drinking()
and temperaturChange()
. These are the same two methods that were in our Drink
superclass in the object-oriented programming section of this chapter. Following is the code for our Drink
extension:
extension Drink { mutating func drinking(amount: Double) { volume -= amount } mutating func temperatureChange(change: Double) { temperature += change } }
Now, any type that conforms to the Drink
protocol will automatically receive the drinking()
and the temperaturChange()
methods. Protocol extensions are perfect for adding common functionality to all the types that conform to a protocol. This is similar to adding functionality to a superclass where all subclasses receive the functionally from the superclass. The individual types that conform to a protocol can also shadow any functionality provided by an extension similar to overriding functionality from a superclass.
Now let's create our Jolt
and CaffeineFreeDietCoke
types:
struct Jolt: Drink { var volume: Double var caffeine: Double var temperature: Double var drinkSize: DrinkSize var description: String init(temperature: Double) { self.volume = 23.5 self.caffeine = 280 self.temperature = temperature self.description = "Jolt Energy Drink" self.drinkSize = DrinkSize.Can24 } } struct CaffeineFreeDietCoke: Drink { var volume: Double var caffeine: Double var temperature: Double var drinkSize: DrinkSize var description: String init(volume: Double, temperature: Double, drinkSize: DrinkSize) { self.volume = volume self.caffeine = 0 self.temperature = temperature self.description = "Caffiene Free Diet Coke" self.drinkSize = drinkSize } }
As we can see, both the Jolt
and
CaffeineFreeDietCoke
types are structures rather than classes. This means that they are both value types rather than reference types, as they were in the object-oriented design. Both of the types implement the five properties that are defined in the Drink
protocol as well as an initializer that will be used to initialize the instances of the types.
There is more code needed in these types as compared to the drink classes in the object-oriented example. However, it is easier to understand what is going on in these drink types because everything is being initialized within the type itself rather than in a superclass.
Finally, let's look at the cooler type:
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 } } }
As we can see, the Cooler
class is the same class that we created in the Object-oriented programming section of this chapter. There could be a very viable argument for creating the Cooler
type as a structure rather than a class, but it really depends on how we plan to use it in our code. Earlier, we stated that various parts of our code will need to interact with a single instance of our cooler. Therefore, in our example, it is better to implement our cooler as a reference type rather than a value type.
The following diagram shows how the new design looks:
Now that we have finished redesigning, let's summarize what protocol-oriented programming is and how it is different from object-oriented programming.