1. 概要

このチュートリアルでは、オブジェクト指向設計のSOLID原則について説明します。

まず、それらが発生した理由と、ソフトウェアを設計するときにそれらを考慮する必要がある理由を調査することから始めます。 次に、いくつかのサンプルコードとともに各原則の概要を説明します。

2. SOLID原則の理由

SOLIDの原則はRobertCによって導入されました。 マーティンは2000年の論文「DesignPrinciplesandDesignPatterns」で、これらの概念は後に、SOLIDの頭字語を紹介したMichaelFeathersによって構築されました。 そして、過去20年間で、これらの5つの原則は、オブジェクト指向プログラミングの世界に革命をもたらし、ソフトウェアの作成方法を変えました。

では、SOLIDとは何ですか?それはどのように私たちがより良いコードを書くのに役立ちますか? 簡単に言えば、Martin and Feathersの設計原則は、より保守しやすく、理解しやすく、柔軟なソフトウェアを作成することを奨励しています。したがって、アプリケーションのサイズが大きくなるにつれて、複雑さを軽減して節約できます。私たち自身、さらに先の多くの頭痛の種です!

次の5つの概念は、SOLIDの原則を構成しています。

  1. S単一の責任
  2. Oペン/クローズ
  3. Liskov置換
  4. Iインターフェースの分離
  5. D ependency Inversion

これらの概念は気が遠くなるように思えるかもしれませんが、いくつかの簡単なコード例で簡単に理解できます。 次のセクションでは、これらの原則を深く掘り下げ、それぞれを説明する簡単なJavaの例を示します。

3. 単一責任

単一責任の原則から始めましょう。 ご想像のとおり、この原則は次のように述べています。 クラスには1つの責任しかありません。 さらに、変更する理由は1つだけです。

この原則は、より優れたソフトウェアを構築するのにどのように役立ちますか?その利点のいくつかを見てみましょう。

  1. テスト– 1つの責任を持つクラスでは、テストケースがはるかに少なくなります。
  2. より低い結合–単一のクラスの機能が少ないほど、依存関係が少なくなります。
  3. 組織–小さく、よく組織化されたクラスは、モノリシッククラスよりも検索が簡単です。

たとえば、簡単な本を表すクラスを見てみましょう。

public class Book {

    private String name;
    private String author;
    private String text;

    //constructor, getters and setters
}

このコードでは、 Book のインスタンスに関連付けられた名前、作成者、およびテキストを格納します。

次に、テキストをクエリするためのメソッドをいくつか追加しましょう。

public class Book {

    private String name;
    private String author;
    private String text;

    //constructor, getters and setters

    // methods that directly relate to the book properties
    public String replaceWordInText(String word){
        return text.replaceAll(word, text);
    }

    public boolean isWordInText(String word){
        return text.contains(word);
    }
}

これで、 Book クラスが適切に機能し、アプリケーションに好きなだけ本を保存できます。

しかし、テキストをコンソールに出力して読み取ることができない場合、情報を保存することはどのようなメリットがありますか?

風に注意を払い、印刷方法を追加しましょう。

public class Book {
    //...

    void printTextToConsole(){
        // our code for formatting and printing the text
    }
}

ただし、このコードは、前に概説した単一責任の原則に違反しています。

混乱を修正するには、テキストの印刷のみを処理する別のクラスを実装する必要があります。

public class BookPrinter {

    // methods for outputting text
    void printTextToConsole(String text){
        //our code for formatting and printing the text
    }

    void printTextToAnotherMedium(String text){
        // code for writing to any other location..
    }
}

素晴らしい。 Book の印刷業務を軽減するクラスを開発しただけでなく、BookPrinterクラスを活用してテキストを他のメディアに送信することもできます。

メール、ロギング、その他のいずれであっても、この1つの懸念事項に特化した別のクラスがあります。

4. 拡張のために開き、変更のために閉じます

今度は、オープンクローズ原則として知られるSOLIDのOの時間です。簡単に言えば、クラスは拡張のために開いている必要がありますが、変更のために閉じている必要があります。 そうすることで、 we は、既存のコードを変更したり、他の点では満足のいくアプリケーションで潜在的な新しいバグを引き起こしたりするのを防ぎます。

もちろん、ルールの1つの例外は、既存のコードのバグを修正する場合です。

簡単なコード例を使用して、概念を調べてみましょう。 新しいプロジェクトの一環として、Guitarクラスを実装したと想像してください。

それは本格的であり、ボリュームノブさえあります:

public class Guitar {

    private String make;
    private String model;
    private int volume;

    //Constructors, getters & setters
}

私たちはアプリケーションを起動し、誰もがそれを気に入っています。 しかし、数か月後、ギターは少し退屈で、クールな炎のパターンを使用して、よりロックンロールに見えるようにすることができると判断しました。

この時点で、 Guitar クラスを開いて炎のパターンを追加したくなるかもしれませんが、アプリケーションでどのようなエラーが発生するかは誰にもわかりません。

代わりに、オープンクローズの原則に固執し、Guitarクラスを拡張してみましょう。

public class SuperCoolGuitarWithFlames extends Guitar {

    private String flameColor;

    //constructor, getters + setters
}

Guitar クラスを拡張することで、既存のアプリケーションが影響を受けないようにすることができます。

5. リスコフの置換

次のリストは、リスコフの置換です。これは、おそらく5つの原則の中で最も複雑です。 簡単に言えば、クラスAがクラスBのサブタイプである場合、プログラムの動作を中断することなく、BをAに置き換えることができるはずです。

この概念を理解するのに役立つコードに直接ジャンプしましょう。

public interface Car {

    void turnOnEngine();
    void accelerate();
}

