It’s time for another post in my intermittent series of Best Practices Gone Bad (BPGB). This time we’ll cover the dark side of the Single Responsibility Principle (SRP). According to Robert Martin:
A class should have one, and only one, reason to change.
The basic concept is easy. Every object or method should be responsible for only one thing. This makes a lot of sense because combining two separate pieces of functionality in a single class or method usually makes that item harder to maintain, understand, or troubleshoot. It seems obvious that you can improve any design by ruthlessly breaking any class with more than one responsibility into two of more classes. Likewise, any method that does more than one thing should be separated into two or more methods. The logic here seems pretty obvious.
If you notice, the third word in the name is actually important. This is a principle that should help you think about how to organize your objects and methods. It is not a law that mandates how all code is written. The problem begins when you take the principle too far. You could argue that almost anything can be divided into separate responsibilities. Smaller classes and methods must be easier to understand, right? Unfortunately, if you divide a concept inappropriately, you might need to add a class to pull the concept back together.
An Example
For example, let’s say you have a circle class. To define a circle, you need a center point (let’s say just x
and y
), and a radius. For the sake of the argument, let’s say you want the circle to be able to return a circumference and a bounding box. (Let’s use Ruby for this example.)
class Circle
attr_reader :x, :y, :radius
def initialize x: x, y: y, radius: radius
@x = x
@y = y
@radius = radius
end
def diameter
radius * 2.0
end
def circumference
diameter * Math::PI
end
def bounding_box
[ x - radius, y - radius, diameter, diameter ]
end
end
You can see that I factored out a diameter
method to DRY the code, which also moved a responsibility into a separate method.
A Step Too Far
Now, someone in the throes of Single Responsibility Preoccupation might reason that the Circle
class should just contain the attributes of the circle, it should not also be responsible for calculating synthetic attributes, like circumference
, and bounding_box
. Maybe we should separate those out. After all, other classes might need a circumference
or a bounding_box
. So, let’s create a Circumference class, that returns the correct length when supplied with a Circle
object. And, we can create a BoundingBox
class that calculates the upper, left corner and width and height, given a Circle
object.
The problem is that it almost makes sense. But, I would argue that the knowledge of how to calculate the circumference, or make the appropriate bounding box is actually part of the concept of a Circle. Separating those methods out splits the concept of a Circle, moving some if its responsibility elsewhere. This leaves the concept of a Circle
in three classes.
Now, some of you will think that this is a Straw Man argument. That’s not the case. I was working on a system recently with a set of classes that supplied 4 public methods as an interface. Some people suggested that we should separate each of these classes into 4 classes, one for each original method. It did take a bit of argument to show how the 4 methods together made a single concept, that would be harder to work with if it were broken down.
Conclusion
The Single Responsibility Principle is an effective mental tool to avoid classes that do too much. But, becoming preoccupied with the principle can lead to hard-to-understand code.