HapInS Developers Blog

HapInSが提供するエンジニアリングの情報サイト

ソフトウェア開発に守るべき、SOLID原則

はじめに

皆さん、こんにちは!

HapInSアドベントカレンダー2023、10日目です。

今回、ソフトウェア開発に守るべきのSOLID原則をご紹介いたします。🎉

SOLID原則とゆうのは?

SOLID原則についてご存知でしょうか?

SOLID原則は、オブジェクト指向OOP)で採用される五つの原則です。

  • S : 単一責任の原則 (single-responsibility principle)
  • O : 開放閉鎖の原則(open/closed principle)
  • L : リスコフの置換原則(Liskov substitution principle)
  • I : インターフェース分離の原則 (interface segregation principle)
  • D : 依存性逆転の原則(dependency inversion principle)

この原則は、CLEAN CODEと呼ばれる考え方を実現し、ソフトウェアの拡張性や保守性を向上させるためのガイドラインとなっています。

CLEAN CODE 🤔

コードを美しく書くことよりも、慎重に考えてコードを記述することが重要です。

コードが複雑になるにつれて、プロジェクトが拡大するにつれて、保守性や理解可能性はもちろん、品質が低下していきます。

これらの課題に対処するためには、最初から適切にプログラミングを行うことが重要ではないですか。

紹介者

Uncle Bob」として知られるアメリカのソフトウェア技術者、ロバート・C・マーティン(Robert Cecil Martin)が2000年に初めて紹介したことです

有名なクリーンアーキテクチャ(Clean Architecture)も、Uncle Bobさんが紹介してくれたものであり、彼は非常に優れた方であるとされています。

公開されている本も多数ありますので、皆さんが機会があればぜひ一読してみてください。ブログの最後には、彼の有名な二つの本もメンションしています。

五つの原則

詳細を例で紹介しますね。

単一責任の原則 (single-responsibility principle)

クラスは単一の責任を持つべきであり、変更する理由は一つでなければならない。

クラスが多くの責任を持っていると、それが変更される際に他の責任にも影響を与える可能性が高まります。これはコードの理解や保守性を低下させ、バグの発生を促進する可能性があります。単一責任の原則を守ることで、各クラスが明確な役割を果たし、変更が発生した際に影響範囲が限定され、コードの品質が向上します。

この原則を遵守することで、コードの可読性が向上し、保守性が向上するだけでなく、再利用性も高まります。各クラスが特定の目的を果たすように設計されると、そのクラスを他のコードベースで再利用しやすくなります。

例えば、特定の機能や振る舞いが変更される必要がある場合、それに関連するクラスだけを変更すれば良くなり、他のクラスには影響を与えないようになります。

この原則は、以下のような例でよく理解されます:

class GeneralStaff {
    public void serveCustomer() {}
    
    public void washDish() {}
    
    public void cookFood() {}
}

GeneralStaffクラスで全ての役割を一括して処理されるより、WaiterDishWasherChefのようにそれぞれのクラスに分け、各クラスが特定の役割に責任を持つように保守性や拡張性が向上します。

class Waiter {
    public void serveCustomer() {}
}

class DishWasher {
    public void washDish() {}
}

class Chef {
    public void cookFood() {}
}
開放閉鎖の原則(open/closed principle)

ソフトウェアのエンティティ(クラス、モジュール、関数など)は、拡張には開かれていなければなりませんが、修正には閉じていなければなりません。

これは、新しい機能や振る舞いを追加する際には既存のコードを変更せずに行えるようにするという考え方を指しています。具体的には、新しい機能を追加するときには既存のコードを変更するのではなく、既存のコードを拡張することで新しい機能を追加できるようにすべきだという原則です。

この原則に従うことで、既存のコードが変更されない限り、新しい機能を追加することができます。これにより、システムが成長しても既存の安定性が損なわれず、保守性や拡張性が向上します。開放閉鎖の原則は、コードの柔軟性を高め、変更が容易なシステムの構築を支援します。

Chefクラスに新しい機能(餃子を作る)を追加する場合、元々のserve関数を修正すると、予期せぬバグが発生する可能性があります。

class Chef {
    public void serve() {
        System.out.println("ラーメン");
    }
}
// ↓
class Chef {
    public void serve() {
        System.out.println("ラーメン");
        System.out.println("餃子");
    }
}

元々のserve関数を修正する代わりに、新しい機能を追加するためには、serve関数を変更するのではなく、別のserveGyouzaという関数を作成する方が良いでしょう。

class Chef {
    public void serve() {
        System.out.println("ラーメン");
    }
}
// ↓
class Chef {
    public void serve() {
        System.out.println("ラーメン");
    }
    
    public void serveGyouza() {
        System.out.println("餃子");
    }
}
リスコフの置換原則(Liskov substitution principle)

