Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
All Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds

All About the Protocol

Save for later
  • 19 min read
  • 22 Nov 2016

article-image

In this article, Jon Hoffman, the author of the book Swift 3 Protocol Oriented Programming - Second Edition, talks about 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 Open Service Gateway Initiative (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 were appropriate, 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.

(For more resources related to this topic, see here.)

In this article you will learn the following:

  • How to define property and method requirements within a protocol
  • How to use protocol inheritance and composition
  • How to use a protocol as a type
  • What polymorphism is

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 an 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 it.

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 tasks. 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 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 article, 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.

Protocol syntax

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.

Defining 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 article. This concept is known as protocol composition.

Now let's see how we would add property requirements to our protocol.

Property requirements

A protocol can require that the conforming types provide certain properties with specified names and types. The protocol does not say whether 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. Both of these properties are defined as read-write properties. Any type that conforms to the FullName protocol must implement these properties. If we wanted to define the property as read-only, we would define it using only the get keyword, as shown in the following code:

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.

Method requirements

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 these methods as instance or type methods using the static keyword. Adding default values to the method's parameters is not allowed when defining the method withina 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, such as 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. 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.

Optional requirements

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.

It is important to note that only classes can adopt protocols that use the @objc attribute. Structures and enumerations cannot adopt these protocols.

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}
  @objc optional var emailAddress: String {get set}
  
  func dialNumber()
  @objc 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.

Protocol inheritance

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, as well as the requirements defined in the FullName protocol. As an example, we could define a Student structure that conforms to the Person protocol as shown in the following code:

struct Student: Person {
    var firstName = ""
    var lastName = ""
    var age = 0
    
    func getFullName() -> String {
        return "(firstName) (lastName)"
    }
}

Note 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.

Protocol composition

Protocol composition lets our types adopt multiple protocols. This is a major advantage that we get when we use protocols rather than a class hierarchy because classes, in Swift and other single-inheritance languages, can only inherit from one superclass. The syntax for protocol composition is the same as the protocol inheritance that we just saw. The following example shows how to do protocol composition:

Unlock access to the largest independent learning library in Tech for FREE!
Get unlimited access to 7500+ expert-authored eBooks and video courses covering every tech area you can think of.
Renews at AU $19.99/month. Cancel anytime
struct MyStruct: ProtocolOne, ProtocolTwo, Protocolthree {
    // implementation here
}

Protocol composition allows us to break our requirements into many smaller components rather than inheriting all requirements from a single superclass or class hierarchy. This allows our type families to grow in width rather than height, which means we avoid creating bloated types that contain requirements that are not needed. Protocol composition may seem like a very simple concept, but it is a concept that is essential to protocol-oriented programming. Let's look at an example of protocol composition so we can see the advantage we get from using it.

Let's say that we have the class hierarchy shown in the following diagram:

all-about-protocol-img-0

In this class hierarchy, we have a base class named Athlete. The Athlete base class then has two subclasses named Amateur and Pro. These classes are used depending on whether the athlete is an amateur athlete or a pro athlete. An amateur athlete may be a colligate athlete, and we would need to store information such as which school they go to and their GPA. A pro athlete is one that gets paid for playing the game. For the pro athletes, we would need to store information such as what team they play for and their salary.

In this example, things get a little messy under the Amateur and Pro classes. As we can see, we have a separate football player class under both the Amateur and Pro classes (the AmFootballPlayer and ProFootballPlayer classes). We also have a separate baseball class under both the Amateur and Pro classes (the AmBaseballPlayer and ProBaseballPlayer classes). This will require us to have a lot of duplicate code between theses classes.

With protocol composition, instead of having a class hierarchy where our subclasses inherit all functionality from a single superclass, we have a collection of protocols that we can mix and match in our types.

all-about-protocol-img-1

