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

本文へ

フッターへ

お役立ち情報Blog



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

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

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

  • ISP(Interface Separation Principle、インターフェース分離の原則)
  • DIP(Dependency Inversion Principle、依存関係逆転の原則)

の2つについて解説していきたいと思います。

前提

  • PHP8.1.6

ISP(Interface Separation Principle、インターフェース分離の原則)

「Clean Architecture 達人に学ぶソフトウェアの構造と設計」によると、

使っていないメソッドを持つクラスに依存しない

不要なものには依存しないこと

必要としないお荷物を抱えたものに依存していると、予期せぬトラブルの元につながる

ということだそうです。
つまり、不要なものには依存しない。ということですね。

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

原則違反コード

<?php 

interface Bird
{
    public function eat();
    public function fly();
}

class Sparrow implements Bird
{
    public function eat(): void
    {
        
    }

    public function fly(): void
    {
        
    }
}

class Penguin implements Bird
{
    public function eat()
    {
        
    }

    public function fly()
    {
        // penguin can't fly but has to define fly() method because of implementing Bird Interface
    }
}

原則違反箇所

 Penguin クラスが Bird インターフェースを実装していることで、 eat()  fly() の2つのメソッドを持たなければいけなく、ペンギンも飛ぶことになってしまいます。これはペンギンは飛べないという事実と相反しますので不要ですよね。
つまり、 Penguin クラスが fly() メソッドを持つことは無駄で不要で、ISPに違反している点dです。

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

原則準拠コード

<?php

interface Bird
{
    public function eat();
}

interface FlyingBird
{
    public function fly();
}

class Sparrow implements Bird, FlyingBird
{
    public function eat(): void
    {

    }

    public function fly(): void
    {

    }
}

class Penguin implements Bird
{
    public function eat(): void
    {
        
    }
}

やったこと

 eat() メソッドを持つ Bird インターフェース、 fly() メソッドを持つ FlyingBird インターフェースをそれぞれ作り、 Sparrow クラスは食べるし、飛ぶので、両方のインターフェースを実装。
 Penguin クラスは食べるが飛べないので、 Bird インターフェースのみを実装。

思考の流れ

実装したインターフェースに含まれるメソッドは、絶対に含めないといけなくなるので、何が必要で何が不要かを考える。

ポイント

鳥の共通の食べるという振る舞いと、共通ではない(例外が存在する)飛ぶという振る舞いをわけてそれぞれインターフェースにする。

DIP(Dependency Invension Principle、依存性逆転の原則)

「Clean Architecture 達人に学ぶソフトウェアの構造と設計」によると、

ソースコードの依存関係が(具象ではなく)抽象だけを参照しているもの。それが、最も柔軟なシステムである。これが「依存関係逆転の原則(DIP)」の伝えようとしていることである。

変化しやすい具象クラスを参照しない。その代わりに抽象インターフェイスを参照すること。

依存関係逆転の原則(DIP)の違反を完全に取り除くことはできないが、依存関係逆転の原則(DIP)を満たさない具象コンポーネントを少数に絞り込み、それらをシステムの他の部分と分離することはできる。

ということだそうです。
つまり、修正・変更が入りやすい具象なクラスには極力依存しないようにする。依存する場合は、抽象インターフェースを実装して依存の方向を逆転する。ということでしょうかね。

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

原則違反コード

ここでは、飼育員が鳥を飼育している状況です。鳥は種類が増えたり、減ったりします。1人の飼育員が、種類が増えたり減ったりする鳥を飼育するという状態なので、飼育員は抽象な概念・上位な概念で、鳥はより具象・下位な概念として考えます。

<?php

class Sparrow
{
    public function eat()
    {
        // concrete logic
    }

    public function sleep()
    {
        // concrete logic
    }
}

class Penguin
{
    public function eat()
    {
        // concrete logic
    }

    public function sleep()
    {
        // concrete logic
    }
}

class Keeper
{
    private $bird;

    public function __construct(
      private Penguin $bird
    ) {}

    public function watchOver()
    {
        $this->bird->eat();
        $this->bird->sleep();
    }
}

原則違反箇所

 Keeper クラス(飼育員)はコンストラクタの部分で Penguin クラスのインスタンスを引数に受け取る。これにより、 Penguin クラスに依存している。他の鳥を飼育しようとした場合 Keeper クラスに変更を加えないといけません。
これは Keeper —> Penguin のように図示できます。より抽象・上位な概念のクラスであるはずの飼育員が、具象・下位なクラスであるペンギンに依存してしまっている点がDIPに違反しています。

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

原則準拠コード

<?php

interface Bird
{
    public function eat();
    public function sleep();
}

class Sparrow implements Bird
{
    public function eat()
    {
        // concrete logic
    }

    public function sleep()
    {
        // concrete logic
    }
  
    // ... you can add more methods! 
}

class Penguin implements Bird
{
    public function eat()
    {
        // concrete logic
    }

    public function sleep()
    {
        // concrete logic
    }
  
    // ... you can add more methods! 
}

class Keeper 
{
    private $bird;

    public function __construct(
      private Bird $bird
    ) {}

    public function watchOver()
    {
        $this->bird->eat();
        $this->bird->sleep();
    }
}

やったこと

 Keeper クラスがペンギンやスズメといった特定の種類の鳥に依存している状態から、 Bird インターフェースという鳥はこういう動作をするよね、を示したより抽象的な概念に依存するようにしました。
そして、 Penguin クラスと Sparrow クラスは Bird インターフェースを実装する(依存する)ようにしました。これは Keeper —> IBird ◁— Penguin のように図示できます。

思考の流れ

具象クラスに依存しないといけない場合、抽象インターフェースを作って、具象クラス達はそのインターフェースを実装して、自身はそのインターフェースに依存することを考えます。

ポイント

 Keeper クラス(飼育員)を Bird インターフェースに依存するようになることで、鳥がたとえペンギンだろうが、スズメだろうがハトだろうが、具体的にどんな鳥なのかを気にすることなく、つまり具象が何かを気にしなくていいようになり、ただ鳥が来るんだなと分かっていればよくなりました。
例えば Penguin クラスに新しいメソッドを追加しても、あたらしく Condor クラス(コンドル)を飼育することになっても、 Keeper クラスを特に変更する必要はありません。

逆転という意味では、 Keeper —> Penguin から Keeper —> IBird ◁— Penguin に図が変わりました。依存されるだけの Pengin クラスが IBird を実装することで逆の依存関係が生まれました。
このような事象をうけて依存性逆転の原則と呼ばれているんですね。

おわりに

いかがでしたでしょうか?

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

個人的には、改めてSOLID原則に向き合って説明をしたことで、理解が進んだと同時に、新たな発見や、この場合はどうすればいいんだ?のような疑問点も出てきました。今後もこのSOLID原則を意識しながら開発していくことで生じた疑問点に向き合っていこうと思います。

このような哲学的なものの理解に努めることはすごく好きなので、今後もやっていけたらいいなと思います。

それでは、また。

この記事を書いた人

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

FOLLOW US

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