The problem with the Single Responsibility Principle (SRP) is in defining exactly what is or is not a “responsibility”. As with so many things in programming and life, the definition is fluid and can change depending on context. Sometimes it also has to do with the level of abstraction that you use to discuss the class.
Example: Repository Pattern
Consider the case of the Repository Pattern. As formulated by Martin Fowler in his timeless classic, Patterns of Enterprise Application Architecture (PoEAA), a repository typically contains some or all of the following parts:
- An interface to the database, to execute queries and return results
- A Data Mapper object, which maps results from the Database into objects usable by the software system.
- A Query Object or some kind of query generator which can take query parameters and translate those into queries on the Database
- Typically, some kind of Identity Map to prevent multiple loading and mapping of the same data object more than once.
This seems like the repository has several different responsibilities: Working with the database, mapping database results, constructing search queries and possibly some caching tasks for performance reasons. This seems like more than one responsibility.
But, if we go up a level of abstraction and look at the interface the Repository provides instead of the pieces from which the Repository is assembled, we get the kind of definition that Martin Fowler used to describe it, which undeniably looks like a single unified thing:
Mediates between the domain and data mapping layers using a collection-like
interface for accessing domain objects.
So it mediates between layers using a collection-like interface. That’s one responsibility and everything else is an implementation detail which might be delegated to other classes in other places.
Context Matters
If we look at Repository in a different context, suddenly the same exact pattern becomes a clear violation of the SRP. Consider the case of a CQRS kind of architecture. In CQRS, as a refresher for readers who might not be familiar with the idea, we separate our commands (Insert, Update, Delete) from our queries (Load, Find, LoadAll). Ideally, this separation would be from top to bottom in the application: Separate interfaces at the top-level (or, at least, at the Service Layer) all the way down to separate data stores. We might have one master database which accepts writes, and several read-only slave databases for our reads. Notice also that the schemas of the two databases might actually be different, to satisfy the different needs of the consumers. The read-only slaves may opt for a heavily denormalized storage structure which is easier to query.
In this CQRS setup then, the Command stack would use a database interface which interfaces only with the master DB and the Query stack would use a separate database interface which interfaces with the read-only slaves. We could still use Repository patterns in both places, because we would still like to have a collection-like interface over the data layers. We would probably end up with two separate Repository types, because we have two separate responsibilities: One to do normal CRUD operations on the Master DB, and one to read data from the Slave DBs. Making a change to one side of the stack would not require us to alter logic in the other side. Even though our two repository types are nominally acting on the same conceptual data, we can’t combine them into a single class because they have two separate responsibilities.
(Arguably, the query side of the stack might opt for something more streamlined than a repository because the normal Insert/Update/Delete methods that are part of a typical repository interface wouldn’t be used in this case.)
Different Formulations of the Principle
One way to think about the SRP is this: Describe your class in plain language. If you use the words “and” or “or” in the description, you probably need to break it up into smaller classes. This can work but, as shown in our repository example above, the way we describe the class depends on the level at which it is viewed, and the context in which it lives. (Also, using “and” to show which two layers it mediates between doesn’t count, making this formulation even more problematic).
I’ve heard the SRP described as The class should only have one reason to change, but then the wise-asses among you will immediately retort with “There is only one reason to change: if the code doesn’t meet the requirements!” Not meeting the requirements is the one and only reason why anything changes, therefore your entire application can be stuffed into one super big class. This is clearly not the intention of the SRP.
(Other wise-asses among you may note that a certain reading of the the Open/Closed Principle might preclude any changes to a class in favor of extending through new subclasses, meaning that the SRP is null and void. Don’t think like this.)
I’ve also heard the SRP described as this: There should only be a single requirement which, when changed, will cause your class to change. But that seems kind of vague and seems to open the door to the idea that the granularity with which programmers write the code is dependent on the granularity of the requirements written by the analysts. A pretty big portion of your design suddenly is out of the hands of the people implementing it, and is put into the hands of people who aren’t considering the architecture of the code at all! In this case we have to start asking what the single point of truth is: Are the analysts secretly in charge of your application architecture, or are your programmers supposed to review and re-write all your requirements? Either way, there are clear problems here.
One other way to describe the SRP is Do one thing and do it well, which is decent enough but immediately begs the question “what is a ‘thing’?”. Taking this to the logical extreme can lead to the idea of lots of little classes with one method each:
…and this is clearly not what we want either. I’m not saying that we can’t or shouldn’t use the Command pattern where appropriate, only that if we’re writing this kind of code we can drop the pretense of object-orientation entirely and use a simpler, procedural language instead.
SRP According to Uncle Bob
Robert C Martin, who originally formulated the SRP in his work Agile Software Development, Principles, Patterns, and Practices, has said that “Responsibilities are People” in the sense that they tend to map to people with individual roles. For example, an accountant person might need certain accounting reports, where a manager might need certain reports about people and resources on the team. Even where the logic for these two reports might overlap, we would still keep them implemented as separate classes, because the people who need them are different and therefore the reasons why those report classes may change are also different. If we are aggressive about sharing code between the Accounting and Managerial reports and then one of those two people requests a change to one but not the other, we end up in quite a mess. Two pieces of code which may look the same, but which serve different masters, are repeated and therefore cannot and should not be combined. At least, not directly. As an example, let’s consider two classes, a ManagerReport and an AccountantReport, which both have a method that iterates over a list of Employees and totals up their salaries:
We might be tempted to immediately combine these methods and indeed these classes to maximize code sharing:
We can be quite proud of ourselves at this efficient reuse of code until the accountant comes back and says “I need total yearly pay for all employees, even hourly ones”. Now what do we do? We either have to take apart our entire Report inheritance hierarchy or else we need to override the method in one place. Now we have an inheritance hierarchy which is completely unnecessary and which does nothing but complicate our system.
Going back to the very beginning, we have two options:
- We can keep two classes, with method implementations which look identical, but secretly aren’t, because they serve different purposes for different people or roles (and these differences may not be clearly expressed in the code because they have to do with cultural aspects of our organization).
- We can delegate out the logic which looks shared into some kind of helper class, and easily de-duplicate later:
Now when we need to make a change to the AccountantReport, we can change that implementation without needing to worry at all about hierarchies or affecting the ManagerReport at the same time.
I’m not saying this is the best way to do it, I’m just pointing it out as an option which initially allows code sharing but also allows us to make modifications in the spirit of the SRP without worrying about collateral damage.
One other concern I do have about the “responsibilities are people” idea is that we might fall into the trap of having a single God Object per person: An AccountantStuff object that does everything and anything that the accounting department might want, and a ManagerStuff object that does everything and anything a Manager may want. It might be better to view a responsibility as a combination of person and task. For example the Manager might need information about team salary totals when he’s in the Big Budget Meeting, but he might need information about schedules and vacation time in his Project Planning Meeting. Those two contexts, “Manager In Big Budget Meeting” and “Manager In Project Planning Meeting” require at least two classes (and possibly more if generating, formatting and printing tasks need to be delegated out elsewhere).
The idea of “responsibilities are people” is definitely one dimension to consider when thinking about the SRP, but it certainly cant’t be the only one.
Inverting
Let’s look at the SRP from the other side. Instead of asking “what do I need to do to implement the SRP correctly” let’s try asking “If implemented correctly, what results would we expect the SRP to produce?”
However we define it, assuming our code follows the SRP correctly, what benefits should we get? Here’s at least a partial list:
- When a change needs to be made, it should be possible to unambiguously identify a single place in your code to do it.
- Classes should be easy to understand because they only do one thing and that one thing should be obvious.
- The purpose of the class should be easy to explain.
- If all classes do only one thing each, and those things are well understood, workflows composed of such classes should be easy to understand.
- Operations on objects, including changing data and executing methods, should not produce unintended side-effects or changes to unrelated data and behavior.
- Classes should be able to be reused in new workflows, because the class is not tied to any one particular workflow.
- New workflows should be able to be constructed by composing together classes from existing workflows. That is, if the class “does one thing”, and you need that one thing, you should be able to use that class for that purpose.
- It should be obvious how and when to test the class.
Some of these ideas, especially 6 and 7 do start to blur the lines a little bit and bring in other ideas besides pure SRP. But, my thinking goes like this: if you have a class which implements an operation and you have a consumer which needs to utilize that operation, you should be able to use that class with that consumer. If you cannot, it’s probably because your class does two things: It performs it’s intended operation and it adapts that operation to a specific workflow. If you can tease apart the general implementation from the specific interface, you can make that class more focused and thus more reusable.
I like to think about the SRP as being kissing cousins with DRY: Your class should only have one responsibility, and your program should only implement that responsibility in that one class. Each given idea exists in one and only one place.
Rediculous Example
You need to get your car cleaned, so you take your Car object to your CarWash
object and call the method car.PayForCarWash()
. But that’s clearly stupid,
the car is just a means of transportation. Even though you pay for the wash
while still sitting in the driver’s seat doesn’t mean it’s a method on the car.
The person pays for the wash, so we move that method to
person.PayForCarWash()
.
But that’s kind of stupid too. If I then go out and buy groceries, and pick up
my drycleaning, and grab some diapers, does that mean I need methods
person.PayForGroceries()
and person.PayForDryCleaning()
and
person.PayForDiapers()
? Certainly not. We might have a single payment method
and we might move that onto a wallet object.
(Yes, I know, the wallet is inanimate and doesn’t actually pay. The person is the actor who reaches into the wallet and obtains a method of payment. This doesn’t change the conceptual idea that it is the job of the wallet to hold or “manage” your money, and the job of the person to carry a wallet. I’ve been in line at the grocery store without my wallet before, and I can tell you that no amount of me having the agency to obtain money from a wallet means I can do so if my wallet is sitting on the table back home.)
Now our method call looks like this:
The car has a driver, the driver has a wallet, the wallet can pay any price to any recipient, and the carwash keeps track of it’s own pricing. Every object in our graph has one responsibility, and when they come together, the result is clear and easy to understand. To illustrate, let’s see how this system might change in response to adding new requirements:
- There are different ways to pay for something, and not every business accepts every method of payment. The carwash needs to change to expose a list of accepted payment methods, the wallet needs to change to expose a list of currently available payment methods. Neither the person nor the car should need to change at all.
- It costs more to wash a tractor-trailer than it does to wash a car. The car object should expose a “vehicle type” property to tell what kind of vehicle it is, and the carwash should ask for the vehicle type when determining the price.
- If the driver doesn’t have any money she can ask one of the passengers, starting with whoever is riding shotgun (it’s as much a privilege as a responsibility). Also, if the driver is owed money because she covered lunch, one of the passengers may be obligated to pay for the wash. The car changes to have multiple passengers, not just the driver. Some sort of “social convention” object would keep track of outstanding debts and also create priority list of payors, starting with driver and shotgun, then moving to the poor saps in the back.
In each of these three cases, whenever something needs to be change, it should be possible to quickly determine where that change needs to be made. There may be other options in these cases, depending on the other system requirements which I have not explicitly written here.
Use Your Intuition
So there are different ways to describe and approach the SRP. My point with this post is to demonstrate how the SRP seems quite simple, but when you start trying to define it clearly and concisely, you find that most definitions are lacking. This doesn’t mean that the SRP is unknowable or unworkable, only that it’s worth considering both how you pursue it and what you hope to get from that pursuit. Taken in concert together, all of these ideas along with others I didn’t mention, intuition and experience can help to produce great code.
I believe that writing good software is an iterative process, and one with few absolutes in terms of correct style or design. The Perl folks are quite fond of saying “There’s more than one way to do it”, which really is an apt mantra when it comes to architectural decisions like this. You should be able to look at a class and determine what might cause the class to change. Then, if you feel like the list has more than one non-trivial item (where a trivial item might be “the public interface of a dependency changes, requiring my class to change the way it interacts with that dependency”) you might want to consider either viewing the class from a higher level of abstraction or else breaking it up into smaller pieces.