グローバルナビゲーションへ

本文へ

フッターへ

お役立ち情報Blog



PHPのコード例でSOLID原則を理解する(LSP編)~保守性の高いソフトウェア開発の実現を目指して~


ご無沙汰しております。

今回も前回の「PHPのコード例でSOLID原則を理解する(SRP・OCP編)」の続きで、理解しているつもり、使えているつもり、になっているかもしれないSOLID原則をおさらいしていきます。

今回はSOLID原則のうちの、「L」にあたる、

LSP(Liskov Substitution Principle、リスコフの置換原則)

を扱います。

個人的に最も理解に苦しんだ原則なので、今回はこのLSPだけを解説していきます。

前提

  • PHP8.1.6
  • スーパークラス、親クラス、基底クラス、スーパータイプ等をここでは、「スーパークラス」と代表して呼びます。
  • サブクラス、子クラス、派生クラス、サブタイプ等をここでは、「サブクラス」と代表して呼びます。

LSP(Liskov Substitution Principle、リスコフの置換原則)とは?

「PHPで理解するオブジェクト指向の活用 ちょうぜつソフトウェア設計入門」によると、

派生クラスの振る舞いは、基底クラスの振る舞いを完全にカバーしなければならない

ということだそうです。つまり、サブクラスはそのスーパークラスと置換可能であり、サブクラスはスーパークラスの代わりとして、振る舞いを変更せずに使用できるべきである、ということです。

また、サブクラスはスーパークラスよりも事前条件は強めてはいけなく、事後条件は弱めてはいけません。こちらはこの記事の最後に改めて説明します。

では、コードを見て理解していきましょう。

原則違反コード

飛ぶというメソッドを持つ鳥クラスがあります。ペンギンは鳥の1種であるので、鳥クラスを継承してペンギンクラスが作れます(ペンギンは飛べないので  fly()  メソッドは例外を発生させます)。鳥はもちろん飛ぶだろうということを信じて、鳥を飛ばせる  makeBirdFly()  という関数を作りました。引数には  Bird  型のオブジェクトを受け取ります。

 Bird  クラスのインスタンスを作成し、 makeBirdFly()  関数で飛ばすと、無事飛びました。
 Penguin  クラスのインスタンスを作成して、 makeBirdFly()  関数で飛ばすと、飛ばずに「飛べません」という例外が発生しました。

<?php 

class Bird
{
    public function fly()
    {
        // flying logic 
    }
}

class Penguin extends Bird
{
    public function fly()
    {
        // Penguin can't fly
        throw new Exception('ペンギンは飛べません');
    }
}

function makeBirdFly(Bird $bird)
{
    $bird->fly();
}

$bird = new Bird();
$penguin = new Penguin();

makeBirdFly(bird: $bird);       // OK
makeBirdFly(bird: $penguin);    // Uncaught Exception: Penguin can't fly!

原則違反箇所

 Pengiun  クラスは  Bird  クラスの(サブクラス)であるのにもかかわらず、 Bird  クラスのオブジェクトを引数に取る  makeBirdFly()  関数を使えません。この点が原則違反です。
繰り返しますが、LSPはスーパークラスとが完全に代替可能でなければいけません。

では、原則に則るようなコードとはどんなものでしょうか。

原則準拠コード

「継承を使ったLSPに準拠したコード」と「インターフェースを使ったLSPに準拠したコード」の2つのパターンを説明します。

継承を使ったコード

<?php

class Bird
{
    public function eat()
    {
        // eating logic
    }
}

class Sparrow extends Bird
{
    public function fly()
    {
        // flying logic
    }
}

class Penguin extends Bird
{
    // don't have a fly() method because penguin can't fly
}

function makeBirdEat(Bird $bird)
{
    $bird->eat();
}

$sparrow = new Sparrow();
$penguin = new Penguin();

makeBirdEat(bird: $sparrow);    // OK
makeBirdEat(bird: $penguin);    // OK

やったこと

  • 鳥は生きるために絶対に食べるので、「食べる」能力を持つ   eat()  メソッドを持つ  Bird  クラスを作成します。
  •  Sparrow  クラスも   Penguin  クラスも  Bird  クラスを継承します。
  •  makeBirdEat()  関数は引数に  Bird  クラスのインスタンスを引数に取ります。

思考の流れ

  1. 鳥でも、飛ぶ鳥と飛べない鳥が存在するが、「食べる」能力を共通で持つ。
  2. 鳥の共通の能力である「食べる」を表現するスーパークラス(サブクラス) Bird  を作成。
  3. 飛ぶ鳥は「食べる」能力以外に、「飛ぶ」能力を持つので、例えばスズメの  Sparrow  クラスを作り、 Bird  を継承した上で、 fly()  メソッドを持たせる。
    飛ばない鳥は「食べる」能力のみを持ち、例えばペンギンの Penguin  クラスを作り、 Bird  を継承する。
  4. 実際に食べさせる  makeBirdFly()  関数を作成し、引数に  Bird  クラスが渡される。
  5.  makeBirdEat()  関数は  Sparrow  クラスも  Penguin  クラスも問題なく受け取れる。

ポイント

 Bird  クラスの(サブクラス)である  Sparrow  クラスも  Penguin  クラスも、 makeBirdEat()  関数の引数に問題なく渡されます。言い換えると、 Bird  クラスが保証する全ての能力(今回は「食べる」)が  Sparrow  クラス、 Penguin  クラスによって満たされている状態です。つまり(サブクラス)でスーパークラス(スーパークラス)を完全に代替可能な状態になっています。

インターフェースを使ったコード

<?php 

interface FlyingBird
{
    public function fly();
}

