SOL[I]D - Interface Segregation Principle

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

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.

Comments