The I in SOLID

Today we will discuss the I in SOLID which, you may or may not know, represents the Interface Segregation Principle (ISP). This is the fourth article in the SOLID series. We have already discussed the Single Responsibility, Open/Closed and Liskov Substitution principles.

In this post we will discuss the value of and the process for crafting easy to maintain interfaces. If we have enough time we will also discuss how interfaces might apply to dynamically typed languages such as Ruby. With no further ado, let us start by finding out what an interface actually is.

What

What is an interface

An interface defines a set of unimplemented public methods. Think of it as some sort of class template. The interface does not provide the behavior for the defined methods, it only provides the method signatures. The classes that implement the interface provide the behavior for the predefined methods.

Remember that classes can implement more than one interface, but they must provide behavior for all the methods added by the interfaces.

Not all programming languages support explicit interfaces. Statically typed languages support explicitly declared interfaces. If you are wondering, here is a list of example statically typed languages:

  • Ada
  • C#
  • D
  • Dart
  • Delphi
  • Go
  • Java
  • Logtalk
  • Object Pascal
  • Objective-C
  • PHP
  • Racket
  • Seed7
  • Swift

What is ISP

By now, in this series of blog posts, it is clear that the primary reason for the SOLID principles is to optimize software for maintainability. ISP is concerned with optimizing the types of interactions that can take place between objects.

ISP addresses the disadvantages that bloated and fat interfaces bring to software. Instead, ISP encourages us to create highly specific interfaces.

A client should never be forced to implement an interface that it doesn't use - Robert C. Martin

Why

You might notice some similarities between LSP and ISP. Both principles warn us against adding unused methods. The difference is that ISP is targeted at class interfaces while LSP is targeted at inheritance hierarchies.

When we violate ISP we add unnecessary complexity and redundancy to the software we write. Bloated interfaces increase the chances of side effects rippling through our application, forcing us to perform shotgun surgery when relatively small changes are made.

To illustrate the problem with bloated interfaces, take a moment to imagine an interface that is used in many different places throughout an application. Some methods are not needed by all the classes that implement the interface. However, we decide not to divide the interface into smaller separate interfaces, instead we return nil wherever a method is not needed.

One day we are asked to fix an urgent bug that requires that we alter one of these methods. It is a very simple change, we just need to add one argument. However, unfortunately for us, we need to visit each class that implements the interface to add the missing argument there too. The worst part is that the classes that return nil do not even use the method we now need to change. The cost of this simple change was increased by our earlier decision to not separate the interface out into more specific interfaces.

This example illustrates how changes can ripple through our application. It does not emphasize the confusion bloated interfaces cause. That is a whole different problem. New team members will have no idea that the nil implementation was an unfortunate side effect of implementing a non-specific interface.

We can reduce the cost of future changes and make it much easier for future programmers when we take heed to the guidelines ISP provides.

How

Implementing ISP is very specific to the software it is applied to. Instead of rattling through specific examples, we will make list of things to look out for when you apply ISP.

  • Split interfaces with clearly different roles, just like you would separate classes into separate responsibilities.
  • Be on the lookout for classes with methods that return nil or throw not implemented exceptions. When you find empty methods you can see if there is an opportunity to split the interfaces that the class is implementing.
  • Don't separate interfaces to the point where your application becomes fragmented.

How do we apply ISP to Ruby applications?

It is your interfaces, more than all of your tests and any of your code, that define your application and determine its future - Sandi Metz

It would be completely reasonable to ask if this principle applies to Ruby, or any dynamically typed language. And the answer is yes and no. Of course you don't need to be concerned with interfaces and the syntax that goes along with them. However, dynamically typed languages have interfaces of a different nature.

I like to think of dynamically typed interfaces as implied interfaces. We have all heard the saying “If it walks like a duck and it quacks like a duck, then it must be a duck”, similarly classes that define the same public methods effectively have the same interface.

We need to be extremely deliberate with the public interface we expose to outside clients. Once we have defined a class‘s public interface we need to make sure that it is stable and well tested.

Public interfaces are important because they indicate the primary purpose of a class and represent the set of methods that are safe to depend on. In contrast to public interfaces, we have private interfaces that represent the unstable, unreliable class methods.

Sandi Metz encourages object oriented programmers to shift the emphasis we place on classes over to the messages that pass between objects. In other words, we need to shift our attention to the interfaces we define.

The messages that pass between classes define the types of interactions classes can have with each other. We can improve the maintainability of software by carefully crafting the types of interactions that are allowed.

One useful technique for making sure that we allow the right messages between objects is to apply the “Tell, don't ask” principle. We want to make sure that objects tell each other what outcome is required instead of asking and then proceeding to the desired outcome.

Conclusion

In this post we discussed ISP. ISP applies primarily to statically typed languages such as Javascript or C#. An interface provides us with a template for the methods we would like to see defined within the classes that implement the interface.

We can decrease the costs of change when we are intentional with the interfaces we define.

It is true that ISP does not apply to Ruby or other dynamically typed languages. Ruby does have implied interfaces and there are still a few important principles we can apply to the interface crafting process in Ruby.

As software engineers it is our responsibility to communicate our intentions clearly. We do this by being deliberate with the public interfaces we define. Crafting class interfaces require that we clearly indicate which methods are safe to depend on and which methods should be avoided.

At OmbuLabs we take interface crafting seriously because we care about the future maintainability of the applications we work on. Contact us to hear how our team can help you build public interfaces that communicate your intentions clearly.

Further reading