【PHP】無名関数・継承・インターフェースによる再利用性向上

無名関数・継承・インターフェースを使用した再利用性の向上の方法を説明した上で、それぞれの方法のメリット・デメリットについて言及する。

Keywords

  • OOP
  • Anonymous Function
  • Interface
  • Inheritance

Contents

  • 1. はじめに
  • 2. 再利用性を考慮していないコード
  • 3. このプログラムのどこを再利用したいか
  • 4. 無名関数を使用する場合
  • 5. 継承を使用する場合
  • 6. インターフェースを使用する場合
  • 7. 再利用性の向上
  • 8. それぞれの方法について
  • 8-1. 無名関数
  • 8-2. 継承
  • 8-3. インターフェース
  • 9. インターフェースに依存することでの疎結合性
  • 10. 参考文献

はじめに

本稿では、tailコマンドのような動きをするプログラムを題材にして、再利用性の向上について説明する。

再利用性を考慮していないコードと、無名関数・継承・インターフェースのそれぞれの方法で再利用性を向上したコードを順番に見ていき、それぞれメリット・デメリットに言及していく。

これらのコードはGitHub - ale51/separate-responsibilityで見ることが可能である。

プログラムの実行のイメージは、下記でプログラムを起動した上で、

$ php main.php

別ターミナルで、test.logにデータを書き込むと

$ echo Hello, World >> test.log

標準出力に

$ php main.php
Hello, World

と表示される。

再利用性を考慮していないコード

再利用性を考慮していないコードを下記に示す。 一つのファイルで全ての処理を行なっている。

ログファイル(test.log)を監視しており、ログファイルに書き込まれたデータを標準出力に表示させるプログラムとなっている。

<?php

$sleepInterval = 1;
$filePath = "./test.log";

$fp = fopen($filePath, 'r');

$position = getLastPosition($fp);

do{

    $nextPosition = getLastPosition($fp);

    if($nextPosition > $position){

        fseek($fp, $position);

        $record = fread($fp, $nextPosition - $position);

        echo $record;
    }

    $position = $nextPosition;

    sleep($sleepInterval);

}while(true);

function getLastPosition($fp): int
{
    fseek($fp, 0, SEEK_END);
    return ftell($fp);
}

このプログラムのどこを再利用したいか

上のプログラムでは、ログファイルに書き込んだデータを標準出力に表示している。しかし、ここで標準出力ではなく、チャットでの通知やメールでの通知を行いたいニーズが生じたとする。

このままでは、ログファイルを監視している部分をコピー&ペーストして重複コードが発生してしまう。

そのため、今回はログファイルに書き込みされたデータのハンドリングの部分は、様々な方法でできるようにし、それ以外のログファイルを監視する処理を再利用できるようにリファクタリングする。

無名関数を使用する場合

Tailクラスを作成し、runメソッドで無名関数を受け取れるように変更する。

<?php

$sleepInterval = 1;
$filePath = "./test.log";

$fp = fopen($filePath, 'r');

$position = getLastPosition($fp);

do{

    $nextPosition = getLastPosition($fp);

    if($nextPosition > $position){

        fseek($fp, $position);

        $record = fread($fp, $nextPosition - $position);

        echo $record;
    }

    $position = $nextPosition;

    sleep($sleepInterval);

}while(true);

function getLastPosition($fp): int
{
    fseek($fp, 0, SEEK_END);
    return ftell($fp);
}

そして、main.phpを無名関数を渡せるように変更。

<?php

require_once "lib/Tail.php";

$filePath = "./test.log";
$tail = new Tail($filePath);

$func = function($record){
    echo $record;
};

$tail->run($func);

ここでは、そのままechoしているだけだが、チャット通知用の無名関数やメール通知用の無名関数のいずれかを作成すれば良い。

チャット用

$func = function($record){
    echo "This message is sent via chat: " . $record;
};

メール用

$func = function($record){
    echo "This message is sent via mail: " . $record;
};

継承を使用する場合

Tailクラスでは、recordHandlerというprotectedのメソッドを作成する。

<?php

class Tail
{

    private $filePath;
    const SLEEP_INTERVAL = 1;

    /**
     * Tail constructor.
     * @param $filePath
     */
    public function __construct($filePath)
    {
        $this->filePath = $filePath;
    }