We then use one or more of these protocols as needed for our types. For example, we can create an AmFootballPlayer structure that conforms to the Athlete, Amateur, and Footballplayer protocols. We could also create the ProFootballPlayer structure that conforms to the Athlete, Pro, and Footballplayer protocols. This allows us to be very specific about the requirements for our types and only adopt the requirements that we need.

From a pure protocol point of view, this last example may not make a lot of sense right now because protocols only define the requirements.

One word of warning: If you find yourself creating numerous protocols that only contain one or two requirements in them, then you are probably making your protocols too granular. This will lead to a design that is hard to maintain and manage.

Now let's look at how a protocol is a full-fledged type in Swift.

Using protocols as a type

Even though no functionality is implemented in a protocol, they are still considered a full-fledged type in the Swift programming language, and can mostly be used like any other type. What this means is that we can use protocols as parameters or return types for a function. We can also use them as the type for variables, constants, and collections. Let's take a look at some examples. For these next few examples, we will use the following PersonProtocol protocol:

protocol PersonProtocol {
    var firstName: String {get set}
    var lastName: String {get set}
    var birthDate: Date {get set}
    var profession: String {get}
    
    init (firstName: String, lastName: String, birthDate: Date)
}

In this PersonProtocol, we define four properties and one initializer.

For this first example, we will show how to use a protocol as a parameter and return type for a function, method, or initializer. Within the function itself, we also use the PersonProtocol as the type for a variable:

func updatePerson(person: PersonProtocol) -> PersonProtocol {
var newPerson: PersonProtocol
// Code to update person goes here
  return newPerson
 }

We can also use protocols as the type to store in a collection, as shown in the next example:

var personArray = [PersonProtocol]()
var personDict = [String: PersonProtocol]()

The one thing we cannot do with protocols is create an instance of one. This is because no functionality is implemented within a protocol. As an example, if we tried to create an instance of the PersonProtocol protocol, as shown in the following example, we would receive the error error: protocol type 'PersonProtocol' cannot be instantiated:

var test = PersonProtocol(firstName: "Jon", lastName: "Hoffman", ?birthDate: bDateProgrammer)

We can use the instance of any type that conforms to our protocol anywhere that the protocol type is required. As an example, if we define a variable to be of the PersonProtocol protocol type, we can then populate that variable with the instance of any type that conforms to the PersonProtocol protocol. Let's assume that we have two types named SwiftProgrammer and FootballPlayer that conform to the PersonProtocol protocol. We can then use them as shown in this next example:

var myPerson: PersonProtocol

myPerson = SwiftProgrammer(firstName: "Jon", lastName: "Hoffman", birthDate: bDateProgrammer)

myPerson = FootballPlayer(firstName: "Dan", lastName: "Marino", ?birthDate: bDatePlayer)

In this example, the myPerson variable is defined to be of the PersonProtocol protocol type. We can then set this variable to instances of either of the SwiftProgrammer and FootballPlayer types. One thing to note is that Swift does not care if the instance is a class, structure, or enumeration. It only matters that the type conforms to the PersonProtocol protocol type.

As we saw earlier, we can use our PersonProtocol protocol as the type for an array, which means that we can populate the array with instances of any type that conforms to the PersonProtocol protocol. The following is an example of this (note that the bDateProgrammer and bDatePlayer variables are instances of the Date type that would represent the birthdate of the individual):

var programmer = SwiftProgrammer(firstName: "Jon", lastName: "Hoffman", birthDate: bDateProgrammer)

var player = FootballPlayer(firstName: "Dan", lastName: "Marino", birthDate: bDatePlayer)

var people: [PersonProtocol] = []
people.append(programmer)
people.append(player)

What we are seeing in these last couple of examples is a form of polymorphism. To use protocols to their fullest potential, we need to understand what polymorphism is.

Polymorphism with protocols

The word polymorphism comes from the Greek roots poly (meaning many) and morphe (meaning form). In programming languages, polymorphism is a single interface to multiple types (many forms). There are two reasons to learn the meaning of the word polymorphism. The first reason is that using such a fancy word can make you sound very intelligent in casual conversion. The second reason is that polymorphism provides one of the most useful programming techniques not only in object-oriented programming, but also protocol-oriented programming.

