With protocol-oriented programming, we should always begin our design with the protocols, but how should we design these protocols? In the object-oriented programming world, we have superclasses that contain all the base requirements for the subclasses. Protocol design is a little bit different.
In the protocol-oriented programming world, we use protocols instead of superclasses, and it is preferable to break the requirements into smaller, more specific protocols rather than having bigger monolithic protocols. In this section, we will look at how we can separate the requirements into smaller, very specific protocols and how to use protocol inheritance and composition. In Chapter 3, Extensions, we will take this a little further and show you how to add functionality to all types that conform to a protocol using protocol extensions.
For the example in this section, we will model something that I enjoy building: Robots. There are many types of robots with lots of different sensors, so our model will need the ability to grow and handle all the different options. Since all robots have some form of movement, we will start off by creating a protocol that will define the requirements for this movement. We will name this protocol RobotMovement:
protocol RobotMovement {
func forward(speedPercent: Double)
func reverse(speedPercent: Double)
func left(speedPercent: Double)
func right(speedPercent: Double)
func stop()
}
In this protocol, we define the five methods that all conforming types must implement. These methods will move the robot in the forward, reverse, left or right directions as well as stop the robot. This protocol will meet our needs if we only want the robot to travel in two dimensions but what if we had a flying robot? For this we would need our robot to also go up and down. For this we can use protocol inheritance to create a protocol that adds the additional requirements for traveling in three dimensions:
protocol RobotMovementThreeDimensions: RobotMovement {
func up(speedPercent: Double)
func down(speedPercent: Double)
}
Notice that we use protocol inheritance when we create this protocol to inherit the requirements from the original RobotMovement protocol. This allows us to use polymorphism as described in the Polymorphism with protocols sections of this chapter. This allows us to use instances of types that conform to either of these protocols interchangeably by using the interface provided by the RobotMovement protocol. We can then determine if the robot can travel in three dimensions by using the is keyword, as described in the Type casting with protocols section of this chapter, to see if the RobotMovement instance conforms to the RobotMovementThreeDimensions protocol or not.
Now we need to add some sensors to our design. We will start off by creating a Sensor protocol that all other sensor types will inherit from. This protocol will contain four requirements. The first two will be read-only properties that define the name and type for the sensor. We will need an initiator that lets us name the sensor and a method that will be used to poll the sensor:
protocol Sensor {
var sensorType: String {get}
var sensorName: String {get set}
init (sensorName: String)
func pollSensor()
}
The sensor type would be used to define the type of sensor and would contain a string, such as DHT22 Environment Sensor. The sensor name would let us distinguish between multiple sensors and would contain a string, such as Rear Environment Sensor. The pollSensor() method would be used to perform the default operation by the sensor. Generally, this method would be used to read the sensor at regular intervals.
Now we will create requirements for some specific sensor types. The following example shows how we would create the requirements for an environment sensor:
protocol EnvironmentSensor: Sensor {
func currentTemperature() -> Double
func currentHumidity() -> Double
}
This protocol inherits the requirements from the Sensor protocol and adds two additional requirements that are unique for environment sensors. The currentTemperature() method would return the last temperature reading from the sensor and the currentHumidity() method would return the last humidity reading from the sensor. The pollSensor() method from the Sensor protocol would be used to read the temperature and humidity at regular intervals. The pollSensor() method would probably run on a separate thread.
Let's go ahead and create a couple more sensor types:
protocol RangeSensor: Sensor {
func setRangeNotification(rangeCentimeter: Double,
rangeNotification: () -> Void)
func currentRange() -> Double
}
protocol DisplaySensor: Sensor {
func displayMessage(message: String)
}
protocol WirelessSensor: Sensor {
func setMessageReceivedNotification(messageNotification:
(String) -> Void)
func messageSend(message: String)
}
You will notice that two of these protocols (RangeSensor and WirelessSensor) define methods that set notifications (setRangeNotification and setMessageReceivedNotifications). These methods accept closures in the method parameters and will be used within the pollSensor() method to notify robot code immediately if something has happened. With RangeSensor types, the closure would be called if the robot was within a certain distance of an object and with WirelessSensor types the closure would be called if a message came in.
There are two advantages that we get from a protocol-oriented design like this one. The first is each of the protocols only contain the specific requirements needed for their particular sensor type. The second is we are able to use protocol composition to allow a single type to conform to multiple protocols. As an example, if we have a Display sensor that has Wi-Fi built in, we would create a type that conforms to both the DisplaySensor and WirelessSensor protocols.
There are many other sensor types; however, this will give us a good start for our robot. Now let's create a protocol that will define the requirements for the robot types:
protocol Robot {
var name: String {get set}
var robotMovement: RobotMovement {get set}
var sensors: [Sensor] {get}
init (name: String, robotMovement: RobotMovement)
func addSensor(sensor: Sensor)
func pollSensors()
}
This protocol defines three properties, one initiator, and two methods that will need to be implemented by any type that conforms with this protocol. These requirements will give us the basic functionality needed for the robots.
It may be a bit confusing thinking about all these protocols, especially if we are used to having only a few superclass types. It usually helps to have a basic diagram of our protocols. The following image shows a diagram for the protocols that we just defined with the protocol hierarchy:
This gives us the basic idea of how we can design a protocol hierarchy. You will notice that each of the protocols define the specific requirements for each device type. In Chapter 6, Protocol-Oriented Programming, we will go into greater detail on how to model our requirements with protocols.
In this section, we used protocols to define the requirements for the components of a robot. Now it is your turn; take a moment and see if you can create a concrete implementation of the Robot protocol without creating any concrete implementations of the other protocols. The key to understanding protocols is understanding how to use them without the concrete types that conform to them. In the downloadable code for this book, we have a sample class named SixWheelRover that conforms to the Robot protocol that you can compare your implementation to.
Now let's see how Apple uses protocols in the Swift standard library.