From Apprentice To Artisan - Single Responsibility Principle

Recently, I review this book again, and I have new thinking so I make a note.

Definition

The Single Responsibility Principle states that a class should have one, and only one, reason to change. In other words, a class' scope and responsibility should be narrowly focused.

Reality

So, that's sound like a class should be do specific only, but in reality, many situations could not follow the rules, because the workflow or business flow will be effect it, but in this moment, you should keep your professional, do the best.

Description

Book example:

class OrderProcessor 
{
    public function __construct(BillerInterface $biller)
    {
        $this->biller = $biller;
    }

    public function process(Order $order)
    {
        $recent = $this->getRecentOrderCount($order);
        
        if($recent > 0) {
            throw new Exception('Duplicate order likely.');
        }

        $this->biller->bill($order->account->id, $order->amount);

        DB::table('orders')->insert(array(
            'account'    =>    $order->account->id,
            'amount'    =>    $order->amount,
            'created_at'=>    Carbon::now()
        ));
    }

    protected function getRecentOrderCount(Order $order)
    {
        $timestamp = Carbon::now()->subMinutes(5);
        return DB::table('orders')
               ->where('account', $order->account->id)
               ->where('created_at', '>=', $timestamps)
               ->count();
    }
}

Upon code make me a thinking, what's wrong with that? but you are deeper analysis you will find it mix conditions and handling, and you can find if I try to modify get orders states for 10 minutes ago I need navigate to OrderProcessor make query select to modify, but remember, this class is Order Processor, it's NOT orders states handler.

class OrderProcessor 
{
    ...

    public function process(Order $order)
    {
        /* conditions */
        // get order states for five minutes ago.
        $recent = $this->getRecentOrderCount($order);
        
        // if the amount > 0, duplicate order, throw exception.
        if($recent > 0) {
            throw new Exception('Duplicate order likely.');
        }
        /* conditions */

        // order processing flow...
        $this->biller->bill($order->account->id, $order->amount);

        // history
        DB::table('orders')->insert(array(
            'account'    =>    $order->account->id,
            'amount'    =>    $order->amount,
            'created_at'=>    Carbon::now()
        ));
    }
    
    // get order states for five minutes ago.
    protected function getRecentOrderCount(Order $order)
    {
        $timestamp = Carbon::now()->subMinutes(5);
        
        return DB::table('orders')
                    ->where('account', $order->account->id)
                    ->where('created_at', '>=', $timestamps)
                    ->count();
    }
}

Have two places do calling connection and make query, you should extract a class OrderRepository, let it focus on interactive with database.

class OrderRepository 
{
    public function getRecentOrderCount(Account $account)
    {
        $timestamp = Carbon::now()->subMinutes(5);
        
        return DB::table('orders')
                    ->where('account', $account->id)
                    ->where('created_at', '>=', $timestamp)
                    ->count();
    }

    public function logOrder(Order $order)
    {
        DB::table('orders')->insert(array(
            'account'    =>    $order->account->id,
            'amount'    =>    $order->amount,
            'created_at'=>    Carbon::now()
        ));
    }
}

Finally, inject OrderRepository to OrderProcessor, let OrderRepository do making query with database only.

class OrderProcessor 
{
    public function __construct(BillerInterface $biller, OrderRepository $orders)
    {
        $this->biller = $biller;
        $this->orders = $orders;
    }

    public function process(Order $order)
    {
        $recent = $this->orders->getRecentOrderCount($order->account);

        if($recent > 0) {
            throw new Exception('Duplicate order likely.');
        }

        $this->biller->bill($order->account->id, $order->amount);

        $this->orders->logOrder($order);
    }
}

Pros

Easy to modify

If you want to change get orders states for five minutes to get orders states for ten minutes, navigate to OrderRepository is, but here is a little problem, you may not feeling, thinking if getRecentOrderCount this method work on many classes, if you don't extract it you will be modify many times, againest DRY(don't repeat yourself.) principle

Responsibility is clear and stable

Upon example, OrderProcessor focus on handle workflow processing, it don't need to consider making query with database. OrderRepository responsibility is making query with database, it don't hanle workflow processing. on the rule, you can make high cohesion (focus) class.

Codebase will be test easily.

High cohesion class, each class will be standalone testing, for example we are testing OrderRepository only make mocking database interactive connection class, not necessary think working flow, focus on make query and interactive with database. OrderProcessor is mocking OrderRepository, focuse on workflow.

Last Updated: 1/27/2019, 1:25:04 PM