Severin Perez

Writing Flexible Code with the Single Responsibility Principle

September 07, 2018

If you’ve been around software for a while, then you’ve almost certainly heard of the SOLID principles. In short, these are a set of principles intended to help developers write clean, well-structured, and easily-maintainable code. In software, as in any intellectual and creative endeavor, there is quite a bit of debate about the “right” way to do things. Different practitioners have different ideas about what is “right”, depending on their individual experiences and inclinations. However, the ideas prescribed by SOLID adherents have been widely adopted in the software community, and agree with them or not, they’re a useful set of principles from which to draw best practices. Moreover, SOLID has been thoroughly integrated into a broader set of Agile development practices and understanding them is thus a virtual requirement in the modern software industry.

Developers and bloggers have written seemingly ad infinitum about SOLID in various places across the web. In researching this article, I encountered many such resources, some of which are cited at the bottom of this article for your reference. So, if the SOLID principles are well-covered elsewhere, why write yet another article about them? In short, for my own edification. Writing about complex topics is one of the best ways to learn them yourself. And for that reason, I am planning a series of five articles—one on each of the SOLID principles. What follows is the first such article and focuses on the single responsibility principle. Although I don’t expect my addition to the corpus on this topic will be particularly unique, I hope that it will prove useful to some readers. And with that, let’s dive in.

A Quick Refresher on SOLID

SOLID is an acronym for a set of five software development principles, which if followed, are intended to help developers create flexible and clean code. The five principles are:

  1. The Single Responsibility Principle—Classes should have a single responsibility and thus only a single reason to change.
  2. The Open/Closed Principle—Classes and other entities should be open for extension but closed for modification.
  3. The Liskov Substitution Principle—Objects should be replaceable by their subtypes.
  4. The Interface Segregation Principle—Interfaces should be client specific rather than general.
  5. The Dependency Inversion Principle—Depend on abstractions rather than concretions.

The Single Responsibility Principle

The single responsibility principle (SRP) states that every class or module in a program should have responsibility for just a single piece of that program’s functionality. Further, the elements of that responsibility should be encapsulated by the responsible class rather than spread out in unrelated classes. The developer and chief evangelist of the SRP, Robert C. Martin, describes a responsibility as a “reason to change.” So, another way of putting the SRP is to say, as Martin does, that “a class should have only one reason to change.”

Before going any further it’s worth taking a look at the history of the SRP. Martin originally introduced the term in as part of his Principles of Object Oriented Design [1]. According to Martin, the SRP has origins in Tom Demarco’s idea of cohesion, which describes the extent to which elements in a given class/module are related and relevant to one another. Furthermore, it builds on David Parnas’ description of encapsulation, or information hiding, which says that attributes and behavior relevant to a given object should be bundled together and hidden from outside access [2]. Taken together, these ideas lead naturally to the principle that a given piece of software functionality (aka, a responsibility) should be bundled into a single class and hidden from other elements of the program—exposing only those pieces necessary to the functionality of the program as a whole.

On its face, this seems relatively straightforward. Individual pieces of a program’s functionality should be distributed to distinct entities that are capable of handling them without outside assistance. But how do you define an “individual piece” of a program? What, exactly, is a “responsibility” and how do you reason about it from a business perspective? Martin, popularly known as “Uncle Bob,” clarified just this concern in a 2014 blog article where he tied “responsibility” to the idea of interested actors [3]. Martin’s article is well worth a read, but to summarize, he argues that if a piece of software has several different kinds of users (aka, actors), then the disparate interests of each of those users defines a piece of that software’s responsibilities. Martin uses the example of C-Suite executives (COO, CTO, CFO), each of whom uses some piece of business software for different reasons. Moreover, when considering how software should be changed, each of those actors should be able to dictate changes in the software without affecting the interests of the other actors.

The “God Object”

Per usual, probably the best way to learn about the SRP is to see it in action. But to do that, we should perhaps first see what a program looks like when it does not adhere to the SRP. Let’s take a look at a simple program and see if we can break down its responsibilities. What follows is a brief Ruby program that outlines a class that describes the behaviors and attributes of space stations. Read through it and see if you can identify: a) the various responsibilities of objects instantiated by the SpaceStation class; and, b) the types of actors who might have an interest in an a space station’s activities.

