(Swift) Understanding Protocol-Oriented Programming
At WWDC in 2015, the Apple team introduced the powerful concept called Protocol-Oriented Programming(POP). In this article, I try to summarize the concepts from the video of WWDC. If you haven’t watched the presentation, I strongly recommend that you watch the video at least once.
What is Protocol?
The official documentation from Apple defines a protocol as below.
A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of those requirements. Any type that satisfies the requirements of a protocol is said to conform to that protocol.
In other words, a protocol has some common features among some classes. Classes, structs, or enums conforming to a protocol will have the same kind of functionality. A protocol looks like this.
protocol protocolName {
var property1: String { get set }
var property2: Int { get }
func protocolMethod() -> String
}
Protocol Properties
If you want to declare properties inside of a protocol, you need to stick with its special form of declaration. It looks like this.
var propertyName: type { get set }
There are few differences compared with the typical properties.
get
andset
keyword
These keywords are used to specify how you want to treat a property. If you put bothget
andset
inside the property’s curly braces, the property will be readable and writable . Additionally, it cannot be a constant in the conforming type declarations since it can be readable and writable.
If you put onlyget
keyword, on the other hand, it will be a read-only property. In that case, you can declare it as a constant usinglet
in the conforming types.let
is not allowed in the protocol declaration
You already know there is a special form of variable declaration in protocol properties, usingget
andset
. If you don’t want a property to be changed, you can just putget
in the protocol declaration. In that way, you can declare the property as a constant in the conforming types.
What is POP?
Protocol-Oriented Programming(POP) is a new approach to programming which addresses the problems related to Object-Oriented Programming. I will explain three major problems of OOP.
Three major problems of OOP
1) Implicit sharing
In OOP, you have to keep track of the instances so that they don’t affect each other. When you create instances of a class, instances have the reference to exactly the same object.
Trying to protect the object from unexpected changes by the instances, you will be defensive about coping with the object. You may use let
or static
to make properties or methods. The more you get defensive, the more the object loses its flexibility. The more the object loses its flexibility, the more complicated your code gets.
2) Inheritance all up in your business
The inheritance structure itself is not so flexible because of the following reasons.
- Only one superclass allowed
- Single inheritance weight gain
There might be too many instances which share the reference for the object. - No retroactive modelling
When you make a class, you have to choose a superclass right away. It’s not later in some extensions. - Superclass may have stored properties
If the superclass has stored properties, subclasses must just accept them. That kind of a situation leads to the initialization burden. - Don’t break superclass invariants
Know how to interact with its superclass without breaking its invariants. - Know what/how to override (and when not to)
Without using final, the methods may get overridden.
3) Lost type relationships
Classes don’t fit for the situations where type relationships matter. For instance, symmetric operations such as comparison. Let’s take a look at the example from WWDC.
class Ordered {
func precedes(other: Ordered) -> Bool {
fatalError("implement me!")
}
}class Label: Ordered { var text: String = "" ... }class Number: Ordered {
var value: Double = 0
override func precedes(other: Ordered) -> Bool {
return value < (other as! Number).value
}
}
To put it simply, the code above is trying to compare two double numbers using inheritance. However, this way has a couple of problems.
- Superclass must have something inside of the method body.
Even though you just want to have a blueprint of the function, the method body cannot be empty. It just doesn’t make sense that you have to put something although you are not actually using it. - Down-casting is needed in each subclass.
As you can see in the example, the subclass has to down-cast in order to comparevalue
and its argument. It knowsvalue
is Double type, however,other
argument is anOrdered
type.
TheOrdered
doesn’t even havevalue
property, therefore, theother
argument is needed to be down-casted.
Solving the problems with Protocols
Protocol-Oriented Programming comes in handy in a situation like this. The code below is the Protocol-Oriented version of the previous code.
protocol Ordered {
func precedes(other: Self) -> Bool
}struct Number : Ordered {
var value: Double = 0
func precedes(other: Number) -> Bool {
return self.value < other.value
}
}
There are several merits in adopting Protocol-Oriented approach in this case.
- The body of the method in the superclass has been removed.
Since protocols don’t need a method body, it has simply been removed. No more waste of lines there. - No need for down-casting
You might have noticed theSelf
keyword. If you seeSelf
in a protocol it’s just a placeholder for the type that is going to conform to that protocol. This is called Self-Requirement.
SinceSelf
is going to be the type ofNumber
in this example, there’s no need to down-castother
argument.
Protocol Extensions
At some points, you may want to have a default value for the properties or the methods inside of a protocol. Swift’s extension can make it possible.
protocol Competition {
var prize: String { get set }
func announcePrize(to person: Person)
}extension Competition {
func announcePrize(to person: Person) {
print("\(person.name) gets... \(self.prize)! Congrats!")
}
}struct CodingChallenge: Competition {
var prize: String
}struct Person {
var name: String
}let codingEvent = CodingChallenge(prize: "A trip to San Fransisco")
let challenger = Person(name: "Kenta")// Will print "Kenta gets... Trip to San Fransisco! Congrats!"
codingEvent.announcePrize(to: challenger)
Imagine you are imitating competitions in code. All events conforming to Competition
protocol will have the prize
property and announcePrize(to person:)
method. It’s kind of a troublesome task to implement the announcePrize(to person:)
every time you make some types which are conforming to Competition
. It may be good to have some default announcement style through events.
In the example above, the default announcement style is implemented in the extension. Since Competition
has a default implementation of the method, you don’t even need to write the method in the conforming type (in this case CodingChallenge
struct). This is how you can implement default values. It seems pretty nice, doesn’t it?
Adding Constraints to Protocol Extensions
Lastly, there is one more feature regarding protocol extensions that I would like to introduce, the constraints. Here is the definition of the constraints from the Swift language website.
When you define a protocol extension, you can specify constraints that conforming types must satisfy before the methods and properties of the extension are available. You write these constraints after the name of the protocol you’re extending by writing a generic
where
clause. For more about genericwhere
clauses, see Generic Where Clauses.
If you go to check the link(the Swift language website), there is an example using Collection
type. Let’s see the example in action.
// An example from the documentation
extension Collection where Element: Equatable {
func allEqual() -> Bool {
for element in self {
if element != self.first {
return false
}
}
return true
}
}struct Person: Equatable {
var name: Stringstatic func ==(lhsPerson: Person, rhsPerson: Person) -> Bool {
return lhsPerson.name == rhsPerson.name
}
}let person1 = Person(name: "John")
let person2 = Person(name: "Ringo")
let person3 = Person(name: "George")
let person4 = Person(name: "Paul")
let person5 = Person(name: "John")var personCollection = [person1, person2, person3, person4, person5]// Will be false
personCollection.allEqual()
First of all, Collection
is a protocol which defines behaviours of sequences such as Array, Dictionary, etc.
Notice the code where Element: Equatable
right after extension Collection
at the top. Because of this code, types which conform to Collection
must conform to Equatable
too. The function allEqual()
compares each element in the given collection and returns true if all elements are equal.
Right below the extension. There is a simple Person
class which conforms to Equatable
be implementing func ==(lhsPerson:, rhsPerson:)
method of Equatable
. This method compares given Person
‘s name.
If you test the example in your playground, personCollection.allEqual()
will be false because person1
and person5
have the same name.
That’s everything for this article! When I first saw the video, I was touched by knowing how beautifully designed it is. I hope you will feel the same way.
References
WWDC 2015: Protocol-Oriented Programming in Swift
https://developer.apple.com/videos/play/wwdc2015/408/
Introducing Protocol-Oriented Programming in Swift 3
https://www.raywenderlich.com/814-introducing-protocol-oriented-programming-in-swift-3
The Swift Programming Language Swift 4.2
— Declaration #Protocol Property Declaration
https://docs.swift.org/swift-book/ReferenceManual/Declarations.html#ID370
The Swift Programming Language Swift 4.2 — Protocols
https://docs.swift.org/swift-book/LanguageGuide/Protocols.html#ID269
Apple Developer Documentation — Collection
https://developer.apple.com/documentation/swift/collection
Apple Developer Documentation — Equatable
https://developer.apple.com/documentation/swift/equatable