Coming from an object-oriented background, I am very familiar with protocols (or interfaces, as they are known in other object-oriented languages); however, prior to Apple introducing protocol-oriented programming, protocols or interfaces were rarely the focal point of my application designs unless I was working with an OSGI-based project. When I designed an application in an object-oriented way, I always began the design with the objects. The protocols or interfaces were then used where they made sense mainly for polymorphism when a class hierarchy did not make sense. Now all that has changed and with protocol-oriented programming the protocol has been elevated to the focal point of our application design.
In this chapter you will learn:
When we are designing an application in an object-oriented way, we begin the design by focusing on the objects and how they interact. 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. We cannot create an object without a blueprint that tells the application what attributes 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 the object into a single type.
Most object-oriented programming languages contain an interface type. This interface is a type that contains method and property signatures but does not contain any implementation details. An interface can be considered a contract where any type that conforms to the interface must implement the required functionality defined within the interface.
Interfaces in most object-oriented languages are primarily used as a way to achieve polymorphism. There are some frameworks, such as OSGI, that use interfaces extensively; however in most object-oriented designs the interface takes a back seat to the class and class hierarchy.
Designing an application in a protocol-oriented way is significantly different from designing it in an object-oriented way. As we stated earlier, object-oriented design begins with the objects and the interaction between the objects while protocol-oriented design begins with the protocol. While protocol-oriented design is about so much more than just the protocol, we can think of the protocol as the backbone of protocol-oriented programming. After all, it would be pretty hard to have protocol-oriented programming without the protocol.
A protocol in Swift is similar to interfaces in object-oriented languages where the protocol acts as a contract that defines the methods, properties, and other requirements needed by our types to perform their task. We say that the protocol acts as a contract because any type that adopts, or conforms, to the protocol promises to implement the requirements defined by the protocol.
Any class, structure, or enumeration can conform to a protocol. A type cannot conform to a protocol unless it implements all required functionality defined within the protocol. If a type adopts a protocol and it does not implement all functionality defined by the protocol, we will get a compile time error and the project will not compile.
Most modern object-oriented programming languages implement their standard library with a class hierarchy; however, the basis of the Swift's standard library is the protocol. (https://developer.apple.com/library/prerelease/ios/documentation/General/Reference/SwiftStandardLibraryReference/index.html). Therefore, not only does Apple recommend that we use the protocol-oriented programming paradigm in our applications, but they also use it in the Swift standard library.
With the protocol being the basis of the Swift standard library and also the backbone of the protocol-oriented programming paradigm, it is very important that we fully understand what the protocol is and how we can use it. In this chapter we will go over the basic usage of the protocol, which will include the syntax for defining the protocol, how to define requirements in a protocol, and how to make our types conform to a given protocol. At the end of this chapter, we will look at how to implement the delegation pattern in Swift using protocols and how to use protocol composition to create small, very specific protocols rather than large monolithic ones.
In this section we will look at how to define a protocol, define requirements within a protocol, and specify that a type conforms to a protocol. We will start off by seeing how we define a protocol.
The syntax we use to define a protocol is very similar to the syntax used to define a class, structure, or enumeration. The following example shows the syntax used to define a protocol:
protocol MyProtocol { //protocol definition here }
To define the protocol, we use the protocol
keyword followed by the name of the protocol. We then put the requirements, which our protocol defines, between curly brackets. Custom types can state that they conform to a particular protocol by placing the name of the protocol after the type's name, separated by a colon. The following example shows how we would state that the MyStruct
structure conforms to the MyProtocol
protocol:
struct MyStruct: MyProtocol { //structure implementation here }
A type can also conform to multiple protocols. We list the multiple protocols that the type conforms to by separating them with commas. The following example shows how we would specify that the MyStruct
structure type conforms to the MyProtocol
, AnotherProtocol
, and ThirdProtocol
protocols:
struct MyStruct: MyProtocol, AnotherProtocol, ThirdProtocol { // Structure implementation here }
Having a type conform to multiple protocols is a very important concept within protocol-oriented programming, as we will see later in the chapter. This concept is known as protocol composition.
Next let's see how we would add property requirements to our protocol.
A protocol can require that the conforming types provide certain properties with specified names and types. The protocol does not say if the property should be a stored or computed property, because the implementation details are left up to the conforming types.
When defining a property within a protocol, we must specify whether the property is a read-only or a read-write property by using the get
and set
keywords. We also need to specify the property's type since we cannot use the type inference in a protocol. Let's look at how we would define properties within a protocol by creating a protocol named FullName
as shown in the next example:
protocol FullName { var firstName: String {get set} var lastName: String {get set} }
In the FullName
protocol we define two properties named firstName
and lastName
. Any type that conforms to the FullName
protocol must implement both of these properties. Both of these properties are defined as read-write properties. If we wanted to define the property as a read-only, we would define it using only the get
keyword like this:
var readOnly: String {get}
If the property is going to be a type property then we must define it in the protocol. A type property is defined using the static
keyword as shown in the following example.
static var typeProperty: String {get}
Now let's see how we would add method requirements to our protocol.
A protocol can require that the conforming types provide specific methods. These methods are defined within the protocol exactly as we define them within a class or structure but without the curly brackets and method body. We can define that these methods are instance or type methods using the static
keyword. Adding default values to the method's parameters is not allowed when defining the method within a protocol.
Let's add a method named getFullName()
to our FullName
protocol:
protocol FullName { var firstName: String {get set} var lastName: String {get set} func getFullName() -> String }
Our fullName
protocol now requires one method named getFullName()
and two read-write properties named firstName
and lastName
.
For value types, like the structure, if we intend for a method to modify the instances that it belongs to, we must prefix the method definition with the mutating
keyword. This keyword indicates that the method is allowed to modify the instance it belongs to and any properties of that instance. The following example shows how to use the mutating
keyword with a method definition:
mutating func changeName()
If we mark a method requirement as mutating we do not need to write the mutating
keyword for that method when we adopt the protocol with a reference (class) type. The mutating keyword is only used with value (structures or enumerations) types.
There are times when we want protocols to define optional requirements—that is, methods or properties that are not required to be implemented. To use optional requirements we need to start off by marking the protocol with the @objc
attribute.
To mark a property or method as optional, we use the optional
keyword. Let's look at how we would use the optional
keyword to define optional properties and methods:
@objc protocol Phone { var phoneNumber: String {get set} optional var emailAddress: String {get set} func dialNumber() optional func getEmail() }
In the Phone
protocol we just created, we define a required property named phoneNumber
and an optional property named emailAddress
. We also defined a required function named dialNumber()
and an optional function named getEmail()
.
Now let's explore how protocol inheritance works.
Protocols can inherit requirements from one or more other protocols and then add additional requirements. The following code shows the syntax for protocol inheritance:
protocol ProtocolThree: ProtocolOne, ProtocolTwo { // Add requirements here }
The syntax for protocol inheritance is very similar to class inheritance in Swift except that we are able to inherit from more than one protocol. Let's see how protocol inheritance works. We will use the FullName
protocol that we defined earlier in this section and create a new protocol named Person
.
protocol Person: FullName { var age: Int {get set} }
Now when we create a type that conforms to the Person
protocol we must implement the requirements defined in the Person
protocol and also the requirements defined in the FullName
protocol. As an example, we could define a Student
structure that conforms to the Person
protocol like this:
struct Student: Person { var firstName = "" var lastName = "" var age = 0 func getFullName() -> String { return "\(firstName) \(lastName)" } }
Notice that in the Student
structure we implemented the requirements defined in both the FullName
and Person
protocols; however, the only protocol specified when we defined the Student
structure was the Person
protocol. We only needed to list the Person
protocol because it inherited all of the requirements from the FullName
protocol.
Now let's look at a very important concept in the protocol-oriented programming paradigm: protocol composition.