This article shows how to use Swift protocols to retroactively introduce abstractions not only to enable new use cases for existing concrete types but also to refactor the concrete types themselves. We work through a running example to show how protocol extensions, protocol composition and protocol inheritance can be be used in tandem to create traits that can add new capabilities to concrete types. We also look at how concrete types remain free to override certain capabilities provided by protocol extensions, driven by the needs of the application. We finish by digging a bit into static and dynamic dispatch to point out how to avoid subtle bugs that can be introduced when overriding functionality provided by protocol extensions.
Contents
Protocol conformance vs. inheritance
Starting with concrete types
Beginning retroactive modeling
Protocol extensions
Constrained protocol extensions
Protocol composition
Protocol inheritance
Protocols as traits
Overriding default implementations
Dynamic and static dispatch for protocols
Conclusion
What is a protocol
A protocol is a set of requirements, which may include methods, initializers, subscripts and properties, that conforming types must implement. It establishes a contract and assures users of the protocol that conforming types will meet the requirements of the contract. This means that wherever a protocol type is required, a concrete type conforming to the protocol can be used.
Protocol conformance vs. inheritance
Coming from an object-oriented background to Swift, it is easy to see protocols as just a means for mimicking inheritance for value types. It is important to understand, however, how protocol conformance in Swift differs from inheritance to start to unlock the potential of the protocol-oriented paradigm.
Inheritance is often thought of as an ‘is a’ relationship between entities or concepts in the domain. Horse inherits from Animal because a horse is an animal. Inheritance represents a structural relationship between classes that is established when a class is defined. In Swift, a class can inherit only from one other class and, once it does, it gets inextricably bound to that inheritance hierarchy.
A protocol, on the other hand, can be thought of as a ‘can act as a’ relationship between concrete entities or concepts and an abstraction. A protocol can bring together otherwise disparate entities or concepts into a role or a persona by virtue of these entities or concepts being able to perform that role or adopt that persona. In Swift, protocol conformance can be used to give an entity as many roles or personas as required by the needs of the application. None of these roles needs to represent a structural relationship and none of them needs to be specified when the type representing the entity is defined.
Starting with concrete types
Swift protocols free up developers from having to build fixed structural hierarchies of relationships between entities and concepts in the domain. Instead, developers can think about the different roles an entity or a concept may play as and when required. Unlike an inheritance-based design, where key abstractions are usually identified early in the design process, developers working with Swift protocols can start designing with concrete types. As and when there is a need to think in an abstract way about a characteristic or a set of characteristics common to a group of entities or concepts, any existing concrete type can be extended to conform to a suitable protocol, giving the concrete type a new role or persona. If the concrete type already implements the requirements of the protocol to which conformance is declared through an extension, the extension body can be empty; otherwise, the requirements of the protocol must be implemented inside the body of the extension.
Let’s say we are designing an application to calculate fees to be paid by students studying in a college. With classic object-oriented thinking, we may start with an abstract class we may call Student, which would be the base class from which concrete classes representing various types of students would inherit. With Swift protocols, there is no need to start with an abstraction or a class. We start by modeling the various types of students using concrete types implemented as structs.
To keep the example simple, let’s say the college has two types of students – regular students who are enrolled in the college, and casual students who may take courses as and when they like. We would like each student instance to keep track of the courses the student is currently taking and use that to calculate fees payables by the student, using a fee per course credit. This applies to both regular and casual students. In addition, regular students pay a fixed enrollment fee at each payment cycle.
Before we start modeling student types, we define a type to model fees. Each fee has a category and an amount. The former is of type Category
, an enum defined within the Fee
type. We assume we have already defined a Money
type to represent monetary values.
struct Fee {
enum Category: CustomStringConvertible {
case enrollment
case course(String)
var description: String {
switch self {
case .enrollment:
return "Enrollment fee"
case .course(let title):
return "Course fee (\(title))"
}
}
}
var category: Category
var amount: Money
}
Note that before we have even defined a single protocol of our own, we have started using protocols from the standard library. This is because the standard library makes heavy use of protocols and a lot of capabilities are provided via protocols, which are used by types within the standard library and are also available to application developers. The protocol we are using here is CustomStringConvertible
, which allows any type to provide a customized textual representation. The only requirement is for conforming types to define a String
property called description
. In the Category
enum, we have given the .course
case a String
associated value, which will be used to store the title of the course. We use the description
property required by CustomStringConvertible
to provide more readable textual descriptions of both cases.
The next preparatory step is to create a type to represent an invoice, which each student instance should be able to generate. Given below is a simple Invoice
type, with an array of line items and an auto-calculated total payable amount.
struct Invoice {
typealias LineItem = (description: String, amount: Money)
var totalPayable: Money
var lineItems: [LineItem] {
didSet {
totalPayable = lineItems.reduce(0, { $0 + $1.amount })
}
}
init(lineItems: [LineItem]) {
self.lineItems = lineItems
totalPayable = lineItems.reduce(0, { $0 + $1.amount })
}
}
Having done this pre-work, we start getting into the domain by creating a struct to model courses, each of which has a title and a number of credits.
struct Course: Hashable {
var title: String
var credits: Decimal
}
Note that the Course
struct conforms to Hashable
, another protocol in the standard library. We need this because, as we will shortly see, we will use a set to store courses currently taken by a student to ensure that the same course cannot be added more than once. The Swift Set
type requires that elements conform to Hashable
. Sets need a way to efficiently test for membership and Set
does this using hash values. The Hashable
protocol ensures that all conforming types provide a way to produce an integer hash value. Swift makes it easy to add Hashable
conformance by automatically synthesizing it for user-defined types as long as Hashable
conformance is declared when the type is defined and all stored properties are of types that conform to Hashable
. Both the String
and Decimal
types conform to Hashable
, so all we have to do is declare Hashable
conformance in the definition of the Course
type.
Next, as noted earlier, we use concrete types to model the student types. Here is a struct to represent regular students.
struct RegularStudent {
let enrollmentFeeAmount: Money = 500
let courseCreditFeeAmount: Money = 200
var courses = Set<Course>()
mutating func takeCourse(_ course: Course) {
courses.insert(course)
}
mutating func completeCourse(_ course: Course) {
courses.remove(course)
}
func generateInvoice() -> Invoice {
let fees = calculateFees()
let invoice = Invoice(lineItems: fees.map({ (description: "\($0.category)", amount: $0.amount) }))
return invoice
}
func calculateFees() -> [Fee] {
var fees = [Fee]()
fees.append(calculateEnrollmentFee())
fees.append(contentsOf: calculateCourseFees())
return fees
}
private func calculateEnrollmentFee() -> Fee {
Fee(category: .enrollment, amount: enrollmentFeeAmount)
}
private func calculateCourseFees() -> [Fee] {
courses.map({ Fee(category: .course($0.title), amount: $0.credits * courseCreditFeeAmount) })
}
}
As noted earlier, we use a set to store the courses currently taken by a student. We also have stored properties for the amount of fees a student has to pay for enrollment and for each course credit. There are methods to take and complete courses and a method to generate an invoice for fees payable, which calls other methods, all defined within the same type. There is a lot going on in this type and we can already see opportunities for abstraction. But we suspend judgement for the time being and define another concrete type to model casual students. This is largely the same as the RegularStudent
type. The differences are the absence of the calculateEnrollmentFee()
method and a change in calculateFees()
to only include course fees. We also don’t need a stored property for the enrollment fee amount.
struct CasualStudent {
let courseCreditFeeAmount: Money = 200
var courses = Set<Course>()
mutating func takeCourse(_ course: Course) {
courses.insert(course)
}
mutating func completeCourse(_ course: Course) {
courses.remove(course)
}
func generateInvoice() -> Invoice {
let fees = calculateFees()
let invoice = Invoice(lineItems: fees.map({ (description: "\($0.category)", amount: $0.amount) }))
return invoice
}
func calculateFees() -> [Fee] {
calculateCourseFees()
}
private func calculateCourseFees() -> [Fee] {
courses.map({ Fee(category: .course($0.title), amount: $0.credits * courseCreditFeeAmount) })
}
}
Now that we have a couple of concrete types, we can begin to think not only about ways in which we may need to use these types but also how we refactor their implementations. This is where we start to introduce appropriate abstractions through retroactive modeling.
Beginning retroactive modeling
Let’s say that the only use case we currently have for the student types is to be able to generate invoices for a group of students regardless of whether they are regular or casual. For this, we need an abstraction to bind the student types together so invoices can be generated in a single operation. We will do this with a protocol but, rather than using a protocol to create an abstraction for what these domain entities are, we create an abstraction for what these domain entities can do in the given context, i.e., they can be invoiced.
protocol Invoicable {
func generateInvoice() -> Invoice
}
Since both our concrete types already have the generateInvoice()
method, we simply extend the concrete types to conform to the Invoicable
protocol as follows.
extension RegularStudent: Invoicable {}
extension CasualStudent: Invoicable {}
To test the student types, we write a simple function to print invoices, as shown below.
func printInvoice(_ invoice: Invoice) {
guard invoice.totalPayable != 0 else {
print("No fee payable")
return
}
invoice.lineItems.forEach({ print("\($0.description): \($0.amount)") })
print("Total payable: \(invoice.totalPayable)")
}
Now we can test the student types. We create two student instances, one regular and one casual, make them take some courses, and use them to generate and print invoices in a single batch.
let swift101 = Course(title: "Swift 101", credits: 3)
let dataStruct102 = Course(title: "Data structures 102", credits: 4)
let swift102 = Course(title: "Swift 102", credits: 4)
var regularStudent = RegularStudent()
regularStudent.takeCourse(swift101)
regularStudent.takeCourse(dataStruct102)
var casualStudent = CasualStudent()
casualStudent.takeCourse(swift101)
var invoicables: [Invoicable] = [regularStudent, casualStudent]
invoicables.forEach({ printInvoice($0.generateInvoice()) })
// Enrollment fee: 500
// Course fee (Swift 101): 600
// Course fee (Data structures 102): 800
// Total payable: 1900
// Course fee (Swift 101): 600
// Total payable: 600
As expected, the regular student has to pay enrollment fee in addition to fees for courses while the casual student only pays course fees. Next, we see what happens when the casual student completes the one course she has taken.
casualStudent.completeCourse(swift101)
invoicables = [regularStudent, casualStudent]
invoicables.forEach({ printInvoice($0.generateInvoice()) })
// Enrollment fee: 500
// Course fee (Swift 101): 600
// Course fee (Data structures 102): 800
// Total payable: 1900
// No fee payable
Having established that we can use our concrete types as intended, we turn our attention to refactoring the concrete types themselves. This is where protocol extensions come into play.
Protocol extensions
Protocol extensions provide implementations to conforming types for methods, initializers, subscripts, and computed properties. The most common use of protocol extensions is to implement some or all of the requirements of a protocol to provide a default implementation to conforming types. Conforming types still have the option of providing their own implementation to override the default implementation provided by a protocol extension. This makes protocols really powerful in that we can use them not only to specify requirements that conforming types must implement but also extend them to actually implement these requirements as appropriate.
Protocol extensions can also be used to make available to conforming types functionality not required by the protocol. This enables protocol extensions to contain methods or computed properties that may be needed to support implementations of the methods or properties required by the protocol. This keeps all related code in one place, avoiding code duplication in conforming types without the need to create helper types or global functions as a means of factoring out common code.
Continuing our running example, we start to refactor the concrete types we have used to model regular and casual students. The first common trait we identify is the ability of students to take courses, which applies to regular and casual students alike. To represent this as an abstraction, we define the following protocol.
protocol CourseTaking {
var courses: Set<Course> { get set }
mutating func takeCourse(_ course: Course)
mutating func completeCourse(_ course: Course)
}
Note that the protocol is not concerned with what kinds of entities conform to it, as long as they meet its requirements, i.e., they provide a stored property for the courses taken , to be implemented as a set, and methods to take and complete courses, with the given signatures. With the protocol in place, we can factor the common implementation out of the concrete conforming types, by extending the protocol as follows.
extension CourseTaking {
mutating func takeCourse(_ course: Course) {
courses.insert(course)
}
mutating func completeCourse(_ course: Course) {
courses.remove(course)
}
}
We can now remove these method implementations from the conforming types. Here is the revised RegularStudent
type.
struct RegularStudent {
let enrollmentFeeAmount: Money = 500
let courseCreditFeeAmount: Money = 200
var courses = Set<Course>()
func generateInvoice() -> Invoice {
let fees = calculateFees()
let invoice = Invoice(lineItems: fees.map({ (description: "\($0.category)", amount: $0.amount) }))
return invoice
}
func calculateFees() -> [Fee] {
var fees = [Fee]()
fees.append(calculateEnrollmentFee())
fees.append(contentsOf: calculateCourseFees())
return fees
}
private func calculateEnrollmentFee() -> Fee {
Fee(category: .enrollment, amount: enrollmentFeeAmount)
}
private func calculateCourseFees() -> [Fee] {
courses.map({ Fee(category: .course($0.title), amount: $0.credits * courseCreditFeeAmount) })
}
}
The same goes for the CasualStudent
type.
struct CasualStudent {
let courseCreditFeeAmount: Money = 200
var courses = Set<Course>()
func generateInvoice() -> Invoice {
let fees = calculateFees()
let invoice = Invoice(lineItems: fees.map({ (description: "\($0.category)", amount: $0.amount) }))
return invoice
}
func calculateFees() -> [Fee] {
calculateCourseFees()
}
private func calculateCourseFees() -> [Fee] {
courses.map({ Fee(category: .course($0.title), amount: $0.credits * courseCreditFeeAmount) })
}
}
All we have to do for these types to opt into the functionality provided by CourseTaking
is to extend the RegularStudent
and CasualStudent
types to conform to the CourseTaking
protocol.
extension RegularStudent: CourseTaking {}
extension CasualStudent: CourseTaking {}
Note that the above extensions on RegularStudent
and CasualStudent
add new capabilities to these types without affecting the capabilities added by the extensions we defined earlier.
Constrained protocol extensions
A protocol extension can specify constraints that conforming types must satisfy for the extension to be applicable to the conforming types. This is done by adding a where
clause after the name of the protocol being extended. The most common use case is to require conforming types to also conform to another protocol but we can also make the constraint more specific such that the extension is available only to specific conforming types.
Going back to our running example, we continue to employ protocols to refactor the concrete types used to model students. Now we focus on the ability of students to calculate fees payable given the courses they have taken. To do this, we define the following protocol.
protocol FeePaying {
func calculateFees() -> [Fee]
}
Unlike with the CourseTaking
protocol, this time we have to deal with different behaviour depending on whether a student is regular or casual. Regular students are enrolled so they have to pay an enrollment fee in addition to course fees while casual students only pay course fees.
We start by dealing with course fees through an extension to FeePaying
, available only to conforming types that also conform to the CourseTaking
protocol.
extension FeePaying where Self: CourseTaking {
var courseCreditFeeAmount: Money { 200 }
func calculateFees() -> [Fee] {
calculateCourseFees()
}
private func calculateCourseFees() -> [Fee] {
courses.map({ Fee(category: .course($0.title), amount: $0.credits * courseCreditFeeAmount) })
}
}
This extension includes a private
method to calculate course fees in addition to an implementation of the calculateFees()
method required by the FeePaying
protocol. We also include a computed property to provide the fee per course credit. Although protocol extensions cannot include stored properties, they can include computed properties, which can be used to provide values that are constant, values that can be computed using other properties, and values that need to be read from another source. Here we have simply used a constant value for the fee per course credit. The advantage of using a computed property is that we can easily change the property to read this value from an external source, such as a central repository containing the fee tariff, without having to change any other code.
Note that in the calculateFees()
method in the above protocol extension, we are able to use the courses
property from the CourseTaking
protocol, although it is not defined in the FeePaying
protocol or the extension. This is the magic of constrained protocol extensions. Since this extension is available only to conforming types that also conform to CourseTaking
, the compiler can be certain that conforming types that use this extension will always have the courses
property, so it allows us to freely use this property.
This is all we need to give the CasualStudent
type the ability to calculate course fees. We just make CasualStudent
conform to the FeePaying
protocol.
extension CasualStudent: FeePaying {}
Since CasualStudent
already conforms to CourseTaking
, the constrained extension we have defined to FeePaying
becomes available to the CasualStudent
type.
We no longer need any functionality dealing with fee calculations in the CasualStudent
type, which can be rewritten as follows.
struct CasualStudent {
var courses = Set<Course>()
func generateInvoice() -> Invoice {
let fees = calculateFees()
let invoice = Invoice(lineItems: fees.map({ (description: "\($0.category)", amount: $0.amount) }))
return invoice
}
}
Next, we turn our attention to regular students, who are enrolled so have to pay an enrollment fee in addition to fees for any courses that they may be taking at a given time. We start by defining the following protocol.
protocol Enrolled {}
Note that this protocol has no requirements. This is because, in the model as described, there is no specific information or behaviour linked to being enrolled. Being enrolled just modifies the calculation of fees. This is not to say that we may not have to model behaviour in the future which may be directly linked to being enrolled. When we do, we can use this protocol, and any extensions thereon, to model such behaviour. For now, we will use this protocol only as a constraint. We do this by defining an extension to the FeePaying
protocol which would be available only to conforming types that also conform to the Enrolled
protocol.
extension FeePaying where Self: Enrolled {
var enrollmentFeeAmount: Money { 500 }
private func calculateEnrollmentFee() -> Fee {
Fee(category: .enrollment, amount: enrollmentFeeAmount)
}
}
The above extension just contains a private
method which returns the enrollment fee and a computed property to provide the fee amount. For the purposes of the functionality we are implementing at the moment, this is all we need in this extension.
Since regular students are enrolled and take courses, we need an extension to FeePaying
which will be available to conforming types that conform to both the CourseTaking
and the Enrolled
protocols. For this, we turn to protocol composition.
Protocol composition
Protocol composition combines multiple protocols into a single set of requirements. Although a protocol composition does not define a new protocol type, it has the same effect as defining a temporary protocol which combines the requirements of the protocols included in the composition.
Continuing our example, we use a composition of the Enrolled
and CourseTaking
protocols to define an extension to FeePaying
that will be available only to concrete types that also conform to both the protocols.
extension FeePaying where Self: Enrolled & CourseTaking {
func calculateFees() -> [Fee] {
var fees = [Fee]()
fees.append(calculateEnrollmentFee())
fees.append(contentsOf: calculateCourseFees())
return fees
}
}
It may seem a bit surprising at first why the above extension even compiles. How is it able to call the calculateEnrollmentFee()
and calculateCourseFees()
methods, which are defined as private
methods in extensions to the FeePaying
protocol. This is more constrained protocol extension magic. Because the compiler knows that the extension we have defined above will be available only to conforming types that also conform to both Enrolled
and CourseTaking
, the functionality we have defined in the extension to FeePaying
constrained to conforming types that also conform to Enrolled
as well as the functionality defined in the extension to FeePaying
constrained to conforming types that also conform to CourseTaking
becomes available in this extension. This is why the compiler does not complain when we call the calculateEnrollmentFee()
and calculateCourseFees()
methods in the implementation of the calculateFees()
method.
This is all we need to give regular students the ability to calculate fees applicable to them. We just make the RegularStudent
type conform to the Enrolled
and FeePaying
protocols, noting that it already conforms to the CourseTaking
protocol.
extension RegularStudent: Enrolled, FeePaying {}
With this, we also don’t need any fee calculation logic in the RegularStudent
type, which we rewrite as follows.
struct RegularStudent {
var courses = Set<Course>()
func generateInvoice() -> Invoice {
let fees = calculateFees()
let invoice = Invoice(lineItems: fees.map({ (description: "\($0.category)", amount: $0.amount) }))
return invoice
}
}
Protocol inheritance
Protocols can inherit from other protocols. Unlike classes in Swift, which can inherit only from a single class, a protocol can inherit from multiple protocols. A protocol that inherits from one or more protocols gets all the requirements of the protocols it inherits from and can add its own requirements as well. This enables protocols to add to or specialize the requirements of other protocols.
In our running example, we have the generateInvoice()
method, which is implemented identically in the RegularStudent
and CasualStudent
types. Earlier, we created the Invoicable
protocol with the generateInvoice()
method and made RegularStudent
and CasualStudent
conform to it so we could generate invoices for all students regardless of whether they are regular or casual. However, we did not extend Invoicable
to provide the implementation of generateInvoice()
to conforming classes. This is because this method calls calculateFees()
, which was earlier implemented in the concrete classes. Now that we have moved the calculateFees()
method to the FeePaying
protocol, we can make the calculateFees()
method part of the Invoicable
protocol simply by making Invoicable
inherit from FeePaying
. To do this, we rewrite the Invoicable
protocol as below.
protocol Invoicable: FeePaying {
func generateInvoice() -> Invoice
}
Now we can implement generateInvoice()
in an extension to Invoicable
, using the calculateFees()
method as if it were defined in the Invoicable
protocol.
extension Invoicable {
func generateInvoice() -> Invoice {
let fees = calculateFees()
let invoice = Invoice(lineItems: fees.map({ (description: "\($0.category)", amount: $0.amount) }))
return invoice
}
}
With this, we have factored out all functionality from the RegularStudent
and CasualStudent
concrete types. We rewrite these types below, simply providing storage for the courses
stored property, letting the protocols and extensions provide all the functionality required.
struct RegularStudent {
var courses = Set<Course>()
}
struct CasualStudent {
var courses = Set<Course>()
}
It must be noted that we have worked through a very simple example, where we could refactor all the functionality into protocol extensions, leaving the concrete types responsible only for providing storage for stored properties. Real-life projects are more complex and we should fully expect functionality to be distributed between concrete types and extensions to protocols to which the concrete types conform. The advantage of using protocols and protocol extensions to introduce abstractions and factor out common functionality is that abstractions can be added retroactively as and when required. We can change and even replace existing abstractions as domain requirements evolve or the scope of the model changes.
Protocols as traits
What we have done over the last few sections is to take functionality implemented in concrete classes and convert it into traits, which concrete classes can simply opt into, without having to provide implementations. This is made possible through protocol extensions, which let us add method implementation to conforming types, even imposing conditions on which implementation should be available to which conforming types. The conforming types still have to provide storage for any stored properties that these traits may use, but it makes the process of adding capabilities to new concrete types much easier and simpler.
In the example that we have worked through, we started with concrete types and retroactively modeled the traits. Retroactive modeling saves us from having to decide on key abstractions early in the design process. Once we have fleshed out the basic design and we can begin to think about some of the design elements in terms of abstractions, we can also start directly designing traits and adding them to concrete types. Whether we tease out a trait retroactively from concrete types or design it directly and simply inject it into concrete types depends on how well we understand a particular aspect of the model and how far along we are in the development process. In any moderately complex domain, we would likely end up using a combination of both approaches.
To demonstrate designing a trait directly, we go back to our running example and assume that the college for which we are developing the system has a gym and a library, both of which are accessible only to regular students. First, we use a protocol to define the trait.
protocol FacilityUsing {
func useGym()
func useLibrary()
}
Next, we use a protocol extension to implement the trait.
extension FacilityUsing {
func useGym() {
print("Using the gym")
}
func useLibrary() {
print("Using the library")
}
}
All we need now is to apply this trait to the RegularStudent
type.
extension RegularStudent: FacilityUsing {}
We can see how a regular student can enjoy access to the gym and the library.
regularStudent.useGym()
regularStudent.useLibrary()
// Using the gym
// Using the library
The methods useGym()
and useLibrary()
are not available to instances of CasualStudent
and we will get a compiler error if we try to use these methods with instances of CasualStudent
.
Having traits makes it easier to apply existing capabilities to new concrete types that we may need as we develop the application. Let’s say we also need to model summer students who join for a limited period for a fixed fee. They can take courses and use facilities but, given that the fixed fee is charged up front, there is no need to calculate fees or generate invoices. We can use the traits we have already defined to model a summer student as below.
struct SummerStudent: CourseTaking, FacilityUsing {
var courses = Set<Course>()
}
Overriding default implementations
While protocol extensions allow us to add default implementations to conforming types, conforming types remain free to override any of these with their own implementations. To see this in action, we continue our running example, where we last created a concrete type to model summer students. Let’s say we want to impose a condition that summer students cannot take more than two courses at a given time. To do this, we extend the SummerStudent
type as below.
extension SummerStudent {
var maxCourseCount: Int { 2 }
mutating func takeCourse(_ course: Course) {
guard courses.count < maxCourseCount else { return }
courses.insert(course)
}
}
We have overridden the default implementation of takeCourse(_:)
from the CourseTaking
protocol to impose this check. We test this out by creating an instance of SummerStudent
and seeing if it can take more than two courses.
var summerStudent = SummerStudent()
summerStudent.takeCourse(swift101)
print(summerStudent.courses.count) // 1
summerStudent.takeCourse(dataStruct102)
print(summerStudent.courses.count) // 2
summerStudent.takeCourse(swift102)
print(summerStudent.courses.count) // 2
We can see that when we try to get a summer student to take a third course, the course count remains at two.
Dynamic and static dispatch for protocols
Dynamic dispatch is the process of calling a method where the implementation of the method to be called is decided at run-time. With static dispatch, on the other hand, the implementation of a method to be called is decided at compile-time.
Swift uses dynamic dispatch for methods required by protocols. If a method required by a protocol is only implemented in an extension to the protocol, that implementation will be called. However, if a method required by a protocol is implemented in a conforming type, the implementation in the conforming type will be called regardless of whether an implementation may be available in a protocol extension. For methods not required by a protocol but defined in an extension to the protocol, static dispatch is used. In such cases, which implementation is called depends on the type of variable used to call the method. If the variable is of the conforming type, the implementation in the conforming type is called. If, however, the variable is of the protocol type, the implementation in the protocol extension is called.
To see this in action, we create a protocol with one required method, an extension that implements this method and defines another method, and a conforming type that implements both the method required by the protocol and the method defined in the protocol extension.
protocol TestProtocol {
func protocolRequirement()
}
extension TestProtocol {
func protocolRequirement() {
print("Implementation in protocol extension")
}
func protocolExtensionMethod() {
print("Implementation in protocol extension")
}
}
struct TestStruct: TestProtocol {
func protocolRequirement() {
print("Implementation in conforming type")
}
func protocolExtensionMethod() {
print("Implementation in conforming type")
}
}
We create a variable of type TestProtocol
and assign an instance of TestStruct
to it. We create another variable to which we assign an instance of TestStruct
. This variable would be inferred by the compiler to be of type TestStruct
. We then use both variables to call the method required by the protocol.
let protocolType: TestProtocol = TestStruct()
let conformingType = TestStruct()
protocolType.protocolRequirement()
conformingType.protocolRequirement()
// Implementation in conforming type
// Implementation in conforming type
In both cases above, the implementation in the conforming type is called. This is because of dynamic dispatch, where the Implementation to be called is decided at run-time. Normally, this is the behaviour we would expect since the implementation in the conforming type would be intended to override the implementation in the protocol extension.
Next, we use the same variables to call the method not required by the protocol but defined in the protocol extension.
protocolType.protocolExtensionMethod()
conformingType.protocolExtensionMethod()
// Implementation in protocol extension
// Implementation in conforming type
This time, the first call results in the implementation in the protocol extension being called. This is because the implementation to be called is selected based on the type of the variable. This is something to watch out for as it can cause subtle bugs in code where we may be expecting the override in the conforming type to be called but, if the variable we use to call the method is of the protocol type, calling a method defined in the protocol extension and not required by the protocol will result in the implementation in the protocol extension being called even though the same method may have been implemented in the conforming type. As a general guideline, methods that are meant to serve as customization points for conforming types should be defined in the protocol.
Conclusion
Protocols in Swift open up a whole new way of designing applications. Rather than having to lock ourselves into inflexible inheritance hierarchies and being forced to settle on key abstractions early in the design process, Swift protocols and protocol extensions enable retroactive modeling. We can start with concrete code and work our way to abstractions when we need to assign a role or a persona to entities or concepts in the domain. Even when an abstraction is introduced, we remain free to evolve it or even replace it as and when required by the needs of the application. The protocol-oriented paradigm makes it easy to create traits, which can be applied to concrete types to let them opt into new functionality in a simple and streamlined manner.
Thank you for reading! I always appreciate constructive comments and feedback. Please feel free to leave a comment in the comments section of this post or start a discussion on Twitter.
Adnan says
Always impressed by your writing and detailed explanation, learn more then I intended and looking forward to more reading.
I would definitely purchase a book if you were to write one.
Khawer Khaliq says
Hi Adnan,
Many thanks for your kind thoughts. It is great to see that you find my articles useful and informative.
I do intend to consider writing a book at some point in the future, and you words of encouragement mean a lot.
Cheers,
Khawer