SOLID Principles in Swift - Interface Segragation Principle

Background

In this series of posts we are going to be covering the SOLID principles of software development. These are a set of principles / guidelines, that when followed when developing a software system, make it more likely that the system will be easier to extend and maintain over time. Let’s take a look at the problems that they seek to solve:

  • Fragility: A change may break unexpected parts, it is very difficult to detect if you don’t have a good test coverage
  • Immobility: A component is difficult to reuse in another project or in multiple places of the same project because it has too many coupled dependencies
  • Rigidity: A change requires a lot of effort because it affects several parts of the project

So what are the SOLID principles?

  • Single Responsibility Principle - A class should have only a single responsibility / have only one reason to change
  • Open-Closed Principle - Software should be open for extension but closed for modification
  • Liskov Substitution Principle - Objects in a program should be replaceable with instances of their sub types without altering the correctness of the program
  • Interface Segregation Principle - Many client-specific interfaces are better than one general-purpose interface
  • Dependency Inversion Principle - High level modules should not depend on low level modules. Both should depend on abstractions

In this article we will focus on the Interface Segregation Principle.

What does it mean?

The summary of the principle is as follows:

Many client-specific interfaces are better than one general-purpose interface

In Swift, we use Protocols rather than interfaces in languages such as Java so from here on out we will refer to interfaces as Protocols.

Now the purpose of this rule is quite straight forward in comparison to some of the other rules in the SOLID principles. What it means is its better to create smaller Protocols than to create one large one with lots of methods defined.

What’s the problem

So why does having one large protocol cause a problem? Let’s examine one of the classic Cocoa Touch protocols to see why this is an issue.

public protocol UITableViewDataSource : NSObjectProtocol {

    // 1
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell

    // 2
    optional func numberOfSections(in tableView: UITableView) -> Int
    optional func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? 
    optional func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String?
    optional func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool
    optional func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool
    optional func sectionIndexTitles(for tableView: UITableView) -> [String]?
    optional func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int
    optional func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath)
    optional func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath)
}

I am sure many of you have implemented this protocol at some point in your past ;) I have modified the source slightly to make it easier to read and get the point across So why are we looking at this?

  1. Only 2 methods you have to implement on the first 2.
  2. The rest of the methods are optional and you can implement whichever ones you feel you want to use.

Now this protocol has its roots in Objective C helps it masks the problem somewhat. In Objective C as you can see in the code above its possible to mark certain functions as optional. This means you can implement them if you want to but don’t have to, this allows this protocol declaration to contain too many methods without causing problems for the implementing class.

In Swift, it is not possible to mark functions as optional, all functions need to be implemented. Let’s update the above protocol to be more Swifty and see what problems that might cause us.

protocol MyUITableViewDataSource {

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell

    func numberOfSections(in tableView: UITableView) -> Int
    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? 
    func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String?
    func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool
    func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool
    func sectionIndexTitles(for tableView: UITableView) -> [String]?
    func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath)
    func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath)
}

Now that we have converted our protocol to be more swifty, what problem will this cause when we attempt to make a class conform to this protocol? Let’s have a look at an example.

class MyTableViewDatasource: MyUITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {}
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {}
    func numberOfSections(in tableView: UITableView) -> Int {}
    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {}
    func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {}
    func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {}
    func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {}
    func sectionIndexTitles(for tableView: UITableView) -> [String]? {}
    func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {}
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {}
    func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {}
}

Our class above now has to implement every single protocol method. Even if we don’t intend to use it. In the objective c implementation of the protocol we have the option of implementing only the ones we need whereas now we must implement every single method. Imagine all the view controllers in the world that would be full of empty methods in order to conform to this protocol!

This protocol breaks the interface segregation principle.

A better solution

To improve the solution we could break the one big interface down into smaller protocols. That way we could conform to only the protocols we were interested in implementing for our functionality. This may looks something like:

  1. UITableViewDataSource - For the 2 compulsory methods we are familier with
  2. UITableViewSectionsDatasource - For methods relating to multi section methods
  3. UITableViewSectionTitles - For methods relating to headers and footers in sections
  4. UITableViewEditable - For methods relating to editing and moving cells

This way we could conform to select methods we want, rather than one big interface where we may only want a small subset of the methods.

A good example

A good example of interface segregation in the iOS SDK is Codable. The definition of Codable is as below:

typealias Codable = Decodable & Encodable

Basically Codable is just the combination of 2 other protocols: Decodable and Encodable. This is a great example of how to do the interface segregation. If you are building say a JSON parse struct, you may wish to only conform to Decodable so you can decode the JSON. If in the future you wanted to serialize the struct for something like say data storage you can conform to Encoding later if needed.

Summary

The interface segregation principle is the easiest of the principles to understand in my opinion. In basic terms it means don’t create a big protocol with lots of methods that aren’t always required to be implemented depending on the implementation requirements.

Instead, separate the protocol into smaller protocols with only the methods required for a single piece of functionality to work. Not only does this avoids having lots of redundant methods it also helps to facilitate the single responsibility principle by allowing functionality to be broken down into different classes. For example, you could have different classes to handle different activities rather than one big class with all functionality in.