上記では、単純な Car インターフェースを定義し、すべての車が実行できるはずのいくつかの方法を使用します。エンジンをオンにして前方に加速します。

インターフェイスを実装して、メソッドのコードをいくつか提供しましょう。

public class MotorCar implements Car {

    private Engine engine;

    //Constructors, getters + setters

    public void turnOnEngine() {
        //turn on the engine!
        engine.on();
    }

    public void accelerate() {
        //move forward!
        engine.powerOn(1000);
    }
}

コードで説明されているように、エンジンをオンにして、出力を上げることができます。

しかし待ってください—私たちは今電気自動車の時代に生きています:

public class ElectricCar implements Car {

    public void turnOnEngine() {
        throw new AssertionError("I don't have an engine!");
    }

    public void accelerate() {
        //this acceleration is crazy!
    }
}

エンジンのない車をミックスに投入することで、プログラムの動作を本質的に変更しています。 これはLiskov置換の露骨な違反であり、前の2つの原則よりも修正が少し難しいです。

考えられる解決策の1つは、モデルをCarのエンジンのない状態を考慮したインターフェースに作り直すことです。

6. インターフェイス分離

SOLIDのIはインターフェイス分離を表し、単にそれを意味します大きなインターフェースは小さなインターフェースに分割する必要があります。 そうすることで、クラスの実装で、関心のあるメソッドのみを考慮する必要があることを確認できます。

この例では、飼育係として手を試してみます。 具体的には、クマの囲いの中で作業します。

クマの飼育係としての私たちの役割を概説するインターフェースから始めましょう:

public interface BearKeeper {
    void washTheBear();
    void feedTheBear();
    void petTheBear();
}

熱心な飼育係として、私たちは愛するクマを洗って餌を与えることをとても嬉しく思っています。 しかし、私たちは皆、彼らをかわいがることの危険性を十分に認識しています。 残念ながら、私たちのインターフェースはかなり大きく、クマをかわいがるコードを実装する以外に選択肢はありません。

大きなインターフェースを3つの別々のインターフェースに分割してこれを修正しましょう

public interface BearCleaner {
    void washTheBear();
}

public interface BearFeeder {
    void feedTheBear();
}

public interface BearPetter {
    void petTheBear();
}

現在、インターフェースの分離のおかげで、私たちにとって重要なメソッドのみを自由に実装できます。

public class BearCarer implements BearCleaner, BearFeeder {

    public void washTheBear() {
        //I think we missed a spot...
    }

    public void feedTheBear() {
        //Tuna Tuesdays...
    }
}

そして最後に、危険なものを無謀な人々に任せることができます。

public class CrazyPerson implements BearPetter {

    public void petTheBear() {
        //Good luck with that!
    }
}

さらに、 BookPrinter クラスを前の例から分割して、同じ方法でインターフェイス分離を使用することもできます。 単一のprintメソッドでPrinterインターフェイスを実装することにより、別々のConsoleBookPrinterクラスとOtherMediaBookPrinterクラスをインスタンス化できます。

7. 依存性逆転

依存性逆転の原則は、ソフトウェアモジュールの分離を指します。 このように、低レベルのモジュールに依存する高レベルのモジュールの代わりに、両方が抽象化に依存します。

これを実証するために、昔ながらの方法で、コードを使用してWindows98コンピュータを実現しましょう。

public class Windows98Machine {}

しかし、モニターとキーボードのないコンピューターは何が良いのでしょうか? コンストラクターにそれぞれを1つずつ追加して、インスタンス化するすべてのWindows98ComputerモニターStandardKeyboardがあらかじめパックされているようにします。

public class Windows98Machine {

    private final StandardKeyboard keyboard;
    private final Monitor monitor;

    public Windows98Machine() {
        monitor = new Monitor();
        keyboard = new StandardKeyboard();
    }

}

このコードは機能し、Windows98Computerクラス内でStandardKeyboardおよびMonitorを自由に使用できるようになります。

問題が解決しました? 完全ではありません。 新しいキーワードを使用してStandardKeyboardとMonitorを宣言することにより、これら3つのクラスを緊密に結合しました。

これにより、 Windows98Computer のテストが困難になるだけでなく、必要に応じてStandardKeyboardクラスを別のクラスに切り替えることができなくなります。 そして、Monitorクラスにもこだわっています。

より一般的なKeyboardインターフェースを追加し、これをクラスで使用して、マシンをStandardKeyboardから切り離してみましょう。

public interface Keyboard { }
public class Windows98Machine{

    private final Keyboard keyboard;
    private final Monitor monitor;

    public Windows98Machine(Keyboard keyboard, Monitor monitor) {
        this.keyboard = keyboard;
        this.monitor = monitor;
    }
}

ここでは、依存性注入パターンを使用して、Keyboard依存性をWindows98Machineクラスに追加しやすくしています。

また、 StandardKeyboard クラスを変更して、 Keyboard インターフェイスを実装し、Windows98Machineクラスへの注入に適したものにしましょう。

public class StandardKeyboard implements Keyboard { }

これで、クラスが分離され、Keyboard抽象化を介して通信します。 必要に応じて、インターフェイスの実装を変えて、マシンのキーボードの種類を簡単に切り替えることができます。 Monitorクラスについても同じ原則に従うことができます。

優秀な! 依存関係を切り離し、Windows98Machineを選択したテストフレームワークで自由にテストできます。

8. 結論

この記事では、オブジェクト指向設計のSOLIDの原則を深く掘り下げました。

私たちはSOLIDの歴史と、これらの原則が存在する理由について簡単に説明しました。

手紙ごとに、私たちは各原則の意味を、それに違反する簡単なコード例で分解しました。 次に、コードを修正する方法を確認しました SOLIDの原則に準拠させます。

いつものように、コードはGitHubから入手できます。