SOLID オブジェクト指向プログラミングおよび設計の5つの基本原則

オブジェクト指向プログラミングおよび設計の5つの基本原則である。

  • SRP: 単一責任原則(Single Responsibility Principle)
  • OCP: オープン・クローズド原則(Open-Closed Principle)
  • LSP: リスコフの置換原則(Liskov Substitution Principle)
  • ISP: インターフェース分離原則(Interface Segregation Principle)
  • DIP: 依存性逆転原則(Dependency Inversion)

頭文字を取って、これをSOLIDとも言う。

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

  • ソフトウェアの設計部品(クラス、関数、モジュールなど)は、ただ1つの責任だけを持つべきである。
  • よく設計されたプログラムは、基本的に新しい要求事項によるプログラム変更の影響を受ける部分が少ない。
  • つまり、凝集度が高く、結合度が低いプログラムを意味する。
  • もし1つのクラスが実行できる機能、つまり責任が多くなると、クラス内部の関数同士に強い結合が発生する可能性が高くなる。これは保守コストの増加につながるため、責任を分離する必要がある。
  • クラスは1つの責任を実行し、複数を実行するなら分割する。

単一責任原則に違反した例

以下のコードは従業員ごとの業務に関する例である。

package com.devkuma.tutorial.solid.srp.bad;

public interface Employee {

    // 経理チーム業務
    void calculatePay();

    // 人事チーム業務
    void reportHours();

    // DB管理者業務
    void save();
}

上記のコードでは、3つのメソッドがそれぞれ異なるアクターに対する責任を持っており、SRPに違反している。
つまり、アクターごとに異なるコードは分割されるべきである。

単一責任原則に違反した例の解決策

上記の例に対する解決策は以下のとおりである。

共有データクラス

package com.devkuma.tutorial.solid.srp.good;

// 共有データ
public class Employee {
    private Integer id;
    private String name;
    private Integer salary;

    public Employee(Integer id, String name, Integer salary) {
        this.id = id;
        this.name = name;
        this.salary = salary;
    }
}

人事チーム業務クラス

package com.devkuma.tutorial.solid.srp.good;

// 人事チーム業務
public abstract class HourReporter {

    private Employee employee;

    public HourReporter(Employee employee) {
        this.employee = employee;
    }

    abstract void reportHours();
}

経理チーム業務クラス

package com.devkuma.tutorial.solid.srp.good;

// 経理チーム業務
public abstract class PayCalculator {

    private Employee employee;

    public PayCalculator(Employee employee) {
        this.employee = employee;
    }

    abstract void reportHours();
}

DB管理者業務クラス

package com.devkuma.tutorial.solid.srp.good;

// DB管理者業務
public abstract class EmployeeSaver {

    private Employee employee;

    public EmployeeSaver(Employee employee) {
        this.employee = employee;
    }

    // 開発チームで使用する。
    abstract void save();
}

共有データは1か所に集め、各アクターの異なる関数は、それぞれの業務に合うクラスへ移動させる。

OCP: オープン・クローズド原則(Open-Closed Principle)

  • 既存コードを変更せず(Closed)、機能を修正または追加できるように(Open)設計すべきである。
  • ソフトウェアは拡張(機能)には開かれていなければならず、周辺の変化には閉じていなければならない。コードの直接的な修正は不要であるべきだ。

オープン・クローズド原則に違反した例

以下のコードは従業員情報を管理するサンプルプログラムである。

package com.devkuma.tutorial.solid.ocp.bad;

public class Employee {
    private String description;

    private String[] names;

    public Employee(String description, String[] names) {
        this.description = description;
        this.names = names;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getDescription() {
        return description;
    }

    public void setNames(String[] names) {
        this.names = names;
    }

    public String[] getNames() {
        return names;
    }
}
package com.devkuma.tutorial.solid.ocp.bad;

public class EmployeePrinter {

