Single Responsibility Principle

About a year ago I was asked to do a talk covering the SOLID principles of Object Oriented Programming (OOP) at my local Laravel Meetup. We recently had another meetup after a 9 month hiatus, and there were several new members who showed some interest in the topic. So I thought i would turn my talk into 5 blog posts covering each rule.

The Single Responsibility Principle, is the first SOLID Principle for OOP.

For those new to SOLID, S.O.L.I.D Stands for:

  • Single Responsibility Principle
  • Open Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle

This article is the first of a 5 post series that will cover the SOLID Principles.

A Class should have only one reason to change, meaning that a class should have only one job.

In this Principle you want to make sure your classes are only responsible for a single thing. Think of a thing as a type of Object. If you were to instantiate a class as an object, the methods on that object should really only apply to the type of object it is. For instance the most popular example of an object might be a Car. A car should only have methods that would allow a Car to do something a Car can do. For instance, accelerate, brake, turn left, turn right. Those are all things a car can do. You wouldn't expect to see a method on the Car Object that figures out distance from point A to point B. Even though a car can certainly travel that distance, it's not a cars Responsiblity to calculate distance.

So let's look at some examples that don't involve cars. Let's use a Garden as an example.

A garden is made up of lots of different types of plants.

A garden by it's self is an Object that's made up of lots of other Objects as plants. For instance a garden can have Lettuce, carrots, etc.

Let's take a look at a couple of Classes:

Here is a plant class.

class Carrot 
{
    private calories = 100;
    private type = "root";
    private perBunch = 4;
    private color = "orange";
    private vitaminC = "50mg";
    private count;

    public function __construct($count)
    {
	    $this->count = $count;
    }
   
    public function getCount()
    {
         return $this->count;
    }

    public function getPerBunch()
    {
         return $this->perBunch;
    }
}

In our Carrot class we have our construct getting the count of carrots. We also have a bunch of private variables that define some characteristics of Carrots.

And here is our Garden Class.

class Garden
{
	private $plants;

	public function __construct($plants = array())
	{
		$this->plants = $plants;
	}

	public function quantity()
	{
		$quantity = 0;
		foreach($this->plants as $plant){
			if($plant->type==“root”){
			    $quantity = $quantity + $plant->getPerBunch() * $plant->getCount();
			}else {
			    $quantity = $quantity + $plant->getCount();
		    }
		}
		return $quantity;
	}
}

In our Garden class we accept an array of plants in our constructor, and we have a method to calculate the quantity of plants we have in our garden.

You'll note that our quantity() method is checking to see if the plant type is a root vegetable or not. If it is, it's using the perBunch variable to multiply the count variable to get an accurate count of carrots.

Let's look at some code to instantiate this.

$plants = [
	'carrots' => new Carrot(5),
	'broccoli'=> new Broccoli(2),
];
$garden = new Garden($plants);

For the sake of this example we'll assume Broccoli's are not root vegetables.
Now when we do $garden->quantity(); we should get 22. So far our Carrot class is only responsible for Carrot things, and our Garden class is only responsible for garden things.

Now here's where things get complicated. Let's say we want to have several different formats for the output of our quantity() method on our Garden Class. Say we want just the numerical value for processing in code, json for processing an API response, and HTML for sending pre-formatted output to a View.

Here's an example of what that might look like:

public function quantity($output)
{
    $output = strtolower($output);
    $quantity = 0;
	foreach($this->plants as $plant){
		if($plant->type==“root”){
		    $quantity = $quantity + $plant->getPerBunch() * $plant->getCount();
		}else {
		    $quantity = $quantity + $plant->getCount();
	    }
	}
		
 
    If($output == 'json'){
    	return json->encode(array('quantity'=>$quantity));
	}elseif($output == 'html'){
 		return "<p>Quantity: ".$quantity."</p>";
	}
	return $quantity;
}

So now we can do this:

$plants = [
	'carrots' => new Carrot(5),
	'broccoli'=> new Broccoli(2),
];
$garden = new Garden($plants);
$garden->quantity('json');

And this will give us {'quantity' : 22} instead of just 22.

Now here's where the Single Responsibility Principle throws a red flag.
You'll remember in the beginning of the article we talked about how a class should only have one reason to change, a single job.

Well our Garden class now has two jobs. It has the job of keeping track of what's in our garden, but it also has the job of formatting the response now as well. Meaning the class has to change each time we want to expand our garden methods, but it also has to change every time we want to add/change/remove formatting options as well.

So how do we change this? Well, we need to build another class that manages just formatting, and pass the garden class to it, similar to how we pass our plant classes to our garden class.

with out defining the GardenOutputterclass, what we end up with is something like the following:

//setup our Plants array
$plants = array(
	'carrots' => new Carrot(5),
	'broccoli'=> new Broccoli(2),
	);
//pass plants array to garden
$garden = new Garden($plants);
//pass our garden to our outputter class
$gardenOutput = new GardenOutputter($garden);
//display outputs
Echo $gardenOutput->json();
Echo $gardenOutput->html();
Echo $gardenOutput->xml();

Now we can add any formatting we want, in our GardenOutPutt Class, and we end up with classes being properly separated, so that each one has only one job, and reason to change.

Note: We'll be using these classes in the subsequent articles, so these are overly simplified for a reason.