Welcome to the five-part series of blog posts about SOLID Principles. In each part, I will describe and analyze one of these principles. In the last part, expect a summary of the entire series containing a few tips and thoughts.
Let’s start. What are SOLID Principles? There are five general design principles of object-oriented programming intended to make software more understandable, extendable, maintainable and testable.
Today, more about fourth principle - Interface Segregation Principle.
Interface Segregation Principle
No client should be forced to depend on methods it does not use.
or in other words
Many client-specific interfaces are better than one general purpose interface.
So interfaces that we create should not contain methods that we do not need.
Let's go straight to an example because there is the best way to explain this principle.
In the beginning, there was a completely different example of violation right here. After some discussion on Reddit, I have been convinced that it did not show the violation best. So I decided to reconsider my approach/thinking and provide you a better example.
The old example can be found HERE. It still can be instructive because it shows that we should avoid creating classes with a too rich interface - in the most cases, more classes with smaller interfaces are better than one class with a big interface. But... it's not a point of ISP.
Ok. Let's go to a new example then. In languages like C# or Java where we have interface types, ISP is much easier to understand. We just create a new interface and implement it while creating a new class. Sign of ISP violation is clearly: we have to implement some methods (we are forced to do that) from the interface which we don't need.
But in Ruby, we do not have interface types. We are not forced into implementing anything. Somebody says that in dynamic-typed languages, we cannot violate ISP and we can discuss for a long time about it. For me, while we cannot actually break ISP rule in an obvious way, we can still get some benefits by fulfilling this principle on let's say - lower level. We will not be talking about implementing methods from interfaces in classes. We will discuss creating (good) method signatures. The method signature is part of a class interface, isn't it?
class PostRepository def get_all_by_ids(ids:) entity.where(id: ids) end private def entity Post end end # Usage module Admin class PostsController def index @posts = PostRepository.new.get_all_by_ids(params[:ids]) end end end
At the beginning everything is clear. We have the simple PostRepository class that is responsible for querying infrastructure to obtain data. We have only one public method for now and it's fetching all posts with given ids. We use our repository in a single place - in PostController in Admin area.
Now, let's complicate situation a little bit. Suppose that we want to use the same method on the client side in HomeController but we have to obtain posts ordered by title. The first idea could result in such code:
class PostRepository def get_all_by_ids(ids:, sort:) posts = entity.where(id: ids) posts.order(title: :asc) if sort posts end private def entity Post end end # Usage module Admin class PostsController def index @posts = PostRepository.new.get_all_by_ids(params[:ids], false) end end end module Client class HomeController def index @posts = PostRepository.new.get_all_by_ids(params[:ids], true) end end end
Where is the problem? Let's look on Admin::PostsController#index action. We are forced to start using new parameter although we don't need any sorting at all! So we depend on part of the interface which we don't need.
You would say: Ok! Let's introduce default value for "sort" parameter in the method signature. I would say: Yes, it will do the job but is it not hiding some details? And what if we must introduce the third parameter? Then we will be forced to provide the second one explicitly.
One of the possible solutions is to split up this logic into two separate methods:
class PostRepository def get_all_by_ids(ids:) entity.where(id: ids) end def get_all_by_ids_sorted(ids:) get_all_by_ids(ids).order(title: :asc) end private def entity Post end end # Usage module Admin class PostsController def index @posts = PostRepository.new.get_all_by_ids(params[:ids]) end end end module Client class HomeController def index @posts = PostRepository.new.get_all_by_ids_sorted(params[:ids]) end end end
It's a common way of designing repository classes. Repositories often have methods like: get_by_id, get_by_id_with_association etc. Just do not fall into the trap where you have 100 methods like "get_by_id_with_this", "get_by_id_with_that", "get_by_id_with_this_sorted_by_title" in single class. We could have separate repositories for Admin and Client area.
Let's go back to our problem. Did we solve it? I think we did. Now we have simple and easy to understand interface. Without specifying anything implicitly and without forcing a client to use something that it does not need.
Interface segregation principle seems to be very straightforward but only when it comes to static-typed languages. In dynamic-typed languages, the case is debatable. We can argue whether is ever possible to break this principle. I think that such considerations can be ended with these words:
We use all these rules/principles/patterns to achieve some benefits, not just to fulfill/apply them. So if benefits aren't clear, applying rule will lead to nothing.
We always must see a visible benefit of doing something. If we cannot see it, we will just waste a time. Benefits can be very different: better code quality, better code organization, increased performance, more understandable codebase etc.
So if you are convinced that trying to fulfill ISP (even in farfetched interpretation) gives you nothing, just don't do it.
Let's sum up it all. ISP is about creating minimal interfaces. We should always try to share clients the minimal interface, which contains only methods that are really necessary and nothing more. Remember that method signature is also part of a class interface. These signatures should be also easy to understand and to use.
Thanks to @KMaicher for making me change my point of view and make me think again about the examples I've given