Interface Segregation Principle in Swift

Rodrigo Maximo
Movile Tech
7 min readJan 31, 2021

--

Fourth article of the series of five about SOLID and its use in Swift

Introduction

If you are a developer and you are interested in Software Engineering, you have probably already heard about a concept called SOLID.

In case you have never heard about it, do not worry, the first article of this serie has an introduction here.

Interface Segregation Principle (ISP)

As the name suggests, this principle says we should segregate interfaces, that is, separate them into several ones.

The definition used by Uncle Bob in his paper (it can be found here) is that

"Clients should not be forced to depend upon interfaces that they do not use."

This definition is particularly special to me, because although it seems really easy to understand, I confess when I started to program software using OOP (Oriented Object Programming) I would never think about it. I’d say when you read or hear about segregating interfaces, it makes all sense, but when you’re starting to program, probably you won’t think about it. And that’s ok! It’s normal!

We’re going to talk about the problems in not following this principle in the next section, with some examples, but it may be important to give some brief introductions first.

ISP — Problems Disrespecting This Principle

First of all, a term commonly used by Uncle Bob in his paper, that will help us to make things clearer, is Fat Interfaces. These are interfaces (or protocols in Swift) that are not considered cohesive. This means the interfaces may have more methods/functions than they should in semantic terms to the client that will consume it. Relating this to the ISP definition, basically fat interfaces are interfaces that doesn’t follow this principle.

Having fat interfaces brings us some problems (some of them already seen in the SRP article), such as unnecessary refactoring, necessity to rebuild and retest when changes are needed. This happens because those kind of interfaces are responsible for unnecessary coupling, and when a change in a “fat interface” is required, all classes that implement it will have to be rebuilt and retested, and it takes time. Depending on how complex and big the software we’re talking about is, this could take a long and painful time.

Besides that, implementing fat interfaces could also make the understanding and the testability more difficult. In terms of understanding, because things probably won’t make sense semantically. In the case of testability, we’ll have to create bigger mocks and spies, that will have to implement all the unnecessary and unused methods required by this fat interface.

Finally, when we have a software that has several fat interfaces, if we don’t pay attention and give this the importance we should, they will tend to become a pattern and to increase, both the quantity of methods in each already implemented interface and the number of new fat interfaces.

Example in Swift

Consider the following example, where we have two demonstrative classes, Document and PDF. They’re just be used to represent a simple example where we could break this principle.

The Document class has a name and a content that represents the document, while the PDF class takes a document and creates a pdf file with it, but we’re not implementing this, since it’s not the point of our example.

Next, we describe the protocol Machine (our “fat interface”), a simple interface that some machine classes will implement, with some methods that will use a Document object as a single parameter.

Finally, we have three classes that implement this protocol: FaxMachine, NewIphone, UltraMachine.

The first one (FaxMachine) is only capable of implementing the fax(document: Document) method, because it wouldn’t make sense to implement the other methods of the protocol. But, since this class implements the protocol and is forced to implement all its methods, it will return nil for methods that doesn’t make sense to implement.

The second (NewIphone) is capable of converting a document into a PDF or an UIImage through the convert(document: Document) -> PDF? and convert(document: Document) -> UIImage? methods. However, for the same reason, it doesn’t implement in fact the fax(document: Document) method.

The last one (UltraMachine) is capable of doing all the previous stuff.

Problems in breaking the ISP

Let’s talk about the problems we can observe in this example.

Coupling

The first problem we can observe here is the coupling generated by the protocol Machine, since it’s not cohesive. This happens because it has two different responsibilities (single responsibility principle again here!). It has the responsibility of converting a document into a file/image, and it has the responsibility of sending a fax with the document.

Because of that coupling, if we need some changes in the fax(document: Document) method, the NewIphone will have to be rebuilt unnecessarily, and so does every structure that depends on this. The same argument is valid for retesting.

More difficult understanding and testability

Despite not illustrating the problem we mentioned in the introduction about fat interfaces being more difficult to test and understand, since it’s just an example, and it’s made simple and small to make the reader's understanding easier, have in mind that if we had here not just three methods in the protocol, but ten, fifteen, or even more methods, this would definitely happen.

Optional Returns

Another interesting thing to mention here is about the way probably this kind of implementation usually happens when designing a solution. If we try to think about the concrete implementations (classes) first, for example the FaxMachine, the NewIphone and the UltraMachine, and in the interfaces after that, we probably will tend to try to fit the interface into the concrete classes. By that I mean creating just one interface and put all the methods there. On the other hand, if we think about the interfaces before hand, this will probably not happen, as we’re going to see in the next section.

Since we hypothetically used this first approach to design this example solution, and we tried to fit all the methods in one protocol, we had to have some optional returning in some of the methods, because not all the concrete implementations of this protocol would have the capability to return the necessary object.

Because of that, if we have the following code, where only in runtime we know the type of the object as shown below, we’d have to handle the possibility of not having the conversion to a PDF, for example, even when we’re sure it’s possible to do it.

Respecting the ISP

In order to respect the ISP in that example, we could implement it differently, as shown below. We could define two protocols, instead of just one, DocumentConverter and Fax. This would separate the responsibilities and would turn those new protocols into cohesive protocols.

Here, a brief parenthesis is that we could make it better, and break that protocol into three new protocols, for example PDFConverter, ImageConverter and Fax. This could be done, but maybe it wouldn’t be necessary. The point is we have to find a mid-term when trying to break protocols/interfaces into new ones (even when we're just in the design/concept phase of implementations). If we blindly dive into ISP, we’ll probably over engineer in some cases. And this could be dangerous! ⚠️

So, my advice is to measure how many benefits we’re going to have when breaking a protocol or an interface into new ones. Some good questions to answer before doing it:

  • “Is this interface segregation going to bring us any immediate benefits?”
  • “Will this interface segregation bring us benefits in the future?”

Continuing, with those two new protocols, our three classes would be a little bit different, as shown below.

The FaxMachine wouldn't have to implement unnecessary methods, that could cause unpredictable behaviors, the two methods that were returning nil previously.

The same goes for NewIphone class, which wouldn't have to implement the fax(document: Document) method.

Finally, we could use a really interesting feature in Swift to adjust the UltraMachine class, multiple inheritance of protocols. To do that, we just have to make it implement the both protocols.

Also, we wouldn’t have the problem of optional returns, as we can see in the following.

Overview ISP

  • This principle says Clients should not be forced to depend upon interfaces that they do not use.
  • Not following this principle and implementing fat interfaces are synonyms. Fat interfaces are not cohesive interfaces. We could say that those interfaces have more than one responsibility and co-relate this to Single Responsibility Principle (SRP) too.
  • Respect this principle is positive because:
  1. It avoids fat interfaces and, consequently, unnecessary coupling.
  2. It avoids unnecessary rebuilding and retesting when some changes are required in the fat interface.
  3. It makes the developed software easier to understand and test.
  4. It can inspire less experienced developers in a code base, since they could assume naturally this is a better approach, what could avoid having fat interfaces as a pattern inside that software.
  • Finally, to avoid breaking this principle, the better recommendation is preferring to segregate fat interfaces into smaller ones. Just pay attention on how deep you should go, measuring the present and future benefits of that segregation.

References

  1. What is SOLID
  2. Who is Uncle Bob
  3. Robert C. Martin. Design Principles and Design Patterns, 2000

This was the fourth article of the series of five about SOLID and its use in Swift. I hope you have enjoyed and feel free to leave feedbacks, suggest improvements or even send me some messages.

--

--

Rodrigo Maximo
Movile Tech

Lead Mobile Engineer at Nubank |  iOS Engineer