SOL[I]D - Interface Segregation Principle

Subscribe to receive new articles. No spam. Quality content.

Follow @makagon

We've covered three SOLID principles so far. But this one is going to be special. Interface Segregation Principle refers to Interfaces, but we don't have it in Ruby. Should we omit this part? I don't think so, we can still learn something from it.

This principle was defined by Robert C. Martin this way:

Clients should not be forced to depend upon interfaces that they don't use.

If you're not familiar with the concept of interfaces, I'll try to describe it really briefly, so you can get the idea.

An interface describes only the signatures of methods. A class that implements the interface must implement the methods of the interface that are specified in the interface definition.

I'll show simple C# example, so you will get the idea:

interface IMovable { void Move(); // no implementation, describes method signature } // class implements interface class Mouse : IMovable { void IMovable.Move() { // implementation } }

As we can see if Mouse implements interface IMovable it must implement all methods described in the interface. In this case it's just one method Move() , but very often interfaces are getting "fat". Even if my class needs just couple methods described in the interface, I still have to implement all methods described in that interface.

There are couple more definitions that should help us to get the idea:

Clients should not be forced to depend on methods that they do not use. Many client specific interfaces are better than one general purpose interface. The dependency of one class to another one should depend on the smallest possible interface.

Uncle Bob suggests to split fat interface into smaller ones, so you don't have to implement all methods described in one giant interface. Instead you can pick the interface you need to implement with just subset of methods.

But, I use Ruby...

We don't have interfaces, but there is something that we can learn from this principle, especially from this part:

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

Let me show you by example how we can break this rule using Ruby. Let's say that we have FeeCalculator which allows us, well to calculate fee :)

class FeeCalculator def calculate(product, user, vat) # calculation end end

We use this calculator in just one place of the app.

class ProductController def show @fee = FeeCalculator.new.calculate(product, user, vat) end end

The other developer has request to add new controller. For that controller he needs to store a fee after the calculation.

So he knows that we already have code to calculate fee, all he needs to do is to add saving fee logic to calculate method.

But the problem is that ProductController#show also uses calculate method, and we don't want to store fee for that case. I saw many times how developers did something like that:

class FeeCalculator def calculate(product, user, vat, save_result) # calculation if save_result # storing result into db end end end class ProductController def show @fee = FeeCalculator.new.calculate(product, user, vat, false) end end class OrderController def create @fee = FeeCalculator.new.calculate(product, user, vat, true) end end

They add new argument to calculate method, and pass true/false depending on the need they have.

Let's think why it's bad. First of all now we have to pass some weird boolean that changes behavior of method. I know that we could add default false value to method definition, but it wouldn't help us if we have additional params after save_result param.

So it violates the basic rule:

Clients should not be forced to depend upon interfaces that they don't use.

In our case we're not dealing with interfaces, but we depend on method signature. One of the clients, ProductController#show doesn't want to save fee at all, but it forced to pass false argument to keep using calculate that method.

To refactor this code we have couple options. First of all Interface Segregation says that we should create smaller interfaces. I would suggest to do something like this:

class FeeCalculator def calculate(product, user, vat) # calculation end def calculate_and_save(product, user, vat) fee = calculate(product, user, vat) save(fee) end private def save(fee) # storing result into db end end

I know that when you have "and" in method name it's a code smell itself, but at least now we have client specific interfaces.

Depending on the situation, this refactoring could be possible as well:

class FeeCalculator def calculate(product, user, vat) # calculation end def save(fee) # storing result into db end end

In this case clients are responsible for storing fee if it's required:

class OrderController def create fee = fee_calculator.calculate(product, user, vat) fee_calculator.save(fee) end private def fee_calculator FeeCalculator.new end end

One of the Sandi Metz' rules says:

Pass no more than four parameters into a method. Hash options are parameters.

Really good rule to follow. If method has more than four arguments, probably you should split that big "interface" into smaller ones and make them more client-specific.

I know that many Ruby developers just skip this principle because we don't have interfaces, but it's good to understand ideas behind this principle. It helps C# and Java developers to write better code.

I hope it was interesting reading and you found something new here. Thanks for reading!

Read more about SOLID Principles in case if you missed it:

Subscribe to receive new articles. No spam. Quality content.

Follow @makagon