class Sparrow implements FlyingBird
{
    public function fly()
    {
        // flying logic
    }
}

class Penguin
{
    // Can't implement FlyingBird interface because penguin can't fly
}

function makeBirdFly(FlyingBird $bird)
{
    $bird->fly();
}

$sparrow = new Sparrow();
$penguin = new Penguin();

makeBirdFly(bird: $sparrow);     // OK
makeBirdFly(bird: $penguin);     // Uncaught TypeError: makeBirdFly(): Argument #1 ($bird) must be of type FlyingBird

やったこと

  • 飛ぶ鳥と飛ばない鳥がいることを認識しているので、「飛ぶ」という動作を表現する  fly()  メソッドを考え、それを定義する  FlyingBird  インターフェースを作成し、飛ぶ鳥は全てこのインターフェースを実装するようにします。
  •  makeBirdFly()  関数は引数に飛ぶ振る舞いを持っている  FlyingBird  インターフェースを実装したクラスだけ受け取るようにします。

思考の流れ

  • 鳥でも、飛ぶ鳥と飛べない鳥が存在するので、「飛ぶ」動作ができる鳥達を  FlyingBird  インターフェースで表現します。
  • スズメは「飛ぶ」動作ができるので  FlyingBird  インターフェースを実装します。
    一方、ペンギンは「飛ぶ」動作ができないので実装しません。
  • 実際に飛ばさせる  makeBirdFly()  関数を作成し、引数には  FlyingBird  インターフェースが渡されるようにする。
  •  makeBirdFly()  関数を使って、「飛ぶ」動作ができる鳥は実際に飛び、そうでない鳥は飛ばない。

ポイント

「飛ぶ」動作ができる鳥とできない鳥を認識し、「飛ぶ」動作ができる鳥を表現するために、 FlyingBird  インターフェースを実装する。このインターフェースを実装した鳥はもちろん  makeBirdFly()  関数に問題なく渡されます。どんな鳥のクラスであっても、このインターフェースを実装していれば必ず飛べますし、実装していなければ飛べません。

事前条件・事後条件

事前条件とは、メソッドが呼び出される処理やインスタンスが生成される処理等何かの処理の前に満たされるべき条件のセットです。その処理が正常に機能するために必要な条件を定義します。

事後条件とは、メソッドが呼び出された後の処理やインスタンスが生成された後の処理等何かの処理の後に満たされるべき条件のセットです。その処理の後に続く処理が正常に機能するために必要な条件を定義します。

その上で、

  • サブクラスの事前条件はスーパークラスより強めることは出来ない
  • サブクラスの事後条件はスーパークラスより弱めることは出来ない

についてみていきます。具体的なコードで見ると分かりやすいです。

事前条件のコード例

<?php

class Bird 
{
    public function setSpeed($speed) 
    {
        if ($speed < 0) {
            throw new Exception("速度は正の数である必要があります。");
        }
    }
}

class Sparrow extends Bird 
{}

class Penguin extends Bird 
{
    public function setSpeed($speed) 
    {
        if ($speed > 5) {
            throw new Exception("ペンギンはそんなに速く走れません。");
        }
    }
}

スーパークラスである  Bird  クラスは速度が正の数であればなんでも良いが、サブクラスである  Penguin  クラスは速度が正の数である、かつ、5より小さくないといけないことになってます。サブクラスの受け取れる引数の条件が厳しくなっていますよね(事前条件が強まった)。

つまり、この例では「事前条件を強める=スーパークラスのメソッドが受け付ける引数やその範囲を、サブクラスがより狭く制約することを意味する」 と理解でき、これはLSPの違反に繋がことになってしまいます。

事後条件のコード例

<?php
  
class Bird 
{
    protected $altitude = 0;

    public function fly() 
    {
        return $this->altitude; 
    }

    public function setAltitude($altitude) 
    {
        $this->altitude = $altitude;
    }
}

class Sparrow extends Bird 
{}

class Penguin extends Bird 
{
    public function setAltitude() 
    {
        $this->altitude = 0; // penguin can't fly so the altitude is 0
    }
}


$bird1 = new Bird();
$bird2 = new Penguin();

$bird1->setAltitude(10);
$bird2->setAltitude(10);

echo $bird1->fly(); //=> 10
echo $bird2->fly(); //=> 0

この時、 Bird クラスと Penguin クラスのインスタンスをそれぞれ作成して、高度を設定して、いざ飛ばせた時に、返ってくる結果が違いました。 Bird クラスでは任意の高度を設定できたのに、 Penguin クラスでは問答無用に0が設定されます。これはスーパークラスの動作をサブクラスで保証できていないことを意味しています。

つまり、この例では「事後条件を弱める=スーパークラスのメソッドが保証していた条件を、サブクラスのメソッドが保証しなくなることを意味する」と理解でき、これはLSPの違反に繋がことになってしまいます。

おわりに

今回はSOLID原則のうちのLに当たるLSPに関して説明しました。原則違反コード→原則準拠コードの順で追うと結構理解しやすいのではないか?と思います。

次回はIとDに当たるISPとDIPに関して説明出来ればと思います。

この記事を書いた人

KJG
KJGソリューション事業部 システムエンジニア
大学4年時春に文系就職を辞め、エンジニアになることを決意し、独学でRuby、Ruby on Railsを学習。
約1年間の独学期間を経てアーティスへWebエンジニアとして入社。現在はWebエンジニアとして、主にシステムの開発・運用に従事している。
抽象的なもの、複雑なものを言語化して文章にするのが好きで得意。
この記事のカテゴリ

FOLLOW US

最新の情報をお届けします