It’s equally important and mysterious: the Single Responsibility Principle (SRP). And even though its originator Robert C. Martin tried to explain it again, I feel there is something lacking. Some concreteness, some tangibility.
First the definition of the SRP in Martin’s own words:
“The Single Responsibility Principle (SRP) states that each software module should have one and only one reason to change.”
This is more informative than Wikipedia’s tautology:
“In object-oriented programming, the single responsibility principle states that every class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class.”
But still… Martin’s definition is not crystal clear. It leaves much room for interpretation during TDD refactoring or code reviews. Even he admits this:
“However it begs the question: What defines a reason to change?”
To improve on this, he developed the notion of a “responsibility owner” as I’d call it:
“When you write a software module, you want to make sure that when changes are requested, those changes can only originate from a single person, or rather, a single tightly coupled group of people representing a single narrowly defined business function.”
That’s a clever idea - but it’s only useful for pretty coarse grained responsibilities. Take an online shop for example. I assume we can agree on such a shop having a couple of distinct parts like catalog, shopping cart, recommendation engine, order processing, billing etc.
For each of those parts there probably is a person (stakeholder) owning it, i.e. having a clear vision of how this part of the shop should behave and look like. According to Robert C. Martin this would mean, I guess, each functionality should be put into a separate module.
Who would disagree?
But that’s not really a difficult decision. Yet.
Now, what about a small company ordering such an online shop. There is only a single person responsible for all the parts. Would that mean everything has to go into just one module? Certainly not. Because what Robert C. Martin probably means it not person, but role.
Even if there is only a single responsible person for so many different parts of the software, this person views the whole from different perspectives. Mentally she switches between different roles like accounting, sales, marketing, support, even customer.
Does that help defining what a “reason to change” is? The SRP could be rephrased like this:
Separate code into modules so that each module is owned by only a single business role.
This is not bad. But still it breaks down once you zoom in on such a module. How to apply the SRP within (!) the catalog module? The whole catalog is owned by the sales role. But since it does not seem realistic to implement the whole catalog in a single function or even class, there is no guidance from the owning role as to how to form smaller modules.
I’m sorry, but even this augmented definition of the SRP does not seem to help much. My guess is, we need to look at the matter a bit more systematically.
First let’s be clear about what responsibilities are: If a module has a single or even multiple responsibilities, then that means it contributes to the fulfillment of requirements. A module can be responsible for some functional requirement and/or some non-functional efficiency requirement (e.g. performance, scalability).
Contribution to the fulfillment of such requirements means, the logic in a module produces a part of the desired behavior. Because that’s what software is all about: behavior. Customers want software to behave in a certain way, e.g. show the products in a certain category when clicking on an image (functional requirement) or display a query result in less than 1 second (efficiency requirement).
Behavior is triggered by some input from a user. It consists of output to the user and/or side effects (e.g. changes to a database or printing a document).
Behavior is produced solely by logic, which to me are transformational statements (expressions), control statements (e.g. if, switch, for, repeat), and API calls/hardware access.
That’s it. That’s what customers want. Nothing more, nothing less. Which means, they don’t want modules. Not a single one.
To wrap logic up into modules does not contribute to the behavior of software. So why do it? Why should we wreck our brains about whether a module has already just a single responsibility or not?
We need modules for the non-functional requirements of evolvability and productivity. That’s their sole purpose. Modules don’t contribute to functionality or efficiency; they only make it easier to keep adding features to the code and fixing bugs.
Customers want that, too. Unfortunately they take it for granted. And we developers have a hard time to explain why keeping evolvability high is not that easy. Especially because there is no hard-and-fast metric for evolvability.
So there are now orthogonal responsibilities: the responsibility with regard to functional and efficiency requirements - I call that logic responsibility -, and the responsibility with regard to evolvability.
Following the SRP thus means to identify some logic responsibility and wrap it up into a module to fulfill the evolvability responsibility.
From the above example it should be obvious there is not just a single level of responsibilities. Responsibilities can be coarse grained or fine grained.
A module representing the catalog functionality of an online shop has a pretty broad, but still single responsibility.
But this functionality consists of different sub-functionalities and efficiencies. I imagine there to be maybe a query engine and some price calculation and a cache and several other parts to play together in order to give users a “catalog experience”. And although they are all owned by one role they should not go into the same module, I’d say.
That means a hierarchy of requirements has to be mapped onto a hierarchy of modules. But should those be all of the same kind, e.g. classes? That would dilute the hierarchy. It would not be visible in the code base anymore, at least not that easily. There would be just a bunch of classes (for query engine, calculation etc.), of which maybe one would be conceptually on a higher level (for the overall catalog).
That would work, but I suggest we broaden our view of modules. Let’s think of modules forming a hierarchy of their own.
In his first version of the SRP Robert C. Martin spoke of classes. In the updated version he speaks of modules - but leaves it to the reader to figure out what modules are. I think that doesn’t make it easier to understand what the SRP means.
So here is my take on what modules are:
Modules are design time code containers to enable evolvability and productivity.
This might sound a bit lofty, but in fact it’s pretty simple. To make it more concrete here is my hierarchy of modules from fine grained to coarse grained:
A level n module can be said to physically contain level one or more n–1 modules. Of course each level has its own characteristics. It brings something special to the table: Functions abstract logic behind a name, classes encapsulate details about data and functions, libraries are true black boxes, components separate contract from implementation, and (µ)Services sport platform neutral contracts.
Now think of it: The sole purpose of all these containers is to make it easier for your to maintain your code. They are used to encapsulate details, they save time by letting you re-use them, they decouple through contracts…
None of these containers was invented to improve functionality or performance or scalability or security.
Even µServices, the newest kind of module, are not a matter of scalability or security. They are supposed to improve evolvability at design time and runtime and make polyglot development easier.
With such a physical hierarchy of modules it’s easier to map a hierarchy of required behavior to code. For example the catalog could become a (µ)Service. Then the query engine and the calculation could become components within this (µ)Service. And the even smaller behavioral parts could become libraries and classes and functions. All nested within each other like russian dolls.
With a physical hierarchy of modules this can be done. It helps to map the reality of hierarchical requirements. But of course even this comes to an end. There might be more behavioral levels in the requirements than levels of modules. So in the end modules of the same level need to be arranged in some kind of logical hierarchy.
Anyway, each module on each level is supposed to have a single responsibility. But if there are maybe 5 components in a software system with 3 libraries each with 10 classes with 10 functions each… that would amount to 1,500 modules. And I don’t think you can or want to find 1,500 persons (or roles) out there who own these modules.
Such a simple calculation makes it obvious: The notion of “responsibility equals owner” is only helpful for very coarse grained modules. Don’t neglect it, but don’t put all your responsibility eggs in this basket.
The field the SRP targets now has more structure, it has become more detailed. But my feeling is, we’re still close to square one. Robert C. Martin’s question has not been answered yet:
“What defines a reason to change?”
Which logic to put into this function and not into that one? Which functions to split among two classes instead of keeping them in just one? Which classes to group in a library? Why split a libary into three? etc. That’s the questions we have to ask during design/refactoring all the time.
Let me try to move closer to an answer by introducing the notion of an aspect. The term is not new to software development, but I want to use it in a more general way than AOP (Aspect Oriented Programming) does.
Every module has a purpose. In some way it contributes to the overall behavior of a software. The more it focuses on a certain aspect of the behavior, the better. Then it has a clear responsibility.
A module’s responsibility is to implement an aspect of requirements.
An aspect can be functional or non-functional. Aspects form a hierarchy, they can be on different levels of abstraction.
The above mentioned catalog, shopping cart etc. are aspects of the overall solution. They are easy to identify, but below that…? They are just examples. So what’s the definition of aspect? Mine is:
An aspect is a set of traits on a common level of abstraction which are likely to change together.
This might sound complicated, but in fact we deal with aspects every day. Think of a person. What are aspects of a person?
I’d say for example hairdo and clothes are aspects of a person. Each stands for a set of traits, and each trait can take on one of several values, e.g.
- Hairdo: hair color (black, blond, red…), hair length (long, short, bald…), hair style (curly, lank, pony tail…) and more.
- Clothes: color (black, brown, purple…), style (casual, festive, rugged…), fabric (denim, silk, linnen…) and more.
What binds traits together to form an aspect, what makes up their high cohesion is how they change. We view traits as cohesive when they are affected by the same causes which means they tend to change at the same time.
It’s more likely to change the hair length at the same time as the hair color and the hair style. This happens when you go to a hair dresser. The synchronicity is high, the frequency every couple of weeks. The cause might be you want to look nicer for a party or because bringing your hair in order every morning has become exhausting ;-)
It’s also more likely to change the color of your clothes at the same time as the style and the fabric. This happens when you dress yourself in the morning or dress up to go to the opera. The synchronicity is high, the frequency a couple of times per day. The cause might be you want to feel more comfortable or more appropriate for the occasion.
On the other hand it’s less likely to change your hair style and at the same time also change the color of your clothes. At least it’s that way for the people I know ;-) Which means: If there are circumstances which lead to changing hair style and clothes color more or less together, if there is a common cause for both, then… well, then it’s probably useful to view them as an aspect.
This kind of abstraction we do every day. There are thousands of already agreed upon common aspects.
But when you are confronted with software requirements… then there are no aspects at first. It’s just a vast land of traits. It’s tons of details without abstractions. That’s why the SRP is at the core of our profession. We’re aspect hunters.
Here’s the analogy again:
First there is just a bunch of traits: hair color, clothes color, hair length, fabric etc.
Then we see those traits change their values: hair color goes from black to blond, hair length goes from long to short - but clothes color stays the same.
Then we realize there are patterns in those changes.
Then we abstract from the traits. We group them together according to the observed (or imagined) change patterns (which represent causes). Aspects are born.
In a myriad of real (or anticipated) changes to traits we need to spot patterns and then abstract. Aspect hunters are masters of abstraction. That’s what the SRP is about.
Sure, ultimately all changes originate from people. That’s why Robert C. Martin says:
“[The SRP] is about people.”
But this is only helpful at the beginning. It’s a shortcut to abstraction. We don’t want to wait for changes to happen before we venture into abstraction. We want the abstractions to be served on a silver platter.
There is nothing wrong with shortcuts. But we should recognize them as such - an be prepared to go beyond them.
Hunting for aspects top-down
Spotting cohesion is no easy feat. It becomes easier with experience, though. Fortunately. But still, it’s tough. So we should look for help.
Robert C. Martin provides help when he recommends to look to stakeholders and their roles to find aspects that are worth to be represented by a module. Such roles as owners of a coarse grained aspect mostly help to structure the domain of a software into coarse grained modules.
Take the online shop for example again. There will be a person most interested in the presentation of the products, and a person responsible for the pricing, and a person responsible for order processing, and a person for billing etc. What should you make of them?
Since that’s pretty coarse grained aspects of the problem domain I find it natural to derive from this high level modules, e.g. (µ)Services or components. A catalog service or pricing component would make sense to me, but probably not a class to represent the whole aspect of price calculation.
This is an example of finding aspects top-down. You start with a business role as the root and drill down into to find requirements aspects.
When you look at patterns like the layered architecture or Ports-and-Adapters or Clean Architecture you find more examples of top-down aspect determination.
These patterns are congealed experience telling us: There are aspects common to all software regardless of the domain. Let’s call them cross-cutting aspects as opposed to domain aspects.
For example, interacting with the user is an aspect to be separated from persisting data or communicating with other software. These should be wrapped into modules. But of what size, on what level of the module hierarchy?
I think, such aspects should be encapsulated by at least a library; to view them as black boxes helps testing and exchanging them to keep up with technological development. These aspects form the interface of a software.
But there is more: Robert C. Martin zooms in on what’s hidden behind this interface. That’s use cases and entities to name just two aspects which are less tangible.
Whereas interface aspects can be identified pretty easily by looking at hardware or APIs, domain aspects are harder to spot. But one thing should be clear: Activities are not the same as data. That’s why use cases should be viewed as aspects of their own separted from the entities they work on.
And to represent those aspects classes seem to be the module level of choice.
How to find such “soft” aspects? Talk to the business roles, understand their domain. Certainly each role will give you ideas for lots of different such aspects. But that does not mean they all belong in just one module. Split them up!
And be open to the possibility that the role won’t be able to lead you to all of them. As your understanding of a domain evolves you will see patterns of change in traits - and will realize there are more and the existing aspects should be cut differently. That’s what Eric Evans calls “refactoring to deeper insight”.
In addition to the interface and domain aspects I’d like to point out three more aspects which to me are orthogonal to the ones presented so far:
I think they are so fundamental, they even come before the other aspects. They are the main shearing layers because they “evolve in different timescales”.
Thinking about user interface or persistence or use cases already is pretty specific. It’s about behavioral aspects.
But behavior needs to be coordinated. And behavior needs to be fed with material to work on. To me this distinction is universal. It comes before any of the above mentioned architectural patterns.
Behavior is produced by logic. Logic is encapsulated by modules I call operations. Operations can do some business calculation, query a database, encrypt data, draw a graph or whatever.
Operation is different from data structures. Sorry, OO-folks ;-) Yes, I think for the past 25 years too much data has been entwined with too much logic. We need to start separating data from logic. Data is a very different aspect compared to behavior. Data is about structure, behavior is about process.
And then all the operations need to be “orchestrated”, put into the right order to produce an overall behavior. I call that integration. And it also is a distinct aspect different from data and operations.
Think about a company: There are documents (data) to be processed by clerks (operation) who are overseen by managers (integration). That’s even the case in software development. A product owner produces user stories (data) to be transformed into code (data) by developers (operation). And the whole process is overseen by a Scrum master (integration).
And it’s an anti-pattern if managers (or leaders) try to do the work of the employees they manage. Remember micro-management? ;-)
In code this happens if an integration module does not just “wire-up” other modules to form a process, but also contains logic to add itself to the behavior.
How to translate these universal aspects? Functions and classes are the starting point for me. Focus them on one of these aspects. Let the distinction between these aspects be the fundamental organizing principle for higher level modules/aspects.
Abstracting aspects bottom-up
So much for finding aspects top-down. But there are only so many you can identify this way. But we need to spot more, many more. Because in the end, each function we write should have just a single responsibility. Because a function is a module, the smalles module there is.
That’s where bottom-up aspect hunting starts.
It’s hard to give a definition of an aspect a priori, but it’s comparatively easy to see it as a pattern once there are traits on the table. It’s like the difference between an artist and a spectator. I find a difficult to create a good painting; whereas “good” just means “I like it” ;-) But it’s easy to recognize one when I see it.
So don’t start with the usual object oriented dance around higher level modules like classes - except for the obvious ones. Don’t play the CRC card game.
Instead start from the bottom up. Start with functions. Which means: Start with genuine behavior. All else will follow from there.
Pick some behavior requested by the customer and decompose it. What are the processing steps necessary to transform input into output plus side effects? Then repeat this for each processing step found - until the leafs of this decomposition tree become so small, their implementation is obvious to you.
The processing steps you identify form flows, data flows. Integrating them into a whole is declarative programming, no logic involved. Only the leafs contain logic and only logic (operations). I call this approach “radical object-orientation” or “OOP as if you meant it”. The result of such behavior decomposition are “objects” as Alan Kay thought about them: logic encapsulated in a way so that it only communicates with the outside through messages.
Anyway, what you get are many functions “without a home”. You are at the bottom of abstraction. Now build up from this higher level modules. Take each function as a trait. Which traits seem to be cohesive? Shared state or usage of the same API are signs of cohesion. Likewise focus on different requirements like functionality vs. efficiency. Or within efficiency performance vs. security vs. scalability. Or within functionality calculation vs. transformation or command vs. query. Or of course: integration vs. operation.
Slice and dice the functions (processing steps) in your decomposition hierarchy like you see fit according to observed patterns. First group them into classes. Then group classes into libraries. If you can nest classes in your language, use it to group classes within classes to form ever more coarse grained aspects. The same holds for functions.
But be careful! Within an aspect the traits should be on roughly the same level of abstraction. Abstracting aspect clothes from color and style is fine. But clothes color and collar form seem to be traits on different levels.
This is were experience is needed. Beyond hard and fast rules like suggested above there is a large area where the cohesion between traits and their level of abstraction need to be explored. Try to group them this way, then that way, and feel what makes more sense. Don’t try to avoid “refactoring to deeper insight” later on. Often you can’t be right the first time.
The one reason to change
So where are we after all of this? What about the “one and only one reason to change” which is the hallmark of a module’s responsibility?
You have to learn to spot traits in your code. Like read, write, command, query, calculate, store, data, behavior, encryption, integration, operation, caching, communication, persistence, catalog, billing, logging etc. etc. First see those traits, coarse grained and fine grained, functional as well as non-functional, domain specific vs. cross-cutting.
And then… compare them. Compare them with regard to what causes them to change. And if they have a common cause compare them with regard to how likely it is they change at the same time.
If two traits have a common cause for change and are likely to change at the same time then encapsulate them in a module on an appropriate level. You have defined an aspect and the module’s single responsibility is to implement it.
Learn to do this bottom-up. But don’t set recommendations for common aspects at naught. You find them in patterns and architectural models. Then you can hunt for aspects top-down.