    public void print(Employee employee) {
        System.out.println(employee.getDescription());
        for (String name : employee.getNames()) {
            System.out.println(name);
        }
    }
}
package com.devkuma.tutorial.solid.ocp.bad;

public class Main {

    public static void main(String[] args) {
        Employee employee = new Employee("従業員情報", new String[]{"devkuma", "araikuma", "kimkc"});
        new EmployeePrinter().print(employee);
    }
}

上記の例では、Employeeオブジェクトのnamesデータ構造が変更されると、EmployeePrinterオブジェクトの実装も変更しなければならない。

オープン・クローズド原則に違反した例の解決策

上記の例に対する解決策は以下のとおりである。

package com.devkuma.tutorial.solid.ocp.good;

public class Employee {
    private String description;

    private String[] names;

    public Employee(String description, String[] names) {
        this.description = description;
        this.names = names;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getDescription() {
        return description;
    }

    public void setNames(String[] names) {
        this.names = names;
    }

    public String[] getNames() {
        return names;
    }

    public void printNames() {
        for (String name : names) {
            System.out.println(name);
        }
    }
}
package com.devkuma.tutorial.solid.ocp.good;

public class EmployeePrinter {

    public void print(Employee employee) {
        System.out.println(employee.getDescription());
        employee.printNames();
    }
}

上記のコードを見ると、Employeeオブジェクトがnamesを反復処理するメソッドを持つようになる。
EmployeePrinterオブジェクトでは、Employeeオブジェクトに変更があってもインターフェースが同じなら、変更なしで拡張可能になった。

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

  • 子クラスは親クラスで可能な振る舞いを実行できなければならない。
  • 特定のメソッドが上位型を引数として使う場合、その型の下位型も問題なく正常に動作しなければならないということである。

リスコフの置換原則の例

以下のAnimalは継承のための最上位クラスである。

package com.devkuma.tutorial.solid.lsp;

public class Animal {

    void run(Integer speed) {
        System.out.println("running at " + speed + " km/h");
    }
}

リスコフの置換原則に従う例

以下のDogオブジェクトはLSPに従っていると言える。


package com.devkuma.tutorial.solid.lsp.good;

import com.devkuma.tutorial.solid.liskovsubstitution.Animal;

public class Dog extends Animal {

    void bark() {
        System.out.println("bow-wow");
    }

    void run(Integer speed) {
        System.out.println("running at " + speed + " km/h");
    }
}

リスコフの置換原則に従わない例

以下のSlothオブジェクトはAnimalで置き換えられないため、LSPに違反していると言える。

package com.devkuma.tutorial.solid.lsp.bad;

import com.devkuma.tutorial.solid.liskovsubstitution.Animal;

public class Sloth extends Animal {

    void run(Integer speed) throws Exception {
        throw new Exception("Sorry, I'm too lazy to run");
    }
}

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

  • 1つのクラスは自分が使わないインターフェースを実装すべきではない。1つの一般的なインターフェースより、複数の具体的なインターフェースのほうがよい。(不要な依存性を除去)
  • つまり、自分が使わない機能(インターフェース)の影響を受けてはならないという意味である。
  • 例えば、Printインターフェースではprintとscanを分離すべきである。

インターフェース分離原則の例

以下のAnimalは実装のためのインターフェースである。

package com.devkuma.tutorial.solid.isp.ex1;

public interface Animal {
    void run();

    void eat();

    void cry();
}

以下のコードでは、DogクラスはAnimalインターフェースのすべてのメソッドを実装しているため問題ない。

package com.devkuma.tutorial.solid.isp.ex1.good;

import com.devkuma.tutorial.solid.isp.Animal;

public class Dog implements Animal {
    @Override
    public void run() {
        System.out.println("run");
    }

    @Override
    public void eat() {
        System.out.println("eat");
    }