    function run()
    {
        $fp = fopen($this->filePath, 'r');

        $position = $this->getLastPosition($fp);

        do {

            $nextPosition = $this->getLastPosition($fp);

            if ($nextPosition > $position) {

                fseek($fp, $position);

                $record = fread($fp, $nextPosition - $position);

                $this->recordHandler($record);
            }

            $position = $nextPosition;

            sleep(self::SLEEP_INTERVAL);

        } while (true);
    }

    protected function recordHandler(string $record): void
    {
        // do nothing in default
    }

    private function getLastPosition($fp): int
    {
        fseek($fp, 0, SEEK_END);
        return ftell($fp);
    }
}

ChatTailクラスというTailクラスを継承した子クラスを作成し、chat経由でメッセージを通知するrecordHandlerメソッドでオーラーライドする。(メール用にMailTailクラスを作成しても良い。)

<?php

require_once "Tail.php";

class ChatTail extends Tail {
    function recordHandler(string $record): void
    {
        echo "This message is sent via chat: " . $record;
    }
}

ChatTailをインスタンス化し、runメソッドを呼び出す。

<?php

require_once "lib/ChatTail.php";

$filePath = "./test.log";
$chatTail = new ChatTail($filePath);

$chatTail->run();

継承を使った方法としては、handleという抽象メソッドをもつ抽象クラスTailをrunメソッドで受け取れるようにし、その抽象クラスを継承しhandleメソッドをオーバーライドした子クラスを実際に渡す方法もある。

しかし、今回の例で言えば、インスタンス変数やクラス変数を定義する必要がないため、かつ、多重継承の問題が生じないため、インターフェースを使う方が適切である。そしてその方法は次項で説明する。

インターフェースを使用する場合

インターフェースとそれを実装(ここではChatのみ作成している)するクラスを作成する。

<?php

interface RecordHandler{
    function handle(string $record): void;
}
<?php

require_once "RecordHandler.php";

class ChatRecordHandler implements RecordHandler {
    function handle(string $record): void
    {
        echo "This message is sent via chat: " . $record;
    }
}

Tailクラスはインターフェースを使用するようにする。

<?php

class Tail
{

    private $filePath;
    const SLEEP_INTERVAL = 1;

    /**
     * Tail constructor.
     * @param $filePath
     */
    public function __construct($filePath)
    {
        $this->filePath = $filePath;
    }

    function run(RecordHandler $recordHandler)
    {
        $fp = fopen($this->filePath, 'r');

        $position = $this->getLastPosition($fp);

        do {

            $nextPosition = $this->getLastPosition($fp);

            if ($nextPosition > $position) {

                fseek($fp, $position);

                $record = fread($fp, $nextPosition - $position);

                $recordHandler->handle($record);
            }

            $position = $nextPosition;

            sleep(self::SLEEP_INTERVAL);

        } while (true);
    }

    private function getLastPosition($fp): int
    {
        fseek($fp, 0, SEEK_END);
        return ftell($fp);
    }
}

main.phpはインターフェースを実装しているChatRecordHandlerクラスのインスタンスをrunメソッドに渡す。無名関数の場合と同じように、RecordHandlerを実装したクラスであればなんでもrunメソッドに渡せるので、MailRecordHandlerクラスといったものを実装して渡すことも可能。

<?php

require_once "lib/Tail.php";
require_once "lib/ChatRecordHandler.php";

$filePath = "./test.log";
$tail = new Tail($filePath);

$chat = new ChatRecordHandler();

$tail->run($chat);

再利用性の向上

いずれの方法でも、ファイル監視という責務を分離させ、Tailクラスでその責務を担当させている。

もともとは、ファイル監視の処理が$recordのハンドリングも担当していたため、メールでの通知やチャットでの通知に切り替えることができなかった。if文を使用して、標準出力にするかメールで通知するか、チャットで通知するか切り替えることも可能かもしれないが、その都度、ファイル監視の処理に変更が加わるので、再利用性は低い。

    ~略~
    
    if($nextPosition > $position){

        fseek($fp, $position);

        $record = fread($fp, $nextPosition - $position);

        // ここにif文で標準出力での表示、メールでの通知、チャットでの通知の分岐をさせると、通知の変更のたびに、ファイル監視の処理に影響を与えてしまう。
        echo $record;
    }

    ~略~

今回3つの方法で、ファイル監視の処理と$recordのハンドリングの処理を分割できたため($recordのハンドリング処理という責務をなくしたため)、ファイル監視処理はどこに通知するか責任をおわなくなった。そのことで、ファイル監視処理が再利用できるようになった。

