0:09 Today we'll talk about protocols and class hierachies. There was a
session at WWDC about
protocol-oriented programming, and in that session, they showed how to replace a
class hierarchy with protocols. The reason for doing this is that class
hierarchies sometimes are very inflexible. One problem that most iOS developers
have seen is the GodViewController. All other view controllers are supposed to
inherit from GodViewController. That's very limiting, because it doesn't allow
you to inherit from UITableViewController anymore (or any other view
controller), because Swift has single inheritance.
0:55 With protocols, you can define shared functionality in a more
flexible way than in a common super class. The protocol based approach is not
without limitations either though, and we'll look at them a bit later.
A Class Hierarchy
1:23 In the example code, we start out with a class hierarchy. We have a
Shape class, which is an abstract superclass of sorts. Since Swift doesn't
have abstract classes, we just provide implementations that call fatalError in
the draw method and in the boundingBox property. Then we have a shared
image method, which can be used by all of Shape's subclasses. That's the
power of inheritance: you can reuse things you've written in your superclass.
2:03 We have two subclasses of Shape. Rectangle inherits from
Shape and has additional properties such as origin and size. It also
overrides the boundingBox and draw method with specific implementations:
2:38 To refactor this into a protocol-oriented approach, we'll change
class Shape to protocol Shape, and remove the default implementations for
draw and boundingBox. We'll mark the boundingBox property as read-only.
The shared image method is moved to a protocol extension:
3:50 For Circle, we have to make the same changes. This is all we need
to do to create a protocol-oriented version of our code.
4:04 We can take this a bit further, and replace all our classes with
structs. We simply change the class keywords to struct and we're done. Even
though it's a tiny change at the source level, it has big implications for the
code. All of our rectangles and circles are values now, not references. It's not
better or worse than having them as reference types, just very different.
Defining Shape as a protocol gives us the option to make that change, whereas
the inheritance approach requires us to use classes.
Adding Shared Functionality
5:10 Let's extend Shape by adding a method to rotate shapes. As a
first attempt, we could add a mutating method to the protocol:
5:33 However, now every type needs to implement the rotate method. With
two types, we will have duplication, but it'll only get worse when we add more
types. It would be nicer to provide a way to implement it once for all Shapes.
6:10 Instead of defining a mutating rotate in the protocol, we define
an immutable variant in a protocol extension:
6:16 For the angle, we currently use a CGFloat. It would be much
more precise to use e.g. the Measurement API, because now it's unclear what
the unit of the angle is: radians, degrees, or something else. However, we won't
fix that today.
6:39 The way we'll implement rotated is by returning a new
TransformedShape value. So we'll start by creating TransformedShape, stores
the original shape and a CGAffineTransform value:
7:29 The draw method is a bit more complicated. The approach we'll
take is to rotate the context, and then call the original draw method.
However, because the context is a mutable value, we have to make sure to restore
it to the original state after we're done, so that we don't influence other
9:43 There are some tricky things to be aware of when working with
protocols and protocol extensions. We're going to use a bit of a constructed
example to demonstrate those pitfalls, but you'll encounter them sooner or
Let's say we want to override the rotated method for Circle, and simply
return the Circle directly:
10:26 Now, if we call circle.rotated(by:), we'll see that the
overridden rotated method in the Circle struct gets called. However, with
protocol-oriented APIs you'll often store conforming entities as the protocol
type. In our example this means storing a circle as Shape, not as Circle. If
you do this, the custom rotated method won't get called anymore.
11:13 The reason is that methods defined in protocol extensions are
statically dispatched. To have dynamic dispatch, we have to add our rotated
method to the protocol itself:
12:04 Now our overridden rotated method gets called again. This also
explains why the protocols in the standard library have gotten so large. Even
though there are default implementations for most methods in e.g. the
Collection protocol, they're added to the protocol to allow for dynamic
dispatch. The behavior around static and dynamic dispatch can be unintuitive at
first, so it's important to be aware of the two different options.
13:02 The protocol-oriented solution we came up with isn't necessarily
better or worse. Both protocols and class hierarchies are tools we can use. They
both have different tradeoffs. For example, class hierarchies allow you to
inherit stored properties, and you can call super when overriding something.
In a protocol-oriented approach, this is not possible.
A limitation of class hierarchies is that you can only use single inheritance.
If you need shared functionality, you might run into the issue that you want to
inherit from multiple classes, and that problem doesn't happen with protocols.
With protocols, you can also add conformance later on, whereas with a class
hierarchy, you can't replace a superclass unless you own that class. Depending
on the problem you're solving, you can explore both solutions.
14:33 A related issue is that you often have to decide whether you need
a protocol at all. For example, in the networking
episode we have a Resource struct, not a
Resource protocol. This is because each resource has the same structure: they
all have a URL, a HTTP method and a parse function. In this case it doesn't make
sense to define a protocol.
For the example we worked on today, the different types have different
properties. For example, a Rectangle has on origin and size, whereas a
Circle has a center and radius. Because they have a different structure,
protocols are a much better fit.