class SpaceStation
  def initialize
    @supplies = {}
    @fuel = 0
  end
  
  def run_sensors
    puts "----- Sensor Action -----"
    puts "Running sensors!"
  end
  
  def load_supplies(type, quantity)
    puts "----- Supply Action -----"
    puts "Loading #{quantity} units of #{type} in the supply hold."
    
    if @supplies[type]
      @supplies[type] += quantity
    else
      @supplies[type] = quantity
    end
  end
  
  def use_supplies(type, quantity)
    puts "----- Supply Action -----"
    if @supplies[type] != nil && @supplies[type] > quantity
      puts "Using #{quantity} of #{type} from the supply hold."
      @supplies[type] -= quantity
    else
      puts "Supply Error: Insufficient #{type} in the supply hold."
    end
  end
  
  def report_supplies
    puts "----- Supply Report -----"
    if @supplies.keys.length > 0
      @supplies.each do |type, quantity|
        puts "#{type} avalilable: #{quantity} units"
      end
    else
      puts "Supply hold is empty."
    end
  end
  
  def load_fuel(quantity)
    puts "----- Fuel Action -----"
    puts "Loading #{quantity} units of fuel in the tank."
    @fuel += quantity
  end
  
  def report_fuel
    puts "----- Fuel Report -----"
    puts "#{@fuel} units of fuel available."
  end
  
  def activate_thrusters
    puts "----- Thruster Action -----"
    if @fuel >= 10
      puts "Thrusting action successful."
      @fuel -= 10
    else
      puts "Thruster Error: Insufficient fuel available."
    end
  end
end

Admittedly, our space stations aren’t particularly capable (I guess NASA won’t be calling on me any time soon); however, there is still quite a bit to unpack here. Immediately we can see that the SpaceStation class has several disparate responsibilities. Roughly, we might say that space station operations can be broken down into four areas: sensors; supplies; fuel; and, thrusters. Although personnel are not specified in the class, we can easily imagine different actors who might care about these operational areas. Perhaps a scientist who manages the sensors, a logistical officer who handles supplies, an engineer who manages fuel, and a pilot who manages the thrusters. Given this variety of different operational areas and interested actors, might we say that this class is violating the SRP? Absolutely.

Currently, our SpaceStation class is a classic example of a so-called “God object”—that is, an object that knows about and does everything. This is a major anti-pattern in object-oriented programming and should be avoided. But why? What’s wrong with a “God object”? Well, for starters, such objects are extremely hard to maintain. Our program is very simple right now but imagine what would happen if we added in some new functionality. Maybe our space station will need crew quarters, or a medical area, or a communications bay. As we added in such functionality, the SpaceStation class would grow to immense size. Worse yet, each piece of functionality would be inextricably tied to all the others. If we want to change how the fuel tank is managed we might inadvertently break thruster operations. If the station scientist requests changes to sensor operations, those changes could have trickle-down effects to the communications bay.

Violating the SRP may be convenient in the beginning but the short-term benefits are not worth the long-term maintenance costs. Not only do we have to worry about how changes in one place affect another (due to our failure to separate concerns), but the code itself becomes unwieldy and unpleasant to deal with. Breaking program functionality down into encapsulated pieces is a much better option. Given that, let’s make some changes to our SpaceStation class.

Breaking Down Responsibilities

Earlier, we identified four rough areas of operation that our SpaceStation class was managing. Those seem like a good place to start as we refactor our code to be more in line with the SRP.

class SpaceStation
  attr_reader :sensors, :supply_hold, :fuel_tank, :thrusters
  
  def initialize
    @supply_hold = SupplyHold.new
    @sensors = Sensors.new
    @fuel_tank = FuelTank.new
    @thrusters = Thrusters.new(@fuel_tank)
  end
end

class Sensors
  def run_sensors
    puts "----- Sensor Action -----"
    puts "Running sensors!"
  end
end

class SupplyHold
  attr_accessor :supplies
  
  def initialize
    @supplies = {}
  end
  
  def load_supplies(type, quantity)
    puts "----- Supply Action -----"
    puts "Loading #{quantity} units of #{type} in the supply hold."
    
    if @supplies[type]
      @supplies[type] += quantity
    else
      @supplies[type] = quantity
    end
  end
  
  def use_supplies(type, quantity)
    puts "----- Supply Action -----"
    if @supplies[type] != nil && @supplies[type] > quantity
      puts "Using #{quantity} of #{type} from the supply hold."
      @supplies[type] -= quantity
    else
      puts "Supply Error: Insufficient #{type} in the supply hold."
    end
  end
  
  def report_supplies
    puts "----- Supply Report -----"
    if @supplies.keys.length > 0
      @supplies.each do |type, quantity|
        puts "#{type} avalilable: #{quantity} units"
      end
    else
      puts "Supply hold is empty."
    end
  end
