Object Fundamentals: Single Responsibility Principle
A five-minute summary on creating classes that are easy to reuse.
Recently, I’ve been reading Sandi Metz’s book on object-oriented design, Practical Object-Oriented Design: An Agile Primer Using Ruby (POODR). The following is a polished summary of chapter two, which covers the single responsibility principle (SRP).
In object-oriented programming languages, objects are like building blocks. We put them together in different ways to create something useful.
For example, in Java web applications created with Spring Boot, it is common for Controller
classes to handle HTTP requests but delegate business logic to Service
classes. These Service
classes often read or write to databases, which Spring Boot calls Repository
classes.
The object dependency graph might look something like this:
A Controller
has a dependency on a Service
because a Controller
uses a method in the Service
class. Likewise, a Service
has a dependency on a Repository
.
These classes each have a single responsibility:
Controller
classes handle incoming HTTP requests.Service
classes execute business logic.Repository
classes handle database operations.
In the future, if some other Controller
needed to execute the business logic in a Service
, they could reuse that Service
object:
Imagine if we wrote our business logic in the Controller
instead. Now, we would have a Controller
responsible for handling incoming HTTP requests and executing business logic:
Other Controller
classes wouldn’t be able to reuse this business logic because it is encapsulated by the existing Controller
class.
In this situation, you could take one of three actions:
Duplicate the business logic in the new
Controller
.Expose the business logic as a public method in the existing
Controller
.Pull the business logic out of the existing
Controller
.
Choice 1 is problematic because duplicating business logic makes the codebase less maintainable. Changes in business logic would need to be made in two places instead of one.
Choice 2 is not appealing either because it means our new Controller
would have a dependency on the existing one:
If the existing Controller
were to change one day (e.g., its constructor changes), those changes would affect the new Controller
. It’s not a good sign if classes have to change due to changes unrelated to how they’re used.
Choice 3 is likely the most pragmatic solution and would lead you to a solution like the one in Figure 2.
The extended thought experiment above illustrates why classes should have a single responsibility: Classes that have a single responsibility are easier to reuse.
The single responsibility principle states that a class should only have one purpose. All class methods should be related to this purpose (in object-oriented terminology, the class should be cohesive).
The definition above, while enlightening, isn’t very helpful. How do you know if a class has a single purpose? According to Metz, you should try the following:
Turn each class method into a question and consider if it makes sense. For example, a
Person
class might have anisAdult
method. The question, “Hello person, are you an adult?” is reasonable. In contrast, imagine if aGameMenu
class had asaveGame
method. The question, “Hello game menu, can you save this game?” seems shaky.Try to explain what the class does in one sentence. If the simplest answer requires words like “and” or “or,” then your class has multiple responsibilities.
Any time a class fails one or both of the tests above, you might start wondering about introducing abstractions and breaking things up.
Making design decisions based on an unknown future is difficult. How can we make the right decisions today when we don’t always know what changes will occur in the future?
Metz suggests considering the future cost of doing nothing today. The most pragmatic solution might be to wait for more information before making a design decision.
This approach reminds me of the following well-known phrase in software engineering: No abstraction is better than the wrong abstraction. Similarly, sometimes making no decision is better than making the wrong one.
Metz also acknowledges the opposing side. If you postpone design decisions, future maintainers may replicate this (wrong) pattern in the codebase or reuse your class, compounding the problem.
The ability to navigate this tension between making decisions today with the information available and postponing decisions until more information arises is important.
Design decisions like these often have no clear right answer. The key is to make informed tradeoffs that balance the needs of today with the possibilities of tomorrow.
To my readers: When did you start learning about object-oriented design? What other books on this topicdo you think I should read? Leave a comment down below!