また、通知処理とファイル監視処理が別々にテスト可能になった。通知処理のテストをする際に、ログファイルを作成して、そのファイルにメッセージを追記するといった手間が省くことができる。

それぞれの方法について

無名関数

無名関数を使用した方法は一見手軽ではあるが、Tailのrunメソッドのシグネチャは`function run(callable $func)`であり、これだけでは、どのような無名関数を渡せばいいか判断できず、実際に処理を読まなければならないので、使用しづらい。

継承

継承の方法では、"パトランプに通知する"といった通知に関する変更には対応できるが、それ以外の観点の変更には対応しづらくなってしまう。

例えば、"どのようなログでもチャットで通知する機能"と"特定の文字列を含むログのみをチャットで通知する機能"の両方が必要になった場合のことを考えてみる。

方法としては、親クラスにisHandleというprotectedメソッドを追加し、子クラスでそれをオーバライドする。子クラスとしては、AnyLogSendChatTailクラスとSomeLogSendChatTailクラスの2つが必要となる。

<?php

    ~ 略 ~

                $record = fread($fp, $nextPosition - $position);

                if($this->isHandled($record)){
                  $this->recordHandler($record);
                }
                
    ~ 略 ~
            
    
    protected function isHandled(string $record){
        // return false in default
        return false;
    }
    ~ 略 ~
}

通知のみの観点であれば、MailTailクラス、ChatTailクラス、RotatingWarningLightクラスとった具合で子クラスを作っていけばいいが、観点が複数になると、ChatTailクラスをAnyLogSendChatTailクラスとSomeLogSendChatTailクラスの2つにわけ、もしMailTailクラスが既存に存在していたら、わかり易さのためにAnyLogSendMailTailクラスといったようにクラス名を変更する必要がでてくるかもしれず、想定外のファイルの変更が発生してしまう。

そして、さらに別の観点が追加されれば、AnyLogSendXXXXChatTailクラスのように、わかりづらいクラスが発生してしまうかもしれない。これを俗にクラス爆発と呼ぶ。

理論的にはA観点での種類が2つ、B観点での種類が3つ、D観点での種類が4つあれば、2x3x4の24クラスが存在する可能性が生じてしまう。

インターフェース

無名関数を使った方法と比べて、interfaceのファイルを読めばどのようなオブジェクトを渡せば良いか簡単に理解することができる。文字列を受け取り、何も返す必要のないメソッドで、かつ、名前がhandleであるメソッドであれば何でもよく、それ以上の知識を知る必要がない。

interface RecordHandler{
    function handle(string $record): void;
}

また、継承の方法と比較しても、Tailクラスのrunメソッドに通知のトリガーとなる特定の文字列の変数、もしくは、CheckRecordインターフェースのようなものを用意し、特定の文字列が含まれるか判定する実装クラスを作成すれば、クラス爆発を防ぐことが可能となる。(特定の文字列の判定が簡易であれば、ただの特定の文字列を格納する変数をrunに渡すだけで良いかもしれない。)

    function run(RecordHandler $recordHandler, CheckRecord $checkRecord)
    {
        $fp = fopen($this->filePath, 'r');

        $position = $this->getLastPosition($fp);

        do {

            $nextPosition = $this->getLastPosition($fp);

            if ($nextPosition > $position) {

                fseek($fp, $position);

                $record = fread($fp, $nextPosition - $position);

                if($checkRecord->check($record)){
                    $recordHandler->handle($record);
                }
                
                ~ 略 ~
  • RecordHandlerインターフェース チャット通知やメール通知といった、通知を担当

  • CheckRecordインターフェース ログの文字列のチェックを担当

インターフェースに依存することでの疎結合性

インターフェースを使った例では、Tailクラスは、RecordHandlerインターフェースを使用(依存)している。

$recordHandler->handle($record);

そのため、Tailクラスとしては"自分の責務はファイル監視。それと具体的にはよくわからないけど、このパラメータでこのインターフェースのメソッド(handleメソッド)を呼び出しておけば大丈夫"といったスタンスをとることができる。

そしてこのようにTailクラスから具体的な仕事を奪っていけば、通知に関する変更がTailクラスに影響することはありえず、疎結合の状態となる。 たとえば、メールでの通知にしようが、チャットワークの通知にしようが、そのようなことはTailクラスは"知らない"ので、そもそも影響が及ぶことはない。

参考文献