end

class FuelTank
  attr_accessor :fuel
  
  def initialize
    @fuel = 0
  end
  
  def get_fuel_levels
    @fuel
  end
  
  def load_fuel(quantity)
    puts "----- Fuel Action -----"
    puts "Loading #{quantity} units of fuel in the tank."
    @fuel += quantity
  end
  
  def use_fuel(quantity)
    puts "----- Fuel Action -----"
    puts "Using #{quantity} units of fuel from the tank."
    @fuel -= quantity
  end
  
  def report_fuel
    puts "----- Fuel Report -----"
    puts "#{@fuel} units of fuel available."
  end
end

class Thrusters
  def initialize(fuel_tank)
    @linked_fuel_tank = fuel_tank
  end
  
  def activate_thrusters
    puts "----- Thruster Action -----"
    if @linked_fuel_tank.get_fuel_levels >= 10
      puts "Thrusting action successful."
      @linked_fuel_tank.use_fuel(10)
    else
      puts "Thruster Error: Insufficient fuel available."
    end
  end
end

Phew! That was a lot of changes, but things are already looking a lot better. Now, our SpaceStation class is mostly just a container for subservient parts that manage individual operations, namely: a supply hold; a set of sensors; a fuel tank; and, thrusters. Each of these takes the form of an instance variable that is set during space station initialization. For each variable there is a corresponding class: Sensors; SupplyHold; FuelTank; and, Thrusters.

As you look through this version of the code, you will note several important differences with the first version. Not only are particular pieces of functionality encapsulated in their own classes but they are organized in a manner that is both predictable and consistent. The idea is to group like pieces of functionality in an attempt to follow the cohesion principle and to isolate data such that it is only accessible to relevant actors. Now, if we wanted to change how supplies are managed from a hash structure to an array, we could do so very easily in the SupplyHold class without affecting anything else in the program. Put another way, if the station logistics officer requests changes to her section’s functionality, then we can do that without affecting work being done by the station science officer. Meanwhile, the SpaceStation class has no idea how supplies are being stored and nor does it care!

Our users (science officer, pilot, etc.) are probably reasonably happy now with how their relevant parts are broken out, and they can request changes as needed; however, there is still more work we can do. Note, for example, the report_supplies method in the SupplyHold class and the report_fuel method in the FuelTank class. What happens if flight control back on Earth asks for a change in the way reports are submitted? Well, we would have to change the SupplyHold and FuelTank classes. But what now if flight control decides to change how supplies or fuel are loaded on the station? No problem, we’ll once again change the relevant methods on these classes. Hmm… it would seem then that we have multiple reasons for change on these particular classes. That sounds to me like a violation of the SRP! Let’s see if we can make a few more adjustments.

class SpaceStation
  attr_reader :sensors, :supply_hold, :supply_reporter,
              :fuel_tank, :fuel_reporter, :thrusters
  
  def initialize
    @sensors = Sensors.new
    @supply_hold = SupplyHold.new
    @supply_reporter = SupplyReporter.new(@supply_hold)
    @fuel_tank = FuelTank.new
    @fuel_reporter = FuelReporter.new(@fuel_tank)
    @thrusters = Thrusters.new(@fuel_tank)
  end
end

class Sensors
  def run_sensors
    puts "----- Sensor Action -----"
    puts "Running sensors!"
  end
end

class SupplyHold
  attr_accessor :supplies
  attr_reader :reporter
  
  def initialize
    @supplies = {}
  end
  
  def get_supplies
    @supplies
  end
  
  def load_supplies(type, quantity)
    puts "----- Supply Action -----"
    puts "Loading #{quantity} units of #{type} in the supply hold."
    
    if @supplies[type]
      @supplies[type] += quantity
    else
      @supplies[type] = quantity
    end
  end
  
  def use_supplies(type, quantity)
    puts "----- Supply Action -----"
    if @supplies[type] != nil && @supplies[type] > quantity
      puts "Using #{quantity} of #{type} from the supply hold."
      @supplies[type] -= quantity
    else
      puts "Supply Error: Insufficient #{type} in the supply hold."
    end
  end
end

class FuelTank
  attr_accessor :fuel
  attr_reader :reporter
  
  def initialize
    @fuel = 0
  end
  
  def get_fuel_levels
    @fuel
  end
  
  def load_fuel(quantity)
    puts "----- Fuel Action -----"
    puts "Loading #{quantity} units of fuel in the tank."
    @fuel += quantity
  end
  
  def use_fuel(quantity)
    puts "----- Fuel Action -----"
    puts "Using #{quantity} units of fuel from the tank."
    @fuel -= quantity
  end
end

class Thrusters
  FUEL_PER_THRUST = 10
  
  def initialize(fuel_tank)
    @linked_fuel_tank = fuel_tank
  end
  
  def activate_thrusters
    puts "----- Thruster Action -----"
    
    if @linked_fuel_tank.get_fuel_levels >= FUEL_PER_THRUST
      puts "Thrusting action successful."
      @linked_fuel_tank.use_fuel(FUEL_PER_THRUST)
    else
      puts "Thruster Error: Insufficient fuel available."
    end
  end
end

class Reporter
  def initialize(item, type)
    @linked_item = item
    @type = type
  end
  
  def report
    puts "----- #{@type.capitalize} Report -----"
  end
end

class FuelReporter < Reporter
  def initialize(item)
    super(item, "fuel")
  end
  
  def report
    super
    puts "#{@linked_item.get_fuel_levels} units of fuel available."
  end
end

class SupplyReporter < Reporter
  def initialize(item)
    super(item, "supply")
  end
  
  def report
    super
    if @linked_item.get_supplies.keys.length > 0
      @linked_item.get_supplies.each do |type, quantity|
        puts "#{type} avalilable: #{quantity} units"
      end
    else
      puts "Supply hold is empty."
    end
  end
end

iss = SpaceStation.new

iss.sensors.run_sensors
  # ----- Sensor Action -----
  # Running sensors!

iss.supply_hold.use_supplies("parts", 2)
  # ----- Supply Action -----
  # Supply Error: Insufficient parts in the supply hold.
iss.supply_hold.load_supplies("parts", 10)
  # ----- Supply Action -----
  # Loading 10 units of parts in the supply hold.
iss.supply_hold.use_supplies("parts", 2)
  # ----- Supply Action -----
  # Using 2 of parts from the supply hold.
iss.supply_reporter.report
  # ----- Supply Report -----
  # parts avalilable: 8 units

iss.thrusters.activate_thrusters
  # ----- Thruster Action -----
  # Thruster Error: Insufficient fuel available.
iss.fuel_tank.load_fuel(100)
  # ----- Fuel Action -----
  # Loading 100 units of fuel in the tank.
iss.thrusters.activate_thrusters
  # ----- Thruster Action -----
  # Thrusting action successful.
  # ----- Fuel Action -----
  # Using 10 units of fuel from the tank.
iss.fuel_reporter.report
  # ----- Fuel Report -----
  # 90 units of fuel available.

In this final version of our program, we have broken out reporting responsibilities into a FuelReporter class and a SupplyReporter class, both of which inherit from a parent Reporter class. We then add instance variables to our SpaceStation class to initialize relevant reporters for it to use. Now, if flight control asks for changes to reporting procedures, we can make the relevant changes in the Reporter subclasses without affecting the classes that they are reporting on.

Of course, there is still some coupling taking place between our various classes. A SupplyReporter object depends on being handed a SupplyHold object, as does a FuelReporter object depend on a FuelTank object. Necessarily, the Thrusters too require a FuelTank to draw upon. All of this seems reasonable to me, as some linkage is unavoidable, and we could still alter the operations of one object without drastically affecting the others. However, there is still room to improve on this program and to propose changes that would increase flexibility and maintainability (and indeed, I welcome such proposals in the comments!) What’s important for now is that this version of the code is a fairly significant improvement over our first “God object” version. We have effectively separated responsibilities into individual classes and thus reduced the chances of code changes in one place breaking operations in another. It’s also much nicer to work with when updates are needed.

TL;DR

The Single Responsibility Principle (SRP) is one of the five so-called SOLID principles, developed and promoted by Robert C. Martin to help developers produce flexible and maintainable code. In short, the SRP says that a given module or class should have responsible for a single element of a program’s functionality, and thus have just a single reason to change. The benefits of adhering to the SRP include: clearly defined boundaries as to where a piece of functionality is implemented; information-hiding practices that protect the integrity of data; separation of concerns that ensures changes in one location do not affect others; and, ease of code maintenance. In practice, it can be useful to think of your programs in terms of interested users and whether changes they request to one piece of functionality might inadvertently affect others. In the long run, following the SRP will both save you time and result in more effective code.

References


That’s all for our discussion of the SRP. Stay tuned for articles on the remaining four SOLID principles. Part 2 on the Open-Closed Principle is available here.


Note: This article was originally published on Medium.


You might enjoy...


© Severin Perez, 2021