The Single Responsibility Principle

The Single Responsibility Principle

A class should have one, and only one, reason to change.

– Robert C. Martin

When we talk about reasons to change we are talking about business responsibilities. Pieces of business logic. Each class should deal with one singular piece of business logic. It should do one thing well and have only a singular reason to be changed.

One example of this would be creating an Orders Report class that gathers company order data, calculates and processes it, and finally formats it before presenting it back to the classes consumer. These are multiple responsibilities and multiple reasons to change.

Consider this reporting class.

namespace Report;
 
use DB;
 
class OrdersReport
{
    public function getOrdersInfo($startDate, $endDate)
    {
        $orders = $this->queryDBForOrders($startDate, $endDate);
         
        return $this->format($orders);
    }
 
    protected function queryDBForOrders($startDate, $endDate)
    {  
        // If we would update our persistence layer in the future,
        // we would have to do changes here. <=> reason to change!
        return DB::table('orders')->whereBetween('created_at', [$startDate, $endDate])->get();
    }
     
    protected function format($orders)
    {  
        // If we changed the way we want to format the output,
        // we would have to make changes here also. <=> reason to change!
        return '<h1>Orders: ' . $orders . '</h1>';
    }
}

This class has several reasons to change based on unrelated business considerations.

If the sales department changes the way they want to present the data, for example in a spreadsheet, the class will need to change. The same thing if a DBA makes a change to the data store’s structure or location, the class will have to change as well.

We clearly have two reasons to change: one is business logic-related and the other is infrastructure-related. According to the Single Responsibility Principle, these concerns shouldn’t reside in the same object.

Let’s look at a refactored version.

namespace Report;
 
use Report\Repositories\OrdersRepository;
 
class OrdersReport
{
    protected $repo;
    protected $formatter;
 
    public function __construct(Repository $repo, Formatter $formatter)
    {
        $this->repo      = $repo;
        $this->formatter = $formatter;
    }
 
    protected function getOrderData()
    {
        return $this->repo->getBetween($startDate, $endDate);
    }
 
    public function getAggragatedOrdersReport($startDate, $endDate)
    {
        $orders = $this->getOrderData($startDate, $endDate);
 
        // Process data as needed for report
 
        return $this->formatter->output($orders);
    }
}
 
//---------------------------------------------
namespace Report;
 
class HtmlOutput extends Formatter
{
    public function output($orders)
    {
        // Format orders into HTML and return
    }
}
 
//---------------------------------------------
namespace Report;
 
class CsvOutput extends Formatter
{
    public function output($orders)
    {
        // Add your obligatory fputcsv() code here
    }
}

First, we inject our data access repository object into the reporting class, removing that responsibility and that reason to change. Ideally, we would type-hint an interface but we will save that for a future discussion. Next, we pass in an output formatting object, once again removing business logic and responsibility.

We can now change the data store and output formatting at will and the OrderReport class doesn’t care. It is now easy to maintain, scale, and most importantly test.

For example, simply mock a DataRepository and an OutputFormatter and test the report classes’ data processing and calculation functionality in isolation.

namespace Tests\AppBundle\Service;
 
use PHPUnit\Framework\TestCase;
 
class EnclosureBuilderServiceTest extends TestCase
{
    public function testItCalculatesData()
    {
        $repo = $this->createMock(Repository::class);
        $formatter = $this->createMock(Formatter::class);
        $sut = new OrdersReport($repo, $formatter);
 
        // do stuff
 
        $this->assertCount(['formatted', 'data'], $sut->getAggragatedOrdersReport());
    }
 
}

The Takeaway

This applies to all entities in your coding, not just classes. From Packages to Classes and Functions each entity in your code should have a single responsibility. To do one thing, do it well, and move along.

Always ask yourself whether the logic you are introducing should live in this class or not. Is this a different or unique responsibility? What outside factors could force a change to it? How does it add to the complexity of the class?

When in doubt try writing some basic comments that map out and describe what the code you are implementing will be doing. If you find yourself using a lot of statements like “in this case”, “but if”, “except when”, “or else this” then the method or class will be probably too complex. This is a perfect opportunity to break things out and simplify your code.