• トップ
  • ブログ一覧
  • OOPの5大原則!SOLID原則について勉強してみましょう!
  • OOPの5大原則!SOLID原則について勉強してみましょう!

    イデ(エンジニア)イデ(エンジニア)
    2022.04.04

    IT技術

    初めに

    こんにちは!
    初めてのブログ記事ですが、皆さんに役に立つ記事になると幸いです!🙇‍♂️

    今回のテーマはSOLID原則です!!!
    よろしくお願いいたします。

    SOLID原則とは?

    まず、SOLID原則って言うのが何なのか分かる必要がありますね。
    SOLIDというのは、OOP向けの設計に関して5つの基本原則の意味です。

    時間が過ぎても、メインテナンスと拡張しやすいシステムを構築しようとする時、この原則と一緒に進められます。
    では、一つ一つ確認してみましょうか!🥺

    第一:単一責任原則(Single Responsibility Principle)

    あるクラスを変更する理由は、必ず一つだけでなければならない - ロバート・C. マーチン

    第一の単一責任原則です。
    そのままの意味で、たった一つの責任を持つ!ということです。では、ここで責任の基本単位は?

    まさに【オブジェクト(Object)】です!では責任は?
    OOPの中で、責任ということは【オブジェクト】が「できること - 機能」の意味です。

    1class store { // お店 
    2    addThings() {} // 物の追加 
    3    getThingList() {} // 物のリストを取得 
    4    getThingInfo() {} // 物の詳細情報の取得 
    5    calculation() {} // 購入した物件の計算
    6    printReceipt() {} // レシートの出力
    7    this.thing = null; 
    8    this.receipt = null; 
    9}

    例えば、上のようなclassがあると仮定します。

    storeクラスが持っているものは、6つがあります。
    この場合、一つのクラスができる機能(責任)が複数あると、クラス内部の関数が強い結合を起こす可能性が高まります。
    凝集度は高く結合度は低いプログラムを設計することがOOP設計の核心なのに、上のような例は違反になることです。

    新しい仕様やプログラムの変更によってクラス内の動作が連鎖的に変更される可能性もあるので、
    これは非効率的な方法になります。

    上のクラスに第一原則を導入してみると

    1class store{ // お店 
    2    calculation() {} // 購入した物件の計算
    3    printReceipt() {} // レシートの出力
    4    this.receipt = null; 
    5} 
    6
    7class thing{ 
    8    addThings() {} // 物の追加 
    9    getThingList() {} // 物のリストを取得 
    10    getThingInfo() {}// 物の詳細情報の取得 
    11    this.thing = null; 
    12}

    このように、クラスを分けることがいい感じですね。
    もちろん、仕様によって、store中のreceipt関連変数や関数も他のクラスに分離し、storeの中でreceiptとthingのオブジェクトを受け入れて組み合わせることもありだと思います。

    第二:開放/閉鎖原則(Open-Closed Principle)

    ソフトウェアエンティティ(クラス、モジュール、関数等)は拡張に関しては開いていなければならないが、変更については閉じていなければならない。- ロバート・C. マーチン

    第二の開放/閉鎖原則です。
    上の語録は見つけましたか?CleanArchitectureで有名なロバート・C. マーチンさんからの言葉です。

    つまり、第二原則は既存のコードを変更せずに、修正または追加できるように設計しなければならないということです。

    1class EnglishBook {
    2    public $english;
    3    
    4    public function __construct(A $alphabetA) {
    5        $this->english = $alphabetA;
    6    }
    7    
    8    public function printClass() {
    9        return $this->english->printEnglish();
    10    }
    11}
    12
    13class A {
    14    public function printEnglish() {
    15        return 'A';
    16    }
    17}

    上のコードをみましょうか?
    EnglishBookクラス中でアルファベットクラスを定義しています。
    Aがは始まり次はB、C等ずっとクラスを生成しなきゃいけない仕組みですね。
    でも、このパータンなら、constructから一つ一つアルファベットクラスに応じるtypeを入れて毎回生成しなきゃいけないですね。

    1EnglishBook {
    2    public $english;
    3    
    4    public function __construct(English $Alphabet) {
    5        $this->setEnglish($Alphabet);
    6    }
    7    
    8    public function setEnglish(English $english) {
    9        $this->english = $english;
    10    }
    11    
    12    public function printClass() {
    13        return $this->english->printEnglish();
    14    }
    15}
    16
    17class A implements English {
    18    public function printEnglish() {
    19        return 'A';
    20    }
    21}
    22
    23class B implements English {
    24    public function printEnglish() {
    25        return 'B';
    26    }
    27}
    28
    29interface English {
    30    public function printEnglish();
    31}

    今回は、このコードをみましょうか?
    各アルファベットクラスはinterfaceとしてEnglishというクラスを参照してます。
    EnglishBookのconstructでも定義されているtypeはinterfaceのEnglishで定義されています。
    この場合、EnglishBookに入れるクラスは何でも、EnglishInterfaceを参照するとできるということですね!

    このように拡張には開いていなければならず、変更には閉じていなければなりません。
    あるモジュールの機能を1つ修正する際に、そのモジュールを利用する他のモジュールも修正しなければならないとメンテナンスが複雑になりますね。
    なので、開放閉鎖原則をうまく適用し、既存のコードを変更しなくても機能を新しく作成したり変更したりする必要があります。

    そうでないと、OOPの最大の長所である柔軟性、再使用性、メンテナンス性などをすべて失ってしまいます。

    第三:リスコフ置換原則(Liskov Substitution Principle)

    サブタイプはいつも自分のベースタイプに換えられなければならない。 - ロバート・C. マーチン

    簡単に言うと、子クラスは親クラスでできることを行えるようにという意味です。

    1class pocket{
    2    payCash() {
    3        return 'cash';
    4    }
    5}
    6
    7class cardPocket extends pocket {
    8    payCard() {
    9        return 'card';
    10    }
    11}

    上のように二つのクラスがあるとします。
    pocketクラスでは、payCashを使えます。もちろん、cardPocketでもpayCashを使えますね。

    相続関係では、一般化関係(Generalization)が成立しなきゃいけないということです。(一貫性のある関係なのか)
    相続関係ではないクラス達を設定するとこの原則が違反になります。(再利用の目的で使う場合)

    つまり、親クラスを子クラスに置き換えても通常に動かなきゃいけないことです。

    第四:依存逆転原則(Dependency Inversion Principle)

    高次元モジュールは低次元モジュールに依存してはならない。二つのモジュールは他の抽象化されたものに依存しなければならない。
    抽象化されたものは具体的なものに依存してはならない。具体的なことが抽象化されたことに依存しなければならない。
    頻繁に変更される具体的なクラスに依存するな。 - ロバート・C. マーチン

    難しい言葉ですが。。。簡単にいうと!
    変化しやすいものより、変化しにくいものに依存するべきということです。
    ささっとコードで確認してみましょう!

    1class coffeeStore extends Cook {
    2    public function createCook() {
    3        return 'do you like coffee?';
    4    }
    5}
    6
    7class steakStore extends Cook {
    8    public function createCook() {
    9        return 'do you like steak?';
    10    }
    11}
    12
    13class Home extends Cook {
    14    public function createCook() {
    15        return 'do you like home cooked?';
    16    }
    17}
    18
    19abstract class Cook{
    20    abstract public function createCook();
    21}

    このコードの核心はCookです!
    料理をするというがメインのクラスが三つあります。coffeeStore、steakStore、Homeがありますね。

    料理する店、場所には様々なところがありますね。
    hamburgerStoreもあるし、saladStoreもあるし、Home以外にもKitchenもありますね。
    でも、あの場所で料理をする(createCook)のは変わらないですね。

    高次元モジュール(Cook)は低次元モジュール(coffeeStore、steakStore、Home)に依存してはならないです。
    このように低次元モジュールが変更されても、高次元モジュールは変更が要らない形が理想です。

    第五:インターフェース分離原則(Interface Segregation Principle)

    クライアントは自分が使わないメソッドに依存関係を結んではならない。 - ロバート・C. マーチン

    「クライアントは自分が使うメソッドだけ、依存するべき」ということです。

    各クライアントが必要とするインタフェースを分離することにより、クライアントが使わないインタフェースに変更が発生しても影響を受けないようにすることが核心です!

    最後に、、、

    ここまでいかがでしょうか?

    上記のような原則を事前に知っていれば、実際のサービス開発で
    設計を進めながら、メンテナンスしやすく拡張もしやすいシステムが作れるのではないでしょうか?

    それでは次の記事でお会いしましょう!
    最後まで読んでいただき、ありがとうございました!🙇‍♂️

    イデ(エンジニア)

    イデ(エンジニア)

    おすすめ記事