この原則は、派生クラスが基本クラス(親クラス)と互換性があり、基本クラスのインスタンスが置換可能であるべきだと述べています。

基本型(Base Type)のオブジェクトは、それに基づいている導出型(Derived Type)のオブジェクトに置き換えることができるべきである。

これは、基本クラスと派生クラスの間に、互換性があり、派生クラスのインスタンスが基本クラスの代わりに利用できるという意味です。

リスコフの置換原則が守られている場合、プログラムが基本クラスのオブジェクトを使用している箇所で、派生クラスのオブジェクトを代わりに使っても、プログラムの挙動が変わらないことが期待されます。この原則により、クラスの階層構造が一貫性を持ち、柔軟性が向上します。

例えば、以下のような関係がある場合:

class Shape {
}

class Circle extends Shape {
}

class Square extends Shape {
}

ここで、CircleSquareはどちらもShapeのサブクラスであり、リスコフの置換原則が成り立っています。なぜなら、どちらの派生クラスも基本クラスとして振る舞い、互換性があるためです。

インターフェース分離の原則 (interface segregation principle)

この原則は、クライアントは自分が使用しないメソッドに依存すべきではないと主張しています。

クライアントは、自分が使用しないメソッドに依存すべきではない。

これは、クライアント(クラスやモジュールなど)が実際に必要とするメソッドのみを提供する小さなインターフェースを持つべきであるという考え方です。大きな一般的なインターフェースではなく、クライアントが必要な部分だけを含む複数の小さなインターフェースに分割することで、クライアントが不必要なメソッドに依存することを避けることができます。

この原則は、以下のような例でよく理解されます:

interface Worker {
    void doCoding();
    void attendMeeting();
    void reviewCode();
}

class Programmer implements Worker {
    public void doCoding() {}
    
    public void attendMeeting() {}
    
    public void reviewCode() {}
}

class Leader implements Worker {
    public void doCoding() {}
    
    public void attendMeeting() {}
    
    public void reviewCode() {}
}

上記の例では、WorkerインターフェースがdoCodingattendMeetingreviewCodeというメソッドを提供しています。しかし、EngineerLeaderのようなクライアントが必要なのは、それぞれの職務に関連するメソッドだけです。ISPに従うと、これらのクライアントごとに適切なサブインターフェースを作成することが望ましいです。

下記のように書き直す:

interface Worker {
    void attendMeeting();
}

interface Coder {
    void doCoding();
}

interface Reviewer {
    void reviewCode();
}

class Programmer implements Worker, Coder {
    public void doCoding() {}
    
    public void attendMeeting() {}
}

class Leader implements Worker, Reviewer {
    public void reviewCode() {}
    
    public void attendMeeting() {}
}
依存性逆転の原則(dependency inversion principle)

DIPは、高レベルのモジュールは低レベルのモジュールに依存すべきではなく、両方は抽象に依存すべきだと主張します。

高レベルのモジュールは低レベルのモジュールに依存すべきではなく、両方は抽象に依存すべきである。

この原則は、具体的な実装(低レベルのモジュール)に依存せず、抽象(抽象クラスやインターフェース)に依存することで、柔軟性や拡張性を向上させることを目的としています。これにより、コードがより柔軟で変更しやすくなり、新しい機能を追加する際にも既存のコードを変更せずに済むようになります。

以下は、依存性逆転の原則に従った例です:

class LightBulb {
    public void turnOn() {
        
    }
}

class Switch {
    private LightBulb bulb;

    public Switch() {
        this.bulb = new LightBulb();
    }

    public void operate() {
        bulb.turnOn();
    }
}

上記の例では、SwitchクラスがLightBulbクラスに直接依存しています。これを依存性逆転の原則に従って修正すると、次のようになります:

interface Switchable {
    void turnOn();
}

class LightBulb implements Switchable {
    public void turnOn() {
        
    }
}

class Switch {
    private Switchable device;

    public Switch(Switchable device) {
        this.device = device;
    }

    public void operate() {
        device.turnOn();
    }
}

ここで、SwitchクラスはSwitchableという抽象に依存しており、具体的な実装には依存していません。これにより、Switchクラスは異なるデバイスを操作する際にも、新しいデバイスの実装を追加するだけで済む柔軟性が生まれます。

最後に

皆さん、これからどのようなプログラムを書く場合でも、SOLID原則を守るよう心掛けましょう。コーディングの際は、自分だけでなく他の人がコードを理解しやすく、保守しやすいものになるように最初からしっかりと考えましょう。SOLID原則を覚え、実践することで、柔軟で拡張可能なコードを作成できます。良いコーディング習慣を身につけ、ソフトウェアの品質向上に貢献しましょう。

明日もお楽しみに!

Uncle Bobさんの本

www.amazon.co.jp www.amazon.co.jp