Polymorphism lets us interact with multiple types though a single uniform interface. In the object-oriented programming world the single uniform interface usually comes from a superclass, while in the protocol-oriented programming world that single interface usually comes from a protocol.

In the last section, we saw two examples of polymorphism with Swift. The first example was the following code:

var myPerson: PersonProtocol

myPerson = SwiftProgrammer(firstName: "Jon", lastName: "Hoffman", birthDate: bDateProgrammer)

myPerson = FootballPlayer(firstName: "Dan", lastName: "Marino", birthDate: bDatePlayer)

In this example, we had a single variable of the PersonProtocol type. Polymorphism allowed us to set the variable to instances of any type that conforms to the PersonProtocol protocol, such as the SwiftProgrammer or FootballPlayer types.

The other example of polymorphism was in the following code:

var programmer = SwiftProgrammer(firstName: "Jon", lastName: "Hoffman", birthDate: bDateProgrammer)

var player = FootballPlayer(firstName: "Dan", lastName: "Marino", birthDate: bDatePlayer)

var people: [PersonProtocol] = []
people.append(programmer)
people.append(player)

In this example, we created an array of PersonProtocol types. Polymorphism allowed us to add instances of any types that conform to PersonProtocol to this array.

When we access an instance of a type though a single uniform interface, as we just showed, we are unable to access type-specific functionality. As an example, if we had a property in the FootballPlayer type that records the age of the player, we would be unable to access that property because it is not defined in the PeopleProtocol protocol.

If we do need to access type-specific functionality, we can use type casting.

Type casting with protocols

Type casting is a way to check the type of an instance and/or to treat the instance as a specified type. In Swift, we use the is keyword to check whether an instance is of a specific type and the as keyword to treat an instance as a specific type.

The following example shows how we would use the is keyword:

if person is SwiftProgrammer {
  print("(person.firstName) is a Swift Programmer")
}

In this example, the conditional statement returns true if the person instance is of the SwiftProgrammer type or false if it isn't. We can also use the switch statement (as shown in the next example) if we want to check for multiple types:

for person in people {
    switch (person) {
    case is SwiftProgrammer:
        print("(person.firstName) is a Swift Programmer")
    case is FootballPlayer:
        print("(person.firstName) is a Football Player")
    default:
        print("(person.firstName) is an unknown type")
    }
}

We can use the where statement in combination with the is keyword to filter an array to only return instances of a specific type. In the next example, we filter an array that contains instances of the PersonProtocol to only return those elements of the array that are instances of the SwiftProgrammer type:

for person in people where person is SwiftProgrammer {
    print("(person.firstName) is a Swift Programmer")
}

Now let's look at how we would cast an instance to a specific type. To do this, we can use the as keyword. Since the cast can fail if the instance is not of the specified type, the as keyword comes in two forms: as? and as!. With the as? form, if the casting fails it returns a nil; with the as! form, if the casting fails we get a runtime error. Therefore, it is recommended to use the as? form unless we are absolutely sure of the instance type or we perform a check of the instance type prior to doing the cast.

The following example shows how we would use the as? keyword to attempt to cast an instance of a variable to the SwiftProgammer type:

if let p = person as? SwiftProgrammer {
    print("(person.firstName) is a Swift Programmer")
}

Since the as? keyword returns an optional, in the last example we could use optional binding to perform the cast. If we are sure of the instance type, we can use the as! keyword as shown in the next example:

for person in people where person is SwiftProgrammer {
  let p = person as! SwiftProgrammer
}

Summary

While protocol-oriented programming is about so much more than just the protocol, it would be impossible to have the protocol-oriented programming paradigm without the protocol. We can think of the protocol as the backbone of protocol-oriented programming. Therefore, it is important to fully understand the protocol in order to properly implement protocol-oriented programming.

Resources for Article:


Further resources on this subject: