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 super-classes that contain all of the base requirements for the sub-classes.
In the protocol-oriented programming world, we use protocols instead of super-classes and it is preferable to break our requirements into smaller, very specific protocols rather than having bigger monolithic protocols. In this section we will look at how we can separate our requirements into smaller, very specific protocols and then use protocol composition to add the requirements to our types.
In this section, we will be demonstrating how to define property requirements in our protocols. In the next chapter, Let's Extend Some Types, we will show 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 my daughters and I really enjoy doing and that is Tae Kwon Do. As we progress through the Tae Kwon Do ranks, we are required to take tests to see if we know the requirements for our belt color and rank. If we pass the tests, we are promoted to the next belt color and/or rank. Each of these belt colors and ranks has different testing requirements. For example, to test for a yellow belt we need to do our form, one-step, and focus pad drills but to test for a blue belt we need to do our form, focus pad drills, board breaks, and sparring.
The different testing areas are:
If we were going to model the different testing requirements in an object-oriented way, we would start off by creating a super class that contained all of the testing areas even though not all subclasses will need all of the testing areas. We would then create sub-classes of this super-class that would contain the individual requirements for each of the tests. Before we see how we would do this in an object-oriented way, let's create two enumerations that will define the different belt colors and ranks. We will also create a type alias that will define the board break requirements. The following code shows how we would define these:
enum TKDBeltColors: Int { case White, Yellow, Orage, Green, Blue, Purple, Red, FirstDegreeBlack, SecondDegreeBlack, ThirdDegreeBlack, ForthDegreeBlack } enum TKDColorRank: Int { case NoRank, Probationary, Decided, Intermediate, Senior } typealias BoardBreak = (name: String, required: Bool)
For the board breaking requirements, we have to break a certain number of boards (depending on the rank) from a list of possible techniques. Some of these techniques are required techniques while others are optional. Our BoardBreak typealias
is a tuple that contains a string value that specifies the technique and a Boolean value that specifies whether that technique is required or not.
Now that we have defined our enumerations and typealias
, let's design our class hierarchy that will model our different testing requirements. We will start off by creating a super-class named TKDTestingRequirements
and all of the other Tae Kwon Do testing classes will be sub-classes of this super-class:
class TKDTestingRequirements { var color = TKDBeltColors.White var rank = TKDColorRank.NoRank var formName = "" var focusPadDrills = [String]() var focusPadMissesAllowed = 2 var sparringRoundsRequired = 0 var boardBreaksRequired = 0 var boardBreaks: [BoardBreak]? var oneStepsNumbers: [Int]? }
In the TKDTestingRequirments
class, we defined all possible requirements that we may have for testing. This includes requirements such as the one-steps, which are only requirements for the white and yellow belt testing. All other belt and rank classes will need to set the oneStepsNumbers
property to nil
, indicating that it is not required for testing.
We can then sub-class the TKDTestingRequirments
class to define the requirements for our tests. As examples, the following classes define the requirements for the White and Senior Green Belt tests:
class WhiteBelt: TKDTestingRequirements { override init () { super.init() color = TKDBeltColors.White rank = TKDColorRank.NoRank formName = "Chon-Ji" oneStepsNumbers = [1,2,3] focusPadDrills = ["Reverse Punch", "Number 1 Front Kick"] focusPadMissesAllowed = 2 boardBreaksRequired = 0 sparringRoundsRequired = 0 } } class GreenBeltSenior: TKDTestingRequirements { override init() { super.init() color = TKDBeltColors.Green rank = TKDColorRank.NoRank formName = "Do-San" focusPadDrills = ["Back Fist", "Number 2 Crescent"] focusPadMissesAllowed = 2 boardBreaks = [(name:"Hammer Fist", required: false), (name:"Front Kick", required: false)] boardBreaksRequired = 1 sparringRoundsRequired = 2 oneStepsNumbers = nil } }
This design would work and it would be fairly easy to create different classes for each of our tests. However, we are defining requirements that are not necessary for all of our testing; this is not an optimal design. As an example, we are setting the boardBreaksRequired
and sparringRoundsRequired
properties to 0
in the WhiteBelt
class. Wouldn't it be nice if we only needed to define the requirements that are necessary for the particular test? With protocol-oriented programming, we can. Let's redesign this solution in a protocol-oriented way.
With protocol-oriented programming, instead of putting all of the requirements into a single type, as we did with the object-oriented example, we separate the requirements into multiple smaller protocols. Each of these protocols will contain the requirements for a specific testing area. For our example, the requirements are the individual testing areas:
With object-oriented programming, a class can only inherit from a single super class; therefore we need to put all of our requirements into a single monolithic super-class. With protocol composition we are able to let a single type adopt multiple protocols; thus, we can break our requirements into multiple smaller protocols. This allows us to implement only the requirements necessary for our type.
Our testing area protocols would look like this:
protocol TKDRankProtocol { var color: TKDBeltColors {get} var rank: TKDColorRank {get} } protocol BoardBreakProtocol { var boardBreaks: [BoardBreak] {get} var boardBreaksRequired: Int {get} } protocol FormProtocol { var formName: String {get} } protocol FocusPadProtocol { var focusPadDrills: [String] {get} var focusPadMissesAllowed: Int {get} } protocol OneStepsProtocol { var oneStepsNumbers: [Int] {get} } protocol SparringProtocol { var sparringRoundsRequired: Int {get} }
Each of these protocols only contains the requirements for its specific testing areas. When we design our applications, it is important to separate our requirements into smaller protocols like this so we do not have to implement requirements that do not pertain to our type.
We could now adopt these protocols as shown in the next example:
struct WhiteBelt: TKDRankProtocol, FormProtocol, OneStepsProtocol, FocusPadProtocol { let color = TKDBeltColors.White let rank = TKDColorRank.NoRank let formName = "Chon-Ji" let oneStepsNumbers = [1,2,3] let focusPadDrills = ["Reverse Punch", "Number 1 Front Kick"] let focusPadMissesAllowed = 2 } struct GreenBeltSenior: TKDRankProtocol, FormProtocol, BoardBreakProtocol, FocusPadProtocol, SparringProtocol { let color = TKDBeltColors.Green let rank = TKDColorRank.NoRank let formName = "Do-San" let focusPadDrills = ["Back Fist", "Number 2 Crescent"] let focusPadMissesAllowed = 2 let boardBreaks = [(name:"Hammer Fist", required: false), (name:"Front Kick", required: false)] let boardBreaksRequired = 1 let sparringRoundsRequired = 2 }
For each of these types, we only adopt the requirements that are needed; therefore we are not implementing any unnecessary requirements. This is a more optimal design and should be preferred to the object-oriented design that we saw earlier in this section.
We can then use the is
and as
keywords as described in the Type casting with protocols section of this chapter to check if an instance conforms to a specific type. Checking for and casting to a specific protocol takes the same form as checking for and casting to a specific type.