Solid原則シリーズ 開放閉鎖の原則とは ~何でもかんでもifはあかん~

Uncategorized

はじめに

前回の「単一責任の原則 〜神クラスを避けよう〜」では、1つのクラスに役割を持たせすぎることの危険性に触れました。
実はそこで紹介したサンプルコードの「WorksoutAdviser」クラスにはもう一つの重要なルールである「開放閉鎖の原則」にも違反しています。
何がいけないのか、そのクラスを基に説明していきます。

class WorkoutAdvisor {
  getWorkoutAdvice(targetMuscle: string): string {
    if (targetMuscle === "胸") {
      return "ベンチプレスを3セット、各10回行ってください。";
    }
    if (targetMuscle === "背中") {
      return "ラットプルダウンを3セット、各12回行ってください。";
    }
    if (targetMuscle === "脚") {
      return "スクワットを3セット、各15回行ってください。";
    }
    if (targetMuscle === "肩") {
      return "ショルダープレスを3セット、各10回行ってください。";
    }
    return "ストレッチから始めましょう。";
  }
}

開放閉鎖の原則(Open/Closed Principle)とは

開放閉鎖の原則は既存のコードを修正せず、新しいコードを追加するだけで対応できるように設計する原則です。

何が開放?閉鎖?っていうと以下のイメージです
開放→拡張に対して開いている(容易に追加できる)
閉鎖→修正に対して閉じている(既存コードに影響がない)

開放閉鎖の原則がないとどうなる?

WorkoutAdviserクラスを例に今の状態だと何がいけないのか説明します。

修正範囲が増える

getWorkoutAdviceメソッドでトレーニング部位が増えたらこのままifが増えていくことになりますよね?それによってメソッドの中身がどんどん膨らんでいきます

デグレのリスクが増える

新しい部位を増やすために同じメソッドをいじることになるので気づかないうちに誤って既存のロジックを壊してしまい、なぜか動かなくなるみたいな状態になることもあります。

テスト工数が増える

get WorkoutAdviceという一つの塊の中を修正してるわけなので部位を増やした場合、そのテストだけでなく元からある部位のテストもし直す必要があります。

そのため、開放閉鎖の原則を守らないと、テストのケースがどんどん増えていき、テストコードの管理もこんなになっていきます。

品質保証のためのテストなのにそれでは本末転倒です、、、。

開放閉鎖の原則を守ってみると

開放閉鎖の原則に基づいて修正すると以下のようになります。

interface WorkoutStrategy {
  isMatch(targetMuscle: string): boolean;
  getAdvice(): string;
}

class ChestWorkout implements WorkoutStrategy {
  isMatch(targetMuscle: string): boolean { return targetMuscle === "胸"; }
  getAdvice(): string { return "ベンチプレスを3セット、各10回行ってください。"; }
}

class BackWorkout implements WorkoutStrategy {
  isMatch(targetMuscle: string): boolean { return targetMuscle === "背中"; }
  getAdvice(): string { return "ラットプルダウンを3セット、各12回行ってください。"; }
}

class LegWorkout implements WorkoutStrategy {
  isMatch(targetMuscle: string): boolean { return targetMuscle === "脚"; }
  getAdvice(): string { return "スクワットを3セット、各15回行ってください。"; }
}

class WorkoutAdvisor {
  private strategies: WorkoutStrategy[];

  constructor(strategies: WorkoutStrategy[]) {
    this.strategies = strategies;
  }

  getWorkoutAdvice(targetMuscle: string): string {
    const strategy = this.strategies.find(s => s.isMatch(targetMuscle));
    return strategy ? strategy.getAdvice() : "ストレッチから始めましょう。";
  }
}

共通処理はinterfaceで切り出し各部位ごとのクラスを実装して、interfaceを継承する形にしています。
これによって以下のメリットがあります。

拡張性がある

上記のコードであれば、例えば部位「肩」を追加したい場合、sholderWorkOutクラスを作成し、それをWorkOutAdviserに渡すだけで済みます。既存のWorkOutAdviserには一切手を加えなくても追加できるのです。修正するときもgetWorkoutAdviceには一切手を加えず、各部のクラスに対しておこなうので「閉じている」ものになっています。

テストがしやすい

各部位ごとにテストを書けばよくなるので追加した部位だけテストをすればOKになります。 以前のコードのように部位を追加する度にすべての部位に対してテストをし直すなんで手間はなくなります。

可読性があがる

部位を追加する度にif文の長い羅列がなくなるのでコードも見やすくなります。

開放閉鎖の原則の注意点

「これなら最初からifじゃなくてこの書き方にすればいいじゃん」と思うかもしれませんが、そこは見極める必要があります。

修正前のコードもメリットはあり、1つのメソッドだけ見れば処理が完結していました。分岐が2,3個くらいしかないのにクラスに分けたところで処理を追うときにジャンプする必要があるのでそれなら修正前のコードのようにifまたはswitchでまとめたほうが見やすいと思います。

以下の観点で一度考えてみて検討するのがよさそうです!

  • 将来的に分岐が増える処理なのか、しばらく増えない処理なのか
  • 変更が3回ほど行われているのかどうか

まずはシンプルにifやswitchで書いてみて「分岐に追加が多いな」とか「条件分岐が長すぎて読みづらくなってきた、、、」とか感じ始めたときがタイミングです。

終わりに

上記で説明はしてきましたが僕も以前、if文に頼ってしまい、分岐が多すぎるコードを書いてしまっていました、、、。
ですが、開放閉鎖の原則を知ることで「なんか形としてはきれいだけど分岐多くて醜いな」みたいな何となく言語化できない違和感にも対応できるようになったので、より見やすいコードを書くための見通しも立つようになりましたし、
生成AIに頼むにしても「開放閉鎖の原則基づいて、、、」みたいな形でプロンプトを書くとないときと比べていいコードが出力されるようにもなった気がします。

次回は三つ目の「リスコフの置換原則」について紹介します。また見てくださるとうれしいです!

コメント

タイトルとURLをコピーしました