    @Override
    public void cry() {
        System.out.println("cry");
    }
}

しかし、Lizardクラスのcryメソッドには処理がなく、Animalインターフェースに不要に依存しているため、インターフェース分離原則に違反していると言える。

package com.devkuma.tutorial.solid.isp.bad;

import com.devkuma.tutorial.solid.isp.ex1.Animal;

public class Lizard implements Animal {
    @Override
    public void run() {
        System.out.println("run");
    }

    @Override
    public void eat() {
        System.out.println("eat");
    }

    // cryメソッドに対する処理がない。Animalに不要に依存している。
    @Override
    public void cry() {
        // Don't call this method
    }
}

インターフェース分離原則の解決策

解決策として、以下のように共通部分だけを抜き出し、より細かいインターフェースに分離することで不要な依存をなくすことができる。

以下のAnimalは実装のための最上位インターフェースである。

package com.devkuma.tutorial.solid.isp.ex2;

public interface Animal {
    void run();

    void eat();
}

Mammal(哺乳類)インターフェースでは、Animalインターフェースを継承してcryメソッドを新しく追加した。

package com.devkuma.tutorial.solid.isp.ex2;

interface Mammal extends Animal {

    void cry();
}

Reptile(爬虫類)インターフェースでは、Animalインターフェースを継承し、別のメソッドは作らなかった。

package com.devkuma.tutorial.solid.isp.ex2;

interface Reptile extends Animal {
}

Dogクラスでは、Mammalインターフェースのruneatメソッドを実装し、Mammalインターフェースのcryも実装した。

package com.devkuma.tutorial.solid.isp.ex2;

public class Dog implements Mammal {
    @Override
    public void run() {
        System.out.println("run");
    }

    @Override
    public void eat() {
        System.out.println("eat");
    }

    @Override
    public void cry() {
        System.out.println("cry");
    }
}

Lizardクラスでは、Reptileインターフェースのruneatメソッドを実装した。

package com.devkuma.tutorial.solid.isp.ex2;

public class Lizard implements Reptile {
    @Override
    public void run() {
        System.out.println("run");
    }

    @Override
    public void eat() {
        System.out.println("eat");
    }
}

DIP: 依存性逆転原則(Dependency Inversion)

  • 依存関係を結ぶとき、変わりやすいものより変わりにくいものに依存すべきという原則である。
  • 上位モジュールは下位モジュールに依存してはならず、両方とも抽象に依存しなければならない。(下位モジュールの変更が上位モジュールに影響しないようにする)
  • 抽象(Interfaces/Abstractionクラス)は実装の詳細(Class)に依存してはならず、実装の詳細が抽象に依存しなければならない。

依存性逆転原則の例

以下のHttpClientは実装のための上位抽象クラスである。

package com.devkuma.tutorial.solid.dip;

abstract public class HttpClient {

    public String get(String arg) {
        return arg;
    }
}

以下のコードでは、DataProvider(上位モジュール)がCustomHTTPClient(下位モジュール)に依存した状態であり、DIPの原則に反していると言える。

package com.devkuma.tutorial.solid.dip.good;

import com.devkuma.tutorial.solid.dip.HttpClient;

public class CustomHttpClient extends HttpClient {

}
package com.devkuma.tutorial.solid.dip.good;

import com.devkuma.tutorial.solid.dip.HttpClient;

public class DataProvider {

    public HttpClient httpClient;

    public DataProvider(CustomHttpClient customHttpClient) {
        this.httpClient = customHttpClient;
    }

    public String getData() {
        return httpClient.get("URL");
    }
}

依存性逆転原則の解決策

モジュールが抽象(Abstract)に依存するようにする。

package com.devkuma.tutorial.solid.dip.bad;

import com.devkuma.tutorial.solid.dip.HttpClient;
import com.devkuma.tutorial.solid.dip.good.DataFetchClient;

public class DataProvider {
    public HttpClient httpClient;

    public DataProvider(HttpClient httpClient) {
        this.httpClient = httpClient;
    }

    public String getData() {
        return httpClient.get("URL");
    }
}

参考