- Read Tutorial
- Watch Guide Video
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.
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
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.