技術メモなど

業務や日々のプログラミングのなかで気になったことをメモしています。PHP 成分多め。

PHPで考える単一責任の原則

単一責任の原則(SRP:Single responsibility principle)とは、Robert C.Martin 「アジャイルソフトウェア開発の奥義」*1にまとめられたソフトウェア開発における原則のひとつです。
後年になって、同著者の「Clean Architecture」*2によって以下のように再定義されました。

モジュールは、たったひとつのアクターに対して責務を追うべきである。

本稿では、PHPソースコードを用いながら、この原則(以下SRP)が意味するところについて考えます。

SRP違反の例

 架空の社内アプリケーションを例に考えてみましょう。 以下の Employee は、社員の情報を表すクラスです。

<?php
class Employee
{
    private $code;
    private $name;
    private $email;
    private $privateTelNumber;
    
    public function __construct($code, $name, $email , $privateTelNumber)
    {
        $this->code  = $code;
        $this->name  = $name;
        $this->email = $email;
        $this->privateTelNumber = $privateTelNumber;
    }
    
    public function code(){ return $this->code; }
    public function name(){ return $this->name; }
    public function email(){ return $this->email; }
    public function privateTelNumber(){ return $this->privateTelNumber; }

    public function export(){
        $filename = $this->code . '_' . $this->name . '.csv';
        $arr = $this->profile();
        return File::Csv($filename, $arr);
    }

    public function profile(){
        return [
            'code' => $this->code,
            'name' => $this->name,
            'email' => $this->email,
        ];
    }    
}

メソッド exportCsv は人事担当者がCSV出力する際に使われます。 メソッド profile は、一般社員も利用するアプリ内で、指定した社員のプロフィール画面を出力する際に使われます。 また、出力項目が同じであることから、exportCsv内でも使われています。

上記のモジュール(Employeeクラス)は、ふたつのアクター(一般社員・人事担当者)が利用しているという点において、SRPに違反しています。

モジュールは、たったひとつのアクターに対して責務を追うべきである。

ソフトウェアにおける責務とは、ソフトウェアの利用者=アクターの要望に対応する責務*3です。アクターの要望に変化があれば、そこにあるソースコードも変更しなればなりません。 その際、SRPが守られず複数のアクターに対し責務を持つモジュールは、変更に対しもろさのあるモジュールということが言えます。

現在CSV出力は社員コードと氏名、メールアドレスが対象となっていますが、さらに個人用電話番号を追加して欲しいという人事の要求がありました。 要求を受けた開発者が以下のように profile を改修しました。

<?php
// 略

    public function profile(){
        return [
            'code' => $this->code,
            'name' => $this->name,
            'email' => $this->email,
            'privateTelNumber' => $privateTelNumber,
        ];
    }    
}

CSV出力には個人用電話番号が表示されるようになり、当初の要求は満たしています。
一方で、profile は、一般社員も閲覧できるプロフィール画面に利用されているため、一般社員にも見られてしまうおそれが生まれています。「一般社員は個人用電話番号を閲覧してはならない」といった要件が場合、これはバグとなるでしょう。 このように、あるアクターの要求からくる変更が、他の無関係なアクターへと影響を及ぼすことをSRPは警告しています。

SRPに沿った改修

Employeeクラスの役割を、各アクターに対応するように分離します。

<?php
class Employee
{
    private $code;
    private $name;
    private $email;
    private $privateTelNumber;
    
    public function __construct($code, $name, $email , $privateTelNumber)
    {
        $this->code  = $code;
        $this->name  = $name;
        $this->email = $email;
        $this->privateTelNumber = $privateTelNumber;
    }
    
    public function code(){ return $this->code; }
    public function name(){ return $this->name; }
    public function email(){ return $this->email; }
    public function privateTelNumber(){ return $this->privateTelNumber; }
}

class EmployeeCsvPresenter
{
    private $employee;
    
    public function __construct($employee)
    {
        $this->employee = $employee;
    }
    
    public function export()
    {
        $filename = $this->code . '_' . $this->name . '.csv';
        $body =  [
            'code' => $this->code,
            'name' => $this->name,
            'email' => $this->email,
            'privateTelNumber' => $this->privateTelNumber,
        ];
        return File::Csv($filename, $body);
    }

}

class EmployeeProfilePresenter
{
    private $employee;
    
    public function __construct($employee)
    {
        $this->employee = $employee;
    }
    
    public function profile()
    {
        return [
            'code' => $this->code,
            'name' => $this->name,
            'email' => $this->email,
        ];
    }    
}

プロフィール画面の処理は EmployeeProfilePresenter に、CSV出力は EmployeeCsvPresenter にそれぞれ分離しました。 このソースコードは、最初のものよりも複雑さが増してします。しかし、アクター1つにつき1つのクラスが対応することによって以下のようなメリットが生まれています。

  • 影響範囲の限定
    ひとつのクラスが複数の役割を持っていると、そのクラスで作業する開発者にとっては改修による影響範囲の境界が見えづらくなります。SRPは、モジュールの役割を分離しその境界を明確にすることによってその影響範囲を限定します。分離された EmployeeCsvPresenter は、仮に改修によるエンバグがあったとしてもプロフィール画面機能に影響を及ぼすことはありません。 また、初めの改修について「そもそもprofileメソッドに手を入れなければよかったのではないか」という意見がありえますが、開発する上でのこういった「どこに手を入れるべきか/入れないべきか」という考慮事項そのものを減らす効果があります。

  • コンフリクトの回避
    もしCSV出力機能、プロフィール画面機能にそれぞれ同時に改修が必要になった場合、複数の開発者がコンフリクトを意識せずそれぞれ独立してソースコードを改修できます。これにより、並行開発における開発者間のコミュニケーションコストを減らします。

モジュール分割の指針

一方で、いざモジュールを無制限に分割しはじめると膨大なクラス群が出来上がり、複雑になります。 SRPは、分割の基準をアクターの存在に求めます。アクターが存在しないことは即ち、変更も影響範囲も存在しないからです。
SRPに即しているかは、ソースコード単体で決まるものではなく、その外部にあるコンテクストによって決まります。もし仮にアクターが人事担当者しかおらず、EmployeeクラスがCSV出力にしか使われないのであれば以下のコードでも問題ないということになります。

<?php
class Employee
{
    private $code;
    private $name;
    private $email;
    private $privateTelNumber;
    
    public function __construct($code, $name, $email , $privateTelNumber)
    {
        $this->code  = $code;
        $this->name  = $name;
        $this->email = $email;
        $this->privateTelNumber = $privateTelNumber;
    }
    
    public function code(){ return $this->code; }
    public function name(){ return $this->name; }
    public function email(){ return $this->email; }
    public function privateTelNumber(){ return $this->privateTelNumber; }

    public function export(){
        $filename = $this->code . '_' . $this->name . '.csv';
        $arr = $this->profile();
        return File::Csv($filename, $arr);
    }

    private function profile(){
        return [
            'code' => $this->code,
            'name' => $this->name,
            'email' => $this->email,
            'privateTelNumber' => $this->privateTelNumber,
        ];
    }    
}

まとめ

  • SRPは、ソフトウェアの複雑さを増す代わりに、モジュールの責務の境界を明確にする。
  • モジュールの責務は、それに対応するアクターの存在によって決まる。
  • モジュールは、たったひとつのアクターに対して責務を追うべきである。