When the Single Responsibility Principle is taught among developers, one aspect – the responsibility – is harped on the most. But what counts as a responsibility of a class or a method? Is it the concepts it touches? The number of classes it uses? The number of methods it calls?
While each of the above questions are very good questions to ask of your method, there is an easier way given right in the original explanation – a responsibility is a reason to change. And it turns out that we can use something more than just code to determine that and help guide us to write good code.
As with many programming topics, code is the best place to start. Let’s look at a basic class in Ruby:
class ReportPrinter def print_report records = ReportRecords.all puts 'Records Report' puts '(printed #{DateTime.now.to_s})' puts '-----------------------------------------' records.each do |record| puts 'Title: #{record.title}' puts ' Amount: #{record.total}' puts ' Total Participants: #{record.total_count}' end puts '-----------------------------------------' puts 'Copyright FooBar Corp, 2012' end end
How many reasons could the method in this class change for?
- We need to change where we get records from
- We want to print different information about a record
- New records have fields other records don’t have (conditional logic)
- We want to output to a different format
- We want to make sure line endings are set correctly
- We need to change the report title
- We want to change where the date is printed
- We want to change the separators
- We want to change the footer
- We need to print the report in a different language
10 lines of value add code. 10 (at least) reasons to change. Now, let’s compare that code to this version:
class ReportPrinter def print_report records = load_records header separator records(records) separator footer end def load_records records = ReportRecords.all end def header puts 'Records Report' puts '(printed #{DateTime.now.to_s})' end def separator puts '-----------------------------------------' end def records(recs) recs.each do |record| puts 'Title: #{record.title}' puts ' Amount: #{record.total}' puts ' Total Participants: #{record.total_count}' end end def footer puts 'Copyright FooBar Corp, 2012' end end
The first thing that should strike you is that this is exactly the same code. Yet, this class is better code because each method has a single responsibility – header prints the header, footer prints the footer, etc. We could continue the extractions by pulling out the duplication of puts into a writer method, and then dynamically swap that in.
But, I want to focus on the print_report method for a minute. It seems like there are lots of reasons for it to change – it does an awful lot. However, it has an important job – one that I will title a ‘Sergeant Method’ since that’s the name I got from Alan Shalloway and Scott Bain. To understand its responsibility, let’s step back and look at a way of defining ways of modeling software from Martin Fowler’s UML Distilled. Fowler discusses three levels of modeling:
- Conceptual
- Specification
- Implementation
Coupling should happen at the Conceptual level, and never should there be coupling between the Conceptual and Implementation levels. The way I explain these levels is that the Conceptual level is the container that holds the concepts. The Specification describes what should be implemented, and the Implementation level how it should be implemented (see John Daniels’ Modeling with a Sense of Purpose for more information).
With this in our mind, we can see that the print_report method has the responsibility of defining the specification of what it means to print a report. In other words, it gives us the algorithm for printing a report, and only needs to change if the algorithm changes. We are free to implement that in any way we choose without having to change the print_report method.
With this terminology, we can now look at the records method and be able to define what smells about it. It is operating at two levels – a specification level (loop over a set of records and print information about each one) and an implementation level (print the title, total and count). We could move the loop up to the print_records method, but that would be a true violation of Single Responsibility – it needs to not only know the order of operations, but how to loop over a collection. It’s better to have two methods:
def records(recs) recs.each do |record| print_record(record) end end def print_record(record) puts 'Title: #{record.title}' puts ' Amount: #{record.total}' puts ' Total Participants: #{record.total_count}' end
Which are now both operating at the correct level.
Sharpening your eyes to look for both increases in the reasons a class can change and the level of abstraction the class or method is operating at will open a whole new world of identifying code smells, and understanding where to put in divisions to your code to make it easier to grow and scale your code base.
1 thought on “Thinking Differently About the Single Responsibility Principle”
Comments are closed.