Thinking Differently About the Single Responsibility Principle

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 Robert Martin’s 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 “Seargent 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” http://www.syntropy.co.uk/papers/modelingwithpurpose.pdf 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.

Cory Foy was an 8th Light Software Craftsman.