SOL[I]D - Interface Segregation Principle
- 04 July 2017
- Patterns Ruby Design
- SOL[I]D - Interface Segregation Principle
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: