SOLID OOP Development: Single Responsibility Principle
In this section of understanding the SOLID development pattern and how it applies to OOP we are going to walk through the single responsibility principle. While the concept of single responsibility has been around for a while it was popularized in 2003 by Uncle Bob.
Guide Tasks
  • Read Tutorial
  • Watch Guide Video
Video locked
This video is viewable to users with a Bottega Bootcamp license

In this section of understanding the SOLID development pattern and how it applies to OOP we are going to walk through the single responsibility principle. While the concept of single responsibility has been around for a while it was popularized in 2003 by Uncle Bob.

large

Single Responsibility Principle

So what exactly is the single responsibility principle? The term is pretty self descriptive. Essentially it means that each class and module in a program should focus on a single task.

SRP in the Real World

medium

Before we go into a code example, let’s look at the real world case study of a vacuum cleaner. A vacuum cleaner can perform a number of tasks such as cleaning floors and upholstery. Some of the more upscale vacuums can even work like a blower. However notice how, at its core, a vacuum has an engine that utilizes air to perform the task of cleaning?

It wouldn’t make sense for a vacuum cleaner to also wash windows. If you introduced this type of feature to the vacuum it may work for a while, but would most likely cause increased maintenance costs when it would inevitably break down.

Single Responsibility Principle Example in Ruby

Now that you have an idea of how the single responsibility principle works in the real world, let’s dive into a code example.

The Class That Knew Too Much

class Invoice
  def initialize(customer:, state:, total:)
    @customer = customer
    @state = state
    @total = total
  end

  def details
    "Customer: #{@customer}, Total: #{@total}"
  end

  def sales_tax
    case @state
    when 'AZ' then 5.5
    when 'TX' then 3.2
    when 'CA' then 8.7
    end
  end

  def email_invoice
    puts "Emailing invoice..."
    puts details
  end
end

invoice = Invoice.new(customer: "Google", state: "AZ", total: 100)
puts invoice.sales_tax
invoice.email_invoice

Here we have an Invoice class that seems to be relatively straightforward. The class:

  • Prints out details about the invoice
  • Calculates sales tax
  • Emails the invoice with its details
  • When we run the code everything works fine and prints out these values:
5.5
Emailing invoice...
Customer: Google, Total: 100

This may seem fine at first, however this code is breaking the single responsibility principle in a number of ways.

Rule of Thumb: No ‘Ands’ Allowed

When it comes to following the SOLID design pattern, a good rule of thumb is that if your description of a class has the word "and" in it, then it may need to be refactored. For example, let’s describe this class:

The invoice class prints out invoice details AND calculates sales tax AND emails the invoice.

Whenever I’m performing a refactor I like to treat the behavior between the ANDs as their own class.

A Mailer Class

Here we have created a new class called Mailer that has a single method that will send out an email. The email method takes an argument where we can pass the invoice details to it.

class Invoice
  def initialize(customer:, state:, total:)
    @customer = customer
    @state = state
    @total = total
  end

  def details
    "Customer: #{@customer}, Total: #{@total}"
  end

  def sales_tax
    case @state
    when 'AZ' then 5.5
    when 'TX' then 3.2
    when 'CA' then 8.7
    end
  end
end

class Mailer
  def self.email(content)
    puts "Emailing..."
    puts content
  end
end

invoice = Invoice.new(customer: "Google", state: "AZ", total: 100)
puts invoice.sales_tax
Mailer.email(invoice.details)

If we run this code we’ll see this is working properly.

5.5
Emailing invoice...
Customer: Google, Total: 100

Sales Tax Class

Next is our sales tax feature. This component definitely shouldn’t be included in the Invoice class since it doesn’t take much imagination to realize that this feature may be required by other parts of an application outside of the invoice.

class Invoice
  def initialize(customer:, state:, total:)
    @customer = customer
    @total = total
  end

  def details
    "Customer: #{@customer}, Total: #{@total}"
  end
end

class SalesTax
  def initialize(state:)
    @state = state
  end

  def sales_tax
    case @state
    when 'AZ' then 5.5
    when 'TX' then 3.2
    when 'CA' then 8.7
    end
  end
end

class Mailer
  def self.email(content)
    puts "Emailing..."
    puts content
  end
end

invoice = Invoice.new(customer: "Google", state: "AZ", total: 100)
tax = SalesTax.new(state: 'CA')
puts tax.tax_rate
Mailer.email(invoice.details)

Here we’re create a SalesTax class that takes in the state we want to generate the sales tax for. Running this code will show that our program is still working perfectly.

8.7
Emailing...
Customer: Google, Total: 100

And now our Invoice class is following the single responsibility principle. Notice how the invoice is no longer in charge of generating sales tax or emailing customers?

Why the Single Responsibility Principle is Important

So why is this type of OOP design pattern important? Our initial Invoice code worked fine, so why would we have to change it? Let’s imagine that this program was used by a real world accounting division. What if a user wanted to see what the tax rate would be for a specific state? It wouldn’t make sense for the system to require the user to create an invoice to calculate that value.

By refactoring the program in the way we did in this guide our SalesTax class could be used independently or by any other classes that may need the feature. In the computer science world this concept is called coupling. In our initial code example, the sales tax component was highly coupled to the Invoice class. This means that it would be messy to work with the tax rate generator without having to also work with the Invoice class.

Our refactor fixed this issue and now we can say that the tax feature has low coupling. Which means that users can access the tax rate component without having to work with other classes.