SOLID Ruby: Single Responsibility Principle
We make use of BDD and object oriented programming techniques to obtain clear and elegant code, right? Actually, these are outcomes of the main goal: to create code with low maintenance cost, code that doesn’t demands a lot of time and people for fixes and improvements.
There’s a group of guidelines and principles to help us achieve that goal. It’s called SOLID and was first described by Robert Martin (aka Uncle Bob) more than a decade ago.
SOLID isn’t a set of rigid rules nor is the only guideline group you should use, but it’s a very good starting point and should change the way you design and code software right away. Here are the principles described by SOLID:
- Single Responsibility Principle
- Open-Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
The goal of these principles is to make code changes as side-effect free as possible. This means that fixes or improvements should cause the code to change in as few places as possible. We’re effectively diminishing the maintenance cost through a design made to accommodate changes.
I urge you not to fall prey to the easy thinking of “this is Java BS, we don’t need it here”. Good object oriented design applies to any object oriented language. The form may change (tip: Ruby makes it ridiculously easy to implement those principles), but the idea and the fundamentals are the same.
Another obligatory disclaimer is: there isn’t a single way to implement all these principles. Try some techniques and chose what makes sense for you based on your style and way of thinking. I’ll usually show one option, but keep in mind there are a lot more. If you don’t agree, please feel free to express your point of view in the comments.
In this article I’ll talk about the Single Responsibility Principle (SRP). It’s the easiest one to understand and it’s the base for all good object oriented code.
Note: I’ll often use the term “class” in this article but, most of the time, the principles can be applied to any “entity”, be it a class, a module, a method and so on.
The SRP states that a class should have only one responsibility and should fully execute it – that responsibility shouldn’t be splattered among more than one class either. This is a way to achieve high cohesion, a highly desirable trait in OO software. A cohesive class fully executes its responsibility, guarding it against fragmentation and hiding the implementation details.
One way of thinking about what is a responsibility is to think of it as a reason for a class to change. So, we can define this principle as: a class should have only one reason to change.
Okay, time to code! Here’s an hypothetical ActiveRecord class representing a video-game:
This class’ main responsibility is to take care of business logic related to the game state – validations and relationships in this case.
But, as we can see, it’s also responsible for consulting a web service to get the game price and for outputting the game information in a pre-defined format. This implementation has low cohesion, it has more than one reason to change:
– Validation and relationships (we’ll talk more about ActiveRecord breaking the SRP later)
– Price information
– Output formatting
A nice way to solve this problem it to split this class and write two additional classes:
Now we have a class focused on each responsibility previously identified.
To set the game’s price you could use an “orchestration” object that gets the game and applies the price fetched from the web service. Another option is to make this transparent through the Game class:
I don’t like to call external services from ActiveRecord models, but this is a way to do it.
The code still has some flaws and we’ll improve it in the next articles (tip: we’re coupling our objects to concrete implementations instead of roles).
A real example I always like to bring up is the Creditcard class from Spree.
You can see that, besides the “default” ActiveRecord behavior (validation and relationships), this class is responsible for authorizing a card, processing the payment of a purchase and a lot of related behavior. This is obviously bad. We have a class with more than one responsibility with other responsibilities partially implemented by it. It lacks cohesion and is highly coupled (it depends on other parts to fully execute its responsibility).
If you take a look at the current class (master branch) you’ll note that it has been refactored and a lot of the behavior has been extracted from it. It’s a great example of how things can get better by using the SRP.
What about ActiveRecord? Does it violate the SRP? Yes it does, and it’s by design. If you read the description of the Active Record pattern (do not confuse it with ActiveRecord, the framework), you’ll note how this is a conscious trade-off: we’re exchanging a greater level of isolation for a more direct, quick approach. I’m fine with that as long as ActiveRecord models don’t do more than finders and boolean checks (predicate methods).
Keep in mind that being extremist (on either side) is bad. Do not over think the principles or you’ll end up with too many levels of indirection and code that is as hard to maintain as the “thrown together” version.
Here’s a list of tips to avoid breaking the SRP:
- Your class has too many comments explaining why something is there;
- You’re using too many instance variables and often use them as separated groups throughout the methods, like each one was a data structure (each group most likely belongs in a distinct class);
- You’re passing too many parameters on method calls or constructors. There isn’t a “magic number” here, but I start to look for a better abstraction after four parameters;
- The use of private/protected methods often (but not always) means that chunk of code doesn’t belongs there and should be extracted;
- Most of the methods of your class makes heavy use of conditionals.
Here’s a gist with additional links about SOLID and overall good OOP code.
So, that’s it for the Single Responsibility Principle. Next stop is Dependency Inversion Principle.