きれいなコードを書くためにSOLID原則を学びました② ~オープン・クローズドの原則~


今回はSOLIDのオープン・クローズドの原則についてまとめました。

その他の記事は以下。
きれいなコードを書くためにSOLID原則を学びました① ~単一責任の原則~
きれいなコードを書くためにSOLID原則を学びました③ ~リスコフの置換原則~
きれいなコードを書くためにSOLID原則を学びました④ ~インターフェース分離の原則~
きれいなコードを書くためにSOLID原則を学びました⑤ ~依存関係逆転の原則~

オープン・クローズドの原則

Software components should be closed for modification, but open for extension

これを直訳すると、「ソフトウェアのコンポーネントは、修正に対しては閉じていて、拡張に対しては開けているべきである。」という意味になります。

任○堂の人気ゲーム機を例として考えてみましょう。

このゲーム機は完成度が非常に高く、本体だけあればほとんどのゲームを楽しむことができます。ただ、本体だけだと、「マリオ○ートをハンドル操作したーい」とか「オフラインで4人対戦がしたーい」みたいなニーズには答えられません。

しかし、天下のNINT○NDOはそんなことを百も承知。専用の拡張オプションを取り付けるだけで、これらのニーズに対応することができます。

また、仮にユーザが本体のスペックアップを図ろうとしても、中身をこじ開けるのが難しいような構造になっています。

このように、様々な拡張機能だけでユーザのニーズを満たすことができ、改造ができないような構造になっているのが、オープン・クローズドな設計であるということになります。

ソフトウェアにおいては、以下のように言い換えることができます。
・ソフトウェアコンポーネント(オブジェクト指向でいうクラス)への新規機能の追加において、既存のコードを修正するべきではない。
・新機能の追加に対しては拡張性が高くなっているべきである。

特別な顧客に対して保険料の割引率を計算するInsuranceDiscountCalculatorクラスを例に考えてみます。

calculateDiscountPercentメソッドの引数にMedicalInsuranceCustomerProfileのcustomerインスタンスをとり、メソッド内部で医療保険の割引を受けられる顧客かどうかを判定しています。

public class InsuranceDiscountCalculator {
  public int calculateDiscountPercent(MedicalInsuranceCustomerProfile customer)
  {
    if (customer.isLoyalCustomer()) {
      return 20;
    }
    return 0;
  }
}

public class MedicalInsuranceCustomerProfile {
  public boolean isLoyalCustomer() {
    return true;
  }
}

ここに自動車保険の割引を加えるとしたらどうなるでしょうか。
オープン・クローズドの原則が頭にない方は、「簡単じゃん!医療保険の計算とやってること同じなんだから、クラスとメソッドをコピペして名前を変えるだけっと...」という感じで以下のようにすることでしょう。

public class InsuranceDiscountCalculator {
  public int calculateDiscountPercent(HealthInsuranceCustomerProfile customer)
  {
    if (customer.isLoyalCustomer()) {
      return 20;
    }
    return 0;
  }
  public int calculateDiscountPercent(VehicleInsuranceCustomerProfile customer)
  {
    if (customer.isLoyalCustomer()) {
      return 20;
    }
    return 0;
  }
}

public class HealthInsuranceCustomerProfile {
  public boolean isLoyalCustomer() {
    return true;
  }
}

public class VehicleInsuranceCustomerProfile {
  public boolean isLoyalCustomer() {
    return true;
  }
}

これはダメですね。
なぜなら、既存のクラス(InsuranceDiscountCalculator)をいじってしまっており、原則に違反しているからです。

ではどうすればいいのかというと、ここでインターフェースというものを利用します。

インターフェースとは、クラスに含まれるメソッドの具体的な処理内容を記述せず、変数とメソッドの型のみを定義したものです。

public interface CustomerProfile {
  public boolean isLoyalCustomer();
}

インターフェースをimplementsとしてクラスに実装します。

public class HealthInsuranceCustomerProfile implements CustomerProfile {
  public boolean isLoyalCustomer() {
    return true;
  }
}

public class VehicleInsuranceCustomerProfile implements CustomerProfile {
  public boolean isLoyalCustomer() {
    return true;
  }
}

すると、インターフェースが実装されたクラスのインスタンスであれば、メソッドの引数としてとれるようになります。

public class InsuranceDiscountCalculator {
  public int
  calculateDiscountPercent(CustomerProfile customer)
  {
    if (customer.isLoyalCustomer()) {
      return 20;
    }
    return 0;
  }
}

生命保険や健康保険の割引対象顧客の判定が必要になったとしても、Customerインターフェースを実装するだけでOKです。どのクラスの中身もいじる必要はありません。

以上の例のように、オープン・クローズドの原則というのは、「インターフェースを利用することで、変更の影響を受けずにシステムを拡張しやすい設計にすること」と同義になります。

おわりに

オープン・クローズドの原則を意識しすぎると、クラスが増えすぎて全体の設計が複雑になってしまう場合があります。この問題を解消するために、各クラスの役割や依存関係を考える必要があるのですが、こちらについては別の記事で書こうと思います。

参考書籍・動画

Crean Architecture Robert.C.Martin
SOLID Principles: Introducing Software Architecture & Design Sujith George