Protocol oriented programming in Swift
Nulab
June 09, 2016
This WWDC talk is where it all started. I watched the video a couple of times and found it a little too provoking. The presentation shows a strong objection against Object-oriented programming (OOP).
I have special interest in this topic because the app that I’m now working on is getting bigger and more complex. It is becoming more difficult to comprehend the code and has a tendency of getting bloated. To be honest, when it is time to put the idea into practice, I find it challenging.
Instead of discussing how Protocol Oriented Programming could fix the world, lets take a look at what we can do with protocol to make our code better. I will be using a portion of Backlog iOS client code as the example in this article.
Value type
If you use struct instead of class but need class-inheritance-like-hierarchy, you have to use protocol because value types in Swift do not support inheritance.
In Backlog, an issue can have several custom fields. If you are using class, you probably will make CustomField as the base class. But for struct, we have to use protocol. The following is the requirements for CustomField.
protocol CustomField { var id: Int { get } var name: String { get } var required: Bool { get } var description: String? { get } var applicableIssueType: [IssueType] { get } }
Based on type property in JSON data, CustomField can be TextField, TextArea, NumericField, DateField, Checkbox, RadioField, etc.
For example, we can now make DateField conform to CustomField. Please notice that the keyword is conform not inherit.
struct DateField: CustomField { // required by CustomField let id: Int let name: String let required: Bool let description: String? var applicableIssueType: [IssueType] { get } // specific to DateField type let initialDate: NSDate? let min: NSDate? let max: NSDate? }
Code reuse
One of the advantages of inheritance, subclass can have all the work done in superclass for free.
In Swift, Protocol Extension allows us to provide a default implementation to any method or property of the protocol.
Let say, we need a method to check if custom field is applicable to provided issue type.
extension CustomField { func applicableToIssueType(type: IssueType) -> Bool { return self.applicableIssueType.indexOf(type) != nil } }
If we need this method to be customizable by conforming types, we can add this method as requirement.
protocol CustomField { var id: Int { get } var name: String { get } var required: Bool { get } var description: String? { get } var applicableIssueType: [IssueType] { get } func applicableToIssueType(type: IssueType) -> Bool }
Helper
In Backlog, there are many issue lists such as my issues, project issues, recently viewed issues, etc. Users can filter lists by typing keywords inside the search box. To make the search feature possible, a helper function is needed to turn original issues and keywords into filtered issues.
There are many ways to write a helper:
- You could write it as a global function, but be aware that it could pollute your global name space.
- You could create a class and make the helper as class method, but this method may feel awkward to you.
- You could make a helper class and instantiate it, but you will have to manage that helper instance in your controller.
- You could write it as method of your controller and duplicate it for each of the other controllers, but this method will get messy.
- You could also put the helper inside common base class, but the superclass often gets bloated by doing this.
Here, we will try to use protocol. In my opinion, protocol is a more natural way to write helper functions.
protocol IssuesFilterable {} extension IssuesFilterable { func filterByKeyword(originalIssues: [Issue], keyword: String) { return originalIssues.filter { $0.issueKey.rangeOfString(keyword, options: .CaseInsensitiveSearch) != nil || $0.summary.rangeOfString(keyword, options: .CaseInsensitiveSearch) != nil } } } extension RecentlyViewedIssuesController: IssuesFilterable {}
You can make originalIssues as requirement so you don’t need it as function parameter.
protocol IssuesFilterable { var originalIssues: [Issue] { get } } extension IssuesFilterable { func filterByKeyword(keyword: String) { return self.originalIssues.filter { $0.issueKey.rangeOfString(keyword, options: .CaseInsensitiveSearch) != nil || $0.summary.rangeOfString(keyword, options: .CaseInsensitiveSearch) != nil } } }
You can save the filter result to required filteredIssues property and reload tableView if the controller is a UITableViewController.
protocol IssuesFilterable: class { var originalIssues: [Issue] { get } var filteredIssues: [Issue] { get set } } extension IssuesFilterable where Self: UITableViewController { func filterByKeyword(keyword: String) { self.filteredIssues = self.originalIssues.filter { $0.issueKey.rangeOfString(keyword, options: .CaseInsensitiveSearch) != nil || $0.summary.rangeOfString(keyword, options: .CaseInsensitiveSearch) != nil } self.tableView.reloadData() } }
View model
In the Backlog app, user model resides in separate module (API client library) that is rarely changed. Here is the model for user.
public struct User { public let id: Int public let userId: String? public let name: String public let roleType: RoleType public let lang: String? public let mailAddress: String? }
Usually before rendering you need some kind of formatting and sure you don’t want to do it neither inside your controller nor UITableViewCell. We need a view model and let use protocol for this.
protocol UserPresentable { var avatar: UIImage { get } var attributedName: NSAttributedString { get } }
Next, we will make all user as UserPresentable.
extension User: UserPresentable { var avatar: UIImage { // load and return image based on id // Usually it's not this simple, it should be asynchronous and return some kind of promise } var attributedName: NSAttributedString { // For example, base on roleType, bold name for administrator and regular for others } }
Now we can ask user cell to render it.
extension UserCell { func render(modelview: UserPresentable) { self.avatarView.image = modelview.avatar self.nameLbl.attributedText = modeview.attributedName } }
Mixins and Traits
Protocol is the closest thing to Mixins and Traits in Swift. Mixins and Traits shine in game development that usually has a deep class hierarchy. The mantra is “prefer composition over inheritance.” Basically, you try to build your class or struct as an aggregation of several capabilities.
Lets say, we need to cache and serialize user to file. Not only user, we may also need other models to be cached. Instead of making a superclass, we will use protocol.
protocol JSONSerializable { var json: [String: AnyObject] { get } } extension User: JSONSerializable { var json: [String: AnyObject] { // return json representation of user } }
Now user model conforms to UserPresentable and JSONSerializable. You can add more capabilities to user as you need.
Wrapping up
If you noticed in the above examples, we discussed things in terms of what a type can do, not what a type is. Although it is not always straightforward to design this way, trying to imagine how your classes or structs are structured using protocols could be helpful to better understand your code.
One year after the WWDC talk, myself and others still do not have a clear idea of what Protocol Oriented Programming is. I have already read many articles and blogs related to this topic and I am expecting more information about this topic in the upcoming WWDC 2016.