SoCcam's Razor: Applying the Single Responsibility Principle using a practical approach
Single Responsibilty Principle
The Single Responsibility Principle (SRP) is a logical extension of Separation of Concerns (SoC), while Separation of Concerns deals with layers in the application, Single Responsibility Principle is a refinement that states "A class should only do one thing"
Robert C. Martin (2014) coined the Single Responsibility Principle and describes it as:
The Single Responsibility Principle (SRP) states that each software module should have one and only one reason to change.
This is a very nice short and to the point definition, but as he points out it can be difficult to apply in the real world:
This sounds good, and seems to align with Parnas' formulation. However it begs the question: What defines a reason to change?
Which he goes on to define as:
Gather together the things that change for the same reasons. Separate those things that change for different reasons.
This is a good explanation, but a philosopher would describe this as getting more meta meaning we're talking about the meaning of a meaning: After defining SRP as "each class should have a single reason to change" we have to explain the meaning of the definition it was given, we also need to define what is meant by "a reason to change". (Incidentally this paragraph is even more meta: I am talking about talking about a meaning!)
The problem is, we can keep getting more meta because this definition is still a little vague on its own: what constitutes "different reasons" and what does or doesn't count as a "reason" As Robert C. Martin states in the same article: "Does a bug fix constitute a reason to change?"
The problem with getting more meta to answer these questions is that you get further and further away from the code itself and in a practical sense the original definition becomes increasingly hard to apply: It's difficult to accurately answer the question does this class follow SRP?. You first need to decide the single reason to change and then determine whether the class follows it or not.... How do you make that determination?
One of the people I cite frequently on this blog is Misko Hevery, he's full of useful insights with clear explanations and his thoughts on this subject are no exception.
In his code reviewers guide (An excellent and concise guide for becoming a better programmer) he describes the Single Responsibility Principle in his article Flaw: Class Does Too Much (2009) and provides a much less meta explanation of how to determine whether a class follows SRP or not.
Try to sum up what a class does in a single phrase. If this phrase includes "AND" it’s probably doing too much.
This is much more easy to apply: Use a single phrase or short sentence to explain what the class does and if you can't do it without using the word and then the class is probably doing too much. This is a much easier process to follow than getting more meta and going into details such as what counts as a "reason"? and produces essentially the same result: Smaller classes that only do one thing. As a pragmatist this is a far better litmus test as it's a lot easier to apply.
However, despite its ease of use and clarity there are a couple of issues with this approach
1) And isn't always a problem.
Consider the following class:
class Serializer {
public function serialize($object) {
return json_encode($object);
}
public function unserialize($object) {
return json_decode($object);
}
}
To describe this class we would probably say "Serializer serializes and unserializes objects". There's that pesky word and. Does this mean that this class is doing too much and violates single responsibility principle? Let's go back to the Reason to change definition. If the encoding format changes from JOSN to XML, both methods need to be changed. This is a single reason to change. So should this class be broken up into a Serializer and an Unserializer? In short, no. The litmus test here is writing a unit test.
Consider a unit test where we do have a different serializer and unserializer class:
$object = new stdclass;
$object->foo = 'bar';
$serializer = new Serializer();
$unserializer = new Unserializer();
$serialized = $serializer->serialize($object);
$unserialized = $undersializer->unserialize($serialized);
$this->assertEquals($unserialized, $object);
The first red flag here, of course, is that the test needs to test two different objects at once. The second giveaway is that if one of the classes changes, the test fails. For example, changing the serializer to use XML without also changing the unserializer will cause the unit test to fail. Both classes have the same reason to change despite the fact we're using the word and
Obviously Misko Hevery isn't advocating splitting up the serializer and as a general rule and is a dead giveaway that SRP has been broken however, in order to provide an easier to follow definition of SRP I needed to clear that up, which I will sum up as:
Complementary functions can use the word AND
Complementary functions are functions that act on the same data. You'll already be familiar with these. For example:
- Save and load
- Open and close
- Connect and disconnect
- Encode and decode
- etc
Going back to Robert C. Martin's reason to change definition, if one of the functions changes, the other has to as well. They share a reason to change so belong together in the same class.
2) Descriptions are vague
The biggest problem with using words to describe a piece of code is that it's possible the explanation is so vague it can be applied to other pieces of code. I've seen this happen, people say their class follows SRP by providing a slightly vague definition of what the class does.
For example, consider the following class:
class Serializer {
const JSON = 1;
const PHP = 2;
private $mode = self::JSON;
public function setMode($mode) {
$this->mode = $mode;
}
public function serialize($object) {
if ($this->mode === self::JSON) {
return json_encode($object);
}
else if ($this->mode === self::PHP) {
retrun serialize($object);
}
}
public function unserialize($object) {
if ($this->mode === self::JSON) {
return json_decode($object);
}
else if ($this->mode === self::PHP) {
retrun unserialize($object);
}
}
}
This could be summed up as "serializes and unserializes objects" following Hevery's definition. It possibly also follows Martin's "Single Reason to change" definition: The only reason to change this class is to add a new serialization format.
...or does it? If we really wanted to explain what this does we need to say "Serializes objects into JSON and PHP's inbuilt serializer formats"
Suddenly, it breaks Hevery's definition by including the word and because we used a more refined definition of what the class does. A easy trap to fall into when using Hevery's definition is that it's possible to describe the class in a vague way that doesn't use the word and giving you a false sense of security.
Occam's Razor
I've already borrowed the term more meta from philosophy in this article. There's another philosophical tool I'm going to borrow and apply here. It's a little more well known: Occam's Razor. The general principle states that "Entities should not be multiplied unnecessarily" (Gibbs, 1996) and this is often simplified as "If you have two equally likely solutions to a problem, choose the simplest." (Gibbs, 1996). The idea is that if you have two competing solutions, choose the simplest.
I'm going to apply this philosophical tool to SRP and show how it can be used to determine whether a class follows SRP or not in a very easy to apply practical manner. I'm going to call this SoCcam's Razor (ok it's to do with SRP not SoC, but the name works better this way!)
Given the two versions of the serializer above:
class Serializer {
const JSON = 1;
const PHP = 2;
private $mode = self::JSON;
public function setMode($mode) {
$this->mode = $mode;
}
public function serialize($object) {
if ($this->mode === self::JSON) {
return json_encode($object);
}
else if ($this->mode === self::PHP) {
retrun serialize($object);
}
}
public function unserialize($object) {
if ($this->mode === self::JSON) {
return json_decode($object);
}
else if ($this->mode === self::PHP) {
retrun unserialize($object);
}
}
}
and
class Serializer {
public function serialize($object) {
return json_encode($object);
}
public function unserialize($object) {
return json_decode($object);
}
}
Both meet the description "serializes and unserializes objects". This definition is vague, but sometimes it's difficult to find the words to accurately describe what a class does in a concise manner. The beauty of SoCcam's Razor is that this doesn't matter.
Following SoCam's Razor, we can look at the two classes, both of which follow the definition, and see that the second one is simpler, therefore follows SRP more closely than the first.
Ok, so what if you only have one version of the class and not the contrast? Imagine you had the longer serializer class:
class Serializer {
const JSON = 1;
const PHP = 2;
private $mode = self::JSON;
public function setMode($mode) {
$this->mode = $mode;
}
public function serialize($object) {
if ($this->mode === self::JSON) {
return json_encode($object);
}
else if ($this->mode === self::PHP) {
retrun serialize($object);
}
}
public function unserialize($object) {
if ($this->mode === self::JSON) {
return json_decode($object);
}
else if ($this->mode === self::PHP) {
retrun unserialize($object);
}
}
}
And nothing to compare it against. With no comparison this is the simplest implementation available. However, we can still apply SoCcam's Razor by asking the question: If we removed any methods or instance variables from this class will it still meet our definition?
In this case: Yes. Given the definition "serializes and unserializes objects" the setMode method can be removed and the class still follows this definition:
class Serializer {
const JSON = 1;
const PHP = 2;
private $mode = self::JSON;
public function serialize($object) {
if ($this->mode === self::JSON) {
return json_encode($object);
}
else if ($this->mode === self::PHP) {
return serialize($object);
}
}
public function unserialize($object) {
if ($this->mode === self::JSON) {
return json_decode($object);
}
else if ($this->mode === self::PHP) {
return unserialize($object);
}
}
}
SoCcam's Razor can be thought of as the question: Can I remove any methods/instance variables from the class and still use the same description?
By using SoCcam's Razor to shave off each method in order, we can test to see whether that method is actually needed to meet our description. In this case, the class doesn't need a mode to fulfill its given responsibility which allows us to shave off all the logic which is related to the mode attribute
However, if I removed the serialize or unserialise methods, the description no longer fits the class meaning they have to stay.
After removing the method, we are left with inaccessible code paths. These can now be removed and we'll end up with something like:
class Serializer {
public function serialize($object) {
return serialize($object);
}
public function unserialize($object) {
return unserialize($object);
}
}
class JsonSerializer {
public function serialize($object) {
return json_encode($object);
}
public function unserialize($object) {
return json_decode($object);
}
}
Now we have two classes that have different responsibilities: Encoding/Decoding into JSON and encoding/decoding using PHP's inbuilt serialize methods. In this instance we could also produce an interface so that they are interchangeable.
The original description "serializes and unserializes objects" fits both classes, but it's impossible to remove any more methods from either class while using that description.
Applying SoCcam's Razor
The general process for applying SoCcam's Razor is:
- Use Misko Hevery's test to give the class a description that doesn't include the word and*
- As a thought, or practical, experiment, remove each method and instance variable from the class one at a time
- If after removing that instance variable and/or method it still meets the description then either your description is wrong and go back to (1) or the class doesn't follow SRP and the method you removed belongs elsewhere (or isn't needed at all).
* Except for complementary functions as described above
In closing
I think there's a little bit of a gap in the explanation of how to work out whether a class follows SRP or not. By borrowing a tool from the philosophy department we're able to make an easy to follow process that allows us to much more easily determine what a single responsibility is at a practical level without concerning ourselves too much with defining meta concepts like reason to change and what is a responsibility anyway?
References
Gibbs, P (1996) What is Occam's Razor? [online]. Available from: http://math.ucr.edu/home/baez/physics/General/occam.html/ [Accessed 17th June 2015]
Hevery, M (2009) Flaw: Class does too much [online]. Available from: http://misko.hevery.com/code-reviewers-guide/flaw-class-does-too-much/ [Accessed 17th June 2015]
Martin, R (2014) The Single Responsibility Principle [online]. Available from: https://blog.8thlight.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html [Accessed 17th June 2015]