There has been a lot of buzz ever since the 2015 WWDC talk on Protocol Oriented Programming. But the underlying principle has been around forever. Protocol is a feature provided by the programming language to facilitate polymorphism.
What is polymorphism?
There are many different kinds of polymorphisms as described in Wikipedia. I’ll focus on the kind that is the most important in Swift.
When I first learned about polymorphism in C++, it was at the function level. The goal is to avoid having to duplicate functions that take different types of parameters but do the same thing, making code reuse possible.
Duplication at the function level
Let’s take an example for a whirl.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
struct Professor { var facultyID: String var name: String var phone: String var department: String } struct Student { var studentID: String var name: String var phone: String var major: String } func call(professor: Professor) { debugPrint("Calling professor \(professor.name) at \(professor.phone)") } func call(student: Student) { debugPrint("Calling student \(student.name) at \(student.phone)") } let steve = Professor(facultyID: "123", name: "Steve", phone: "123-456-7890", department: "English") let tim = Student(studentID: "456", name: "Tim", phone: "987-654-3210", major: "Math") call(steve) call(tim) |
It should be possible to call a professor or a student. But two call()
functions are required because steve
is a professor and tim
is a student. Although they both have a phone number, they are of different types.
Inheritance at the function level
An early attempt to solve this classic problem in an object oriented programming language is through the use of class inheritance.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
class Person { var name: String var phone: String init(name: String, phone: String) { self.name = name self.phone = phone } } class Professor: Person { var facultyID: String var department: String init(facultyID: String, name: String, phone: String, department: String) { self.facultyID = facultyID self.department = department super.init(name: name, phone: phone) } } class Student: Person { var studentID: String var major: String init(studentID: String, name: String, phone: String, major: String) { self.studentID = studentID self.major = major super.init(name: name, phone: phone) } } func call(person: Person) { debugPrint("Calling \(person.phone)") } let steve = Professor(facultyID: "123", name: "Steve", phone: "123-456-7890", department: "English") let tim = Student(studentID: "456", name: "Tim", phone: "987-654-3210", major: "Math") call(steve) call(tim) |
Professor
and Student
are changed to classes and inherit from Person
. The shared attributes name
and phone
are now in the base class.
The call()
function takes a Person
object as its parameter. The function body can simply invoke person.phone
.
You can invoke the call()
function and pass in either a Professor
or Student
. The reason it works is because Professor
and Student
inherit from the same Person
base class. At runtime, the object is cast to a Person
object to be accessed in the body.
Inheritance at the entity level
So far, you’ve seen polymorphism at the function level. You avoid having to duplicate the call()
function by using inheritance and base class pointer.
But the same principle can also be applied at the entity level.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
class Person { var name: String var phone: String init(name: String, phone: String) { self.name = name self.phone = phone } func call(person: Person) { debugPrint("Calling \(person.phone)") } } class Professor: Person { var facultyID: String var department: String init(facultyID: String, name: String, phone: String, department: String) { self.facultyID = facultyID self.department = department super.init(name: name, phone: phone) } } class Student: Person { var studentID: String var major: String init(studentID: String, name: String, phone: String, major: String) { self.studentID = studentID self.major = major super.init(name: name, phone: phone) } } let steve = Professor(facultyID: "123", name: "Steve", phone: "123-456-7890", department: "English") let tim = Student(studentID: "456", name: "Tim", phone: "987-654-3210", major: "Math") steve.call(tim) tim.call(steve) |
I’ve moved the call()
function to the Person
class definition. You now invoke the call method on a Person object. After all, a person calls another person, right?
Class inheritance allows you to extract common behavior to a base class.
Protocol at the entity level
Over time, we’ve discovered inheritance comes with its own problems.
Two subclasses may share some common behaviors but not a lot. As a result, the base class becomes small and not very useful because you can’t put a lot of common behaviors in it. On the other hand, if you try to put more common behaviors in it, you’ll end up having to override the default behaviors in your subclasses.
Another problem occurs when your application grows. You need to model new objects. Expanding your class hierarchy to accommodate these new classes can also quickly turn into a nightmare. You may need to introduce new superclasses and regroup your subclasses. This means existing applications that use your class hierarchy will be completely broken when they are upgraded to use your new class hierarchy.
You often hear composition is preferred over inheritance. Protocols in Swift allow you to compose different behaviors in your classes.
(For an object oriented programming language such as C++, it allows a subclass to inherit from multiple superclasses. This is called multiple inheritance. And you can use it to simulate protocols. This mix-and-match concept is the same.)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
protocol Callable { var phone: String { get set } func call(callee: Callable) } struct Professor: Callable { var facultyID: String var name: String var phone: String var department: String func call(callee: Callable) { debugPrint("Calling \(callee.phone)") } } struct Student: Callable { var studentID: String var name: String var phone: String var major: String func call(callee: Callable) { debugPrint("Calling \(callee.phone)") } } let steve = Professor(facultyID: "123", name: "Steve", phone: "123-456-7890", department: "English") let tim = Student(studentID: "456", name: "Tim", phone: "987-654-3210", major: "Math") steve.call(tim) tim.call(steve) |
The Callable
protocol specifies that in order for a person to be callable, he needs to have a phone number and a call()
method. At this point, it simply declares such a method. It doesn’t require the method to be defined just yet.
The Professor
and Student
classes conform to the Callable
protocol by defining the phone
variable and the call()
method. They need to provide the method body to satisfy the protocol requirements.
The way you use the objects and methods remain the same.
Extension at the entity level
Protocols are great because they allow you to customize behaviors while not inheriting the hindrance from the parent.
But we’re back at our original problem with duplication. The call methods for both Professor
and Student
are the same. Wouldn’t it be nice to remove this duplication?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
protocol Callable { var phone: String { get set } func call(callee: Callable) } extension Callable { func call(callee: Callable) { debugPrint("Calling \(callee.phone)") } } struct Professor: Callable { var facultyID: String var name: String var phone: String var department: String } struct Student: Callable { var studentID: String var name: String var phone: String var major: String } let steve = Professor(facultyID: "123", name: "Steve", phone: "123-456-7890", department: "English") let tim = Student(studentID: "456", name: "Tim", phone: "987-654-3210", major: "Math") steve.call(tim) tim.call(steve) |
You can provide an extension to the Callable
protocol to define the default behavior of the call()
method. Professor
and Student
can simply use the default call()
method if that’s sufficient. Or, they can define a customized version of the call()
method.
A real life use case can be local vs long-distance calling. A default call()
method can implement the details to make a local area call to another person. But a customized call()
method can have extra instructions to process payment.
Generics at the function level
This is great for object oriented programming languages. But functional programming is the new buzz now. To me, functional programming isn’t new. It’s been around for ages. If preceded object oriented programming.
Let’s see how we can combine protocols and generics to achieve polymorphism back at the function level.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
protocol Callable { var phone: String { get set } } struct Professor: Callable { var facultyID: String var name: String var phone: String var department: String } struct Student: Callable { var studentID: String var name: String var phone: String var major: String } func call<T: Callable>(callee: T) { debugPrint("Calling \(callee.phone)") } let steve = Professor(facultyID: "123", name: "Steve", phone: "123-456-7890", department: "English") let tim = Student(studentID: "456", name: "Tim", phone: "987-654-3210", major: "Math") call(steve) call(tim) |
Here, the Callable
protocol only requires a phone number. And we’ve changed Professor
and Student
back to structs. In Swift, structs can conform to protocols like classes. They conform to Callable
by defining the phone
variable.
Next, we define the call()
function to take a generic parameter that confirms to the Callable
protocol. In the method body, you can invoke callee.phone
.
Finally, we’re back at solving our original problem – avoiding function duplication.
In conclusion, writing reusable code without duplication is at the heart of good software design. Pointers, inheritance, interfaces, protocols, and generics are features provided by programming languages to achieve the same goal – polymorphism. The Clean Swift architecture makes heavy use of protocols throughout its VIP cycle to isolate your source code dependencies.
The generics advantages (Updated)
Doug commented below and asked what the advantages are for using Swift generics for the call()
function above. In this specific case, there’s none. I was just trying to illustrate how you could use generics with the simple Callable
protocol to implement polymorphism in Swift.
But that wouldn’t be a very good example, isn’t it? So here’s an expanded version.
Your Callable
protocol can be more complex. For example, if you want to allow a phone number to be a String
or Int
because you want people to be able to dial 123-456-7890 or 1234567890. You’ll then need to use associatedtype
in your Callable
protocol. The PhoneNumber
associated type acts as a placeholder for the type of the phone number, which can be a string or integer.
Furthermore, you can also specify protocol conformance constraints for your associated type such that T.PhoneNumber
conforms to the CustomDebugStringConvertible
protocol. Your phone number can then be printed with dashes regardless if it’s a string or integer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
// MARK: - Generics at the function level import Foundation extension Int: CustomDebugStringConvertible { public var debugDescription: String { var phone = String(self) phone.insert("-", atIndex: phone.startIndex.advancedBy(3)) phone.insert("-", atIndex: phone.startIndex.advancedBy(7)) return phone } } protocol Callable { associatedtype PhoneNumber var phone: PhoneNumber { get set } } struct Professor: Callable { var facultyID: String var name: String var phone: String var department: String } struct Student: Callable { var studentID: String var name: String var phone: Int var major: String } func call<T: Callable where T.PhoneNumber: CustomDebugStringConvertible>(callee: T) { debugPrint(callee.phone) } // Error: Protocol ‘Callable’ can only be used as a generic constraint because it has Self or associated type requirements func call(callee: Callable) { print("Phoning \(callee.phone)") } let steve = Professor(facultyID: "123", name: "Steve", phone: "123-456-7890", department: "English") let tim = Student(studentID: "456", name: "Tim", phone: 9876543210, major: "Math") call(steve) call(tim) |
The non-generic version of the call()
method doesn’t compile because the Callable
protocol uses the associated type PhoneNumber
. As a result, you’ll need to implement the call()
method using generics.
Swift generics can be very powerful. But like Doug said, it shouldn’t be used unnecessarily.
References
Here are some references to polymorphism in different programming languages if you want to read more.
- C pointer: http://www.gabrielgonzalezgarcia.com/papers/polymorph.html
- C++ inheritance: http://www.tutorialspoint.com/cplusplus/cpp\_polymorphism.htm
- Java interface: http://www.artima.com/objectsandjava/webuscript/PolymorphismInterfaces1.html
- Objective-C protocol: https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithProtocols/WorkingwithProtocols.html
- Swift protocol: http://sketchytech.blogspot.com/2014/09/polymorphism-in-swift-xcode-601.html
- Swift generics: http://apple-swift.readthedocs.io/en/latest/Generics.html
What’s the advantage of using Generics in the method func call(callee: T)?
This can be written as:
func call(callee: Callable)
….which seems more simple, and achieves the same thing?
Hi Doug,
I updated the post to answer your question.
Thanks! It took this and played with it a bit. It finally sunk in. Even in this very simple example, if you defined the Callable protocol with just phone: String, or phone: Int, one of the two structs does not conform. With generics, 2 structs with different implementations of the same parameter can conform (easily) to the same protocol.
Thanks for taking the time to explain further! Great example!
Thank you, it was clear 🙂