#あすみかんの上にあすみかん

#たのしいことしかかかないことをここに決意します

PHPのExceptionから学ぶリスコフの置換原則

はじめに

PHPカンファレンス北海道2024にて発表する「失敗例から学ぶSOLID原則」において、発表時間の都合上「単一責任の原則」「リスコフの置換原則」に触れることができませんでした。 そのため、本編拡張版として、ブログで「リスコフの置換原則」の失敗例を書いていきます💪

▼ 本編スライド(O・I・D)はこちら!

speakerdeck.com

▼ Sについての補足記事はこちら!

asumikam.com

失敗例から学ぶリスコフの置換原則

どのように失敗したか

自サービスでの独自例外を実装したい部分があり、私は以下の様な実装をしました。

class UserExistenceException extends RuntimeException
{
    /**
     * @param string[] $message メッセージ一覧
     * @param int $code code
     * @param \Throwable|null $previous previous
     */
    public function __construct(array $message = [], int $code = 0, ?\Throwable $previous = null)
    {
        $message = implode("\n", $message);
        parent::__construct($message, $code, $previous);
    }
}

PHPにおけるException・RuntimeExceptionクラスのを見ていきましょう。

class RuntimeException implements Throwable {
    /* 継承したプロパティ */
    // ...

    /* 継承したメソッド */
    public Exception::__construct(string $message = "", int $code = 0, ?Throwable $previous = null)

    // ...
}

__constructは上記の様に定義されています。

今回注目すべきは第一引数の$messageです。 親クラス(RuntimeException)ではstringですが、子クラス(UserExistanceException)の方ではarrayとなっています。

これが、どう違反しているかを補足するために「ちょうぜつソフトウェア設計入門」の文章を一部引用します。

リスコフの置換現象(LSP)を一言で言うと、「派生クラスの振る舞いは、基底クラスの振る舞いを完全にカバーしなければならない」となります。

(ちょうぜつソフトウェア設計入門 5-4 リスコフの置換原則)

派生クラスでオーバーライドした引数の型は、基底クラスのメソッド引数と同じ型のものならなんでも受け入れないとエラーになります。基底クラスがCatのインスタンスを受け入れるとき、Animalを入力できるようにするのは、何の問題もありません。どんな動物でも入るケージなら、猫なんて余裕で入ります。でもその逆は許されません。どんな動物でも入ると約束したのに猫しか入らないじゃないかとなると、大問題です。

(ちょうぜつソフトウェア設計入門 5-4 リスコフの置換原則: 型の事前条件)

今回、stringだった引数をarrayにしているので、本で挙がっている例とは若干ニュアンスは違うのですが、明らかに「同一ではない」ことから、事前条件に関して「リスコフの置換原則」に違反していると言えます。

どのようにするべきだったか

class UserExistenceException extends RuntimeException
{
    /**
     * @param string|string[] $message メッセージ一覧
     * @param int $code code
     * @param \Throwable|null $previous previous
     */
    public function __construct(string|array $message = [], int $code = 0, ?\Throwable $previous = null)
    {
        $message = implode("\n", $message);
        parent::__construct($message, $code, $previous);
    }
}

事前条件は「同じ」もしくは「弱める」のであれば問題ないため、元々のstringと、今回拡張したいstring[]を受け取る様にしてあげるだけでOKです。

PHPではそもそも「リスコフの置換原則」に違反しにくい

PHP: クラスの基礎 - Manual

メソッドをオーバーライドするときは、 子クラスのシグネチャが親クラスのそれと互換性がなければいけません。 互換性が壊れた場合、致命的なエラーが発生します。 PHP 8.0.0 より前のバージョンでは、 互換性が壊れた場合に、E_WARNING レベルの警告が発生していました。 共変性と反変性 の規則を守っている場合は、シグネチャに互換性があります。 必須の引数をオプションにした場合も、互換性があります。 新しいオプションの引数を追加しただけで、アクセス権を厳しくせず、 緩めただけの場合も互換性があります。 これは、リスコフの置換原則(Liskov Substitution Principle)、 略して LSP として知られています。 但し、コンストラクタ と private メソッドについては、 この規則の例外で、 オーバライドしたシグネチャにミスマッチがあっても致命的なエラーにはなりません。

PHPでは、引数・返り値に関してリスコフの置換原則に違反しようとすると、そもそも致命的なエラーが発生します。 ただ、私は__constructで違反していたので致命的なエラーで気づけなかった、という訳ですね。

今回、ストーリーを入れる際に以下の様な案が出ていました。

  • 今回の実際のExceptionを例にして出すか?
    • 本筋のストーリーと雰囲気が違う
  • 引数・返り値に違反している感じを出すか?
    • PHPのバージョンが低くって・・・」っていうストーリーの追加が必要

どちらをとっても資料中のストーリーと若干雰囲気がずれてしまう、時間があればうまく混ぜれそうですが、15分という時間だと若干資料の内容がグニョるな......と思ったので今回別記事として出しました。

まとめ

実は、このExceptionの件が「失敗例から学ぶSOLID原則」の原点だったりします。 「なんかこれ体系的にストーリー作ったらおもれーんじゃねえかなあ」と思い、ちょうぜつ本を再度手にとって読んだ上で今回のストーリーを作り上げました。

fortee.jp

2024/01/13 15:00〜 クリエイティブスタジオ セッション(15分)

いよいよ明日、ついに!!!失敗例、大公開しちゃうみたいです〜〜〜〜!!!!たのしみ〜〜〜〜!!!!!!はずかっしゃ〜〜〜!!!!

でも頑張るぞっ!!!!お楽しみにいいいいいい!!🦀🦀🦀