Pushing the limits of Protocol-oriented programming
Jesse Squires at Swift Summit in San Francisco, 2016
Jesse: Today we’re going to talk about protocols and pushing the limits of protocol oriented programming. Hopefully you saw Nate Cook’s talk yesterday about protocols and how they work in the standard library with the different collection types. We’re going to be expanding on some of these ideas about how programming against a protocol instead of a specific type can open up your code to be more flexible and easier to change over time.
First, we’re going to talk about … I set this up with the Swift way to do things. Every language has its own personality. Objective-C this is very distinct and Swift. It’s still developing but it still has this very distinct personality. We’re going to see this when we look at the standard library and its protocols and value types all the way down. That’s really what writing swifty code means. When we say this; writing code that’s swifty, it’s really writing code that’s protocol oriented.
What do we really mean by protocol oriented programming? This is a new kind of catchphrase that we’ve been saying for the past couple of years with Swift. It’s actually not a new concept. This is an older idea that comes from the solid design principle. Who knows what these are? Yeah a few hands. These are basically a set of guidelines to make your code clean, reusable. It’s about designing good software modules. If you follow these guidelines. you can reduce debt.
Some of what Ben Sandofsky talked about yesterday about being able to refactor our code. If we follow these principles, we can reduce a lot of that potential debt. Today, we’re going to talk about the ‘I’ in solid which is interface segregation and that’s really what protocol oriented means. This protocol oriented programming embodies this idea of interface segregation and to program in a protocol oriented way means to basically implement this principle in your code.
What do we really mean by interface segregation? Basically what we mean is that no client should depend on or really even know about methods that it doesn’t use. We can create these small separated interfaces which give us some more focused API. If you think about like UITableViewDelegate in UIKit in iOS, there are a lot of methods there. It’d probably be better if we split that big protocol up into smaller protocols like one for cell selection, one for editing the table view and so on.
Protocols also restrict access. Instead of passing in a concrete type to a function or a class like what Nate talked about yesterday. Instead of passing this array into a function, we could just pass a sequence protocol into this function instead because all we need are the properties and members on a sequence. We don’t need everything that you get with an array.
We can also unify disjoint types. Types that don’t share a common ancestor. They don’t have a superclass in common. This is particularly important with the Swift standard library because most types are value types. They’re structs or enums. They can’t have a superclass. The only way to have a collection of say structs and then put those into an array, they’re all these different types and to unify them, to talk about each one in the same way we can use a protocol to do that.
Then finally, protocols have this benefit of hiding that concrete type. When you pass a protocol into a class or into a function, that class or function doesn’t have to know all the specifics about the type you’re passing in, they just talk to this protocol and it doesn’t matter. And so, Greg Heo might say, “Okay this is cool, but why do we want to use this?” Because protocols they add a bit of complexity to our code, they’re more abstract to think about sometimes but protocols help us write better code in a lot of ways.
They give us this more modular design. They’re dynamic. What I mean by that is it can be any type that fulfils this protocol. Most of these things they’re resolved at runtime. We often talk about Objective-C as this dynamic language and Swift as a static language. Protocols are actually this very, very dynamic feature of Swift especially when we constrain them in extensions. They’re also very testable. It’s very easy to set up fake data or mocks with a protocol. You just conform to that protocol in your test and return some fake data.
What if everything were just a protocol? That’s almost how the standard library is built, in a lot of ways. Let’s find out how we can build something to really take advantage of these features. We’re going to do this little experiment building these protocol oriented data sources. In UIKit we have UITableViewDataSource. UICollectionViewDataSource. These two views really are fundamental to almost every app. Some apps are basically only table views and collection views.
We’re going to dive into this. I have a lot of code but it’s simplified. We’re omitting some details to really just focus on these ideas and keep the slides readable. Let’s just outline a couple of quick goals. Obviously we want this to be protocol based. Type safer generic when possible. You’ll see how that plays into this later. We want to unify table view and collection view. This is one of the most important parts here.
Right now tables and collections are like close enough to almost be able to reuse code between them but they’re just barely different enough where you really can’t and it just makes you angry. They’re not interchangeable. We want to remove some of the UIKit boilerplate associated with this. If possible we want to avoid NSObject and have a pure Swift component here. Just to ground this a little bit, we have static table views, collection views that display photos. We have this list or grid just in case you aren’t familiar with these components. Those are our main responsibilities; just display data in a list or a grid.
What do we need to do that? We just need a couple of things really. We need some structured data so some sections with some items. We need to be able to create and configure cells. Then we just need to conform to these protocols somehow. Let’s start with a section so we can define what a section is. We have this associatedtype. It’s just going to be the model that’s in the section. We just have an array of those models. Then an optional header and footer title.
To make this a little more concrete, we can just write a struct real quick that implements this protocol and it just has these properties. Next, we have the full DataSourceProtocol. This is similar to the collection view and table view data sources in UIKit. Again, in our data source we have a particular model in our data source. We have numberOfSections, numberOfItems, etc. Pretty straightforward.
We can easily create a concrete type that conforms to this. A data source, it just has an array of section types. This is not the concrete section struct that we just introduced. This is just talking to the protocol. Then we can implement this DataSourceProtocol in the struct. NumberOfSections just returns sections.count and so on. There we go. We have our structured data. It’s all based on protocols. We implemented a few structs to make it concrete but so far- just protocols.
Next, we need to create and configure the cells. What we want to do is have a common interface for these cells. We don’t want to talk about table views and collection views specifically. We just want one view to talk to. We don’t want specific cells for tables and specific cells for collections. We just want a unified way to create and configure these cells.
We can introduce this CellParentViewProtocol. This is going to represent our table or collection. It will have a CellType. This is an associatedtype, so that will be a table cell or collection cell. Then we just have this dequeue method which both table view and collection view both have this. We’ll spell it a little differently so that they can both conform. It will look like this for a collection view. We conform it to this protocol. We say a collection view creates collection view cells. Then we just wrap the usual collection view dequeue method in the protocol method. Then we do the same thing for a table view.
We can unify the cells. I introduce this ReusableViewProtocol. It knows that its ParentView is going to be a ParentViewProtocol. If this were a collection view cell, its parent view is going to be a collection view. Then we have our reuseIdentifier and prepareForReuse that the reusable views use.
This is all we have to do to make these cells conform to that. Our collection view cell, it just needs to specify that its parent view is a collection view. Then same with table view cell and table view. Note that these methods in the protocol are actually already implemented in UIKit. They’re in the table view cell and collection view cell super classes. We just get those for free.
Now we have a common interface for our cell parent views. We can talk to table views and collection views through the same interface as if they’re the same type. We can talk about the cells as if they are the same type. There we go. Now we need a way to actually create these and configure these. We can introduce another protocol. This is going to be this ReusableViewConfigProtocol. It’s going to have an associated item and a view. Basically a model that you have that you use to configure the cell, set the text label, set the image, whatever you have in the cell. Then we just need to specify the reuseIdentifier for that. Then we just need to configure it. We receive the view which would be one of the cell types. Receive our item. The parentView which will be the table or collection and then the indexPath. That’s all we need to configure that cell.
Then we can write an extension on this method and we can constrain it based on the type of cell that we are configuring. If we are configuring a table cell, then with only those minimum protocol methods we define, we can create the table cell. We can get the reuseIdentifier so this is the method on the protocol. We can tell the table view to dequeue that cell for that identifier. Remember, this method here it’s not the table view dequeue method. This is the one we defined in our protocol.
Then we can just tell it to configure. Call that method in the protocol and there we go. Now with defining the minimum amount of things which is just how to configure we can get this creation for free. Then we can define this exact same thing for collection views where if our view is a collection view cell, then we’ll have a collection cell for Item, collectionView, indexPath. We get all that for free.
Let’s make this a little bit more concrete. We can introduce this struct that conforms to this protocol. We have this view configuration. It’s going to have our model. It’s going to have the type of cell. Then it conforms to this. We can initialize this with a reuseIdentifier. We can give it a closure which is just a function that configures that cell. Then when we conform to this protocol, we can just return that reuseId and then return the result of this closure. There we go. Now we have our structured data and we have a way to configure our cells. Because of extensions, we get that creation part for free.
Next we need to conform to these data source protocol somehow so we can talk to UIKit. So we’re going to introduce this concept of a bridged data source. The first class we’re going to introduce here is going to inherit from NSObject and conform to these protocols. So far we’ve been using pure Swift classes. Just structs and protocols. No @ObjC, no NSObject protocol stuff. This is going to contain all of that and hide it away. This class for each of these data source methods, we’ll basically just give it a closure. We’ll set that closure property which it will use to return and implement for these data sources.
This is what that would look like. We won’t go through all of it just to keep this simple and readable. Let’s say this class has this numberOfSections property which is just a function that takes nothing and returns an Int. Then when we go and implement this data source protocol in UIKit, we have this numberOfSections in the collection view. We’ll just return self.numberOfSections which is whatever we set for this property.
We’ll do that for all of those different methods. The cell for item at index path will have another closure that maps to that so we can encapsulate this. We’ll look at it a little bit more in a couple of slides. That’s all of those responsibilities. We have structured data. We can create and configure cells. We have this other object that glues things together. That’s everything we need.
Just a quick recap. We can define sections based on a protocol. We can define the data source. We can define a way to talk about tables and collections, talk about their cells, a way to configure those cells and then what we just looked at this BridgedDataSource NSObject.
We need a final piece to connect these and so we can introduce this idea of this data source provider. This is going to take a data source and a configuration for the cells. We’re using generic parameters here because Swift requires this to express these things. Really all we want is just a thing that is a DataSourceProtocol, a thing that is a cell configuration protocol. Privately, this object will have this bridgedDataSource. In a couple of slides we’ll see how this gets generated.
Let’s quickly look at the results of what we get out of this. We can create our DataSource. This is that struct we created that conforms to this protocol. We just give it some sections of models. We create this config which is really just a closure. We wrap this closure in the struct. That’s basically all it is. You get your model and then you can configure your cell with that in here. You pass your data. You pass your cell config into this provider object. Then all you have to do is ask it for a collectionViewDataSource to hook up to your collection view or if you’re using a table view, you can just ask it for this tableViewDataSource.
How does that work? How did we get to here based on just defining that data and that cell configuration? This is how we can generate these specific data sources. We’re in this provider class that’s parameterized by this data source and this configuration. We have this private data source object that we want to create. This glues everything together. When we create this bridgedDataSource, it’s just going to use the data source and that cell config to generate an object that conforms to these protocols. We have this private function that does that. This is what it would look like.
We can create this data source and it’s kind of simplified- all this code will be on GitHub. This is simplified so we can read it a little more nicely. On this bridgedDataSource, we have this numberOfSections property which we looked at before. It is just a closure that returns how many sections there are and so we can tell the dataSource.numberOfSections. For the numberOfItemsInSection, we just forward that again to our data source object. For our table cell at the indexPath, we just ask the data source for that item. Then we tell the cell config to create and return that cell. Remember each of these are just protocol methods that we’re talking to now. This is the extension method on that configuration protocol. The collection view APIs would follow similarly to this.
Let’s look at those results one more time about what we get here. All we have to do is define this DataSource. We define this cell configuration. We inject that into this provider and then we can grab back out a collectionViewDataSource or a tableViewDataSource. We have this declarative way to say, “Here’s the definition of my data. Here’s the definition of how to configure a cell using this function.” Then we can encapsulate all of this boilerplate and just get back these data sources.
The compiler can enforce everything, every step of the way. That’s a lot. I know there’s a lot of moving pieces there, it can be dense and we’ve introduced a lot of complexity here. You may be thinking, “Table views and collection views were so simple and now we just added a ton of protocols and generics and everything is like just crazy. I have no idea what’s happening.” Remember this is just an experiment, this exercise to see how far we can push this and to know that protocols are much more powerful in Swift than Objective-C. We just can’t even express this type of thing in Objective-C.
I think the nice thing is that we can encapsulate all of that complexity. If we look back at this, we don’t have any angled brackets or anything specifying the parameters because the compiler can infer all of these types for us and then enforce everything and do all of this heavy lifting.
To summarize and go through some of these benefits, with protocol extensions what we actually get is this dynamic interface segregation. We’re talking about separating these interfaces so that when you pass a protocol into a function or into a class, you’re only providing the minimum amount of information that it needs. When we can write these extension that constrain the protocol based on the associated types, it’s like really embodying this idea of separating these interfaces. Only if this thing is configuring a table view cell will this function be accessible.
If you’re configuring a collection view cell, if that’s what you define, it’s impossible to create a table view cell because this constraint doesn’t hold. Same with the DataSourceProvider. If our configuration view is a table cell, then we only have access to this table data source. We can swap these things out so we can keep our same definition of our data declaratively define a new config for collection views, plug that in and then we get all this functionality just by defining these two things.
We can restrict access. Again, what I just mentioned with having this table data source property. What this is really returning is that kind of complex BridgedDataSource object with these closures that maps our data source and cell config to these methods but the clients don’t know that. They don’t know anything because all they get back is this semi opaque protocol. They don’t get the concrete type back.
We can unify all these disjoint types so we can talk about collections and tables and talk about their cells in the same way through the same interface without worrying about the specific type. We have this modularity because anything can be a section or a data source or anything can configure cells. You can have your view controller do all of these things if you really wanted to, but it’s probably better to create separate objects that do each thing.
Like I mentioned before, protocols are very easily tested so we can quickly mock or fake and return this fake data or we can verify that these protocol methods were called. Protocols and extensions really expand our design space. We can define this minimum amount of functionality. We can write all of these extensions to get these really rich behaviors for free when clients just conform to your protocol, implement a couple of methods or properties and then they get all this for free. Just like what Nate was talking about yesterday with the standard library collection protocols and how if you conform to those protocols you get map, reduce, filter all these things for free. That’s all that I have.
Q1: Is protocol oriented programming compatible with object oriented programming?
Jesse: I think yes. You can talk to some people who really, really hate sub classing and will never subclass and will only use protocols. I think subclassing still has its place. It’s still a useful tool but I think most of the time it does cause maybe more problems in certain situations especially on big teams.
I think they’re compatible because really protocols can be a tool to augment your object oriented code base. Also back to what Ben was saying yesterday. When you have this really tangled mess and you need to refactor some giant view controller, the best way to insert a seam into your code base to start to pull it apart and decouple it is to introduce a protocol there and then you can start to peel back the covers and get more flexibility out of your system. Hopefully that answers the question.