オープン・クローズドの原則 - TypeScriptで学ぶSOLID原則 part 1

SOLID原則を勉強中です。
TypeScriptでOCPを説明している記事があまりなかったので、自分なりにまとめてみました。

# オープン・クローズドの原則（OCP）とは？
> software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification


`クラス・モジュール・関数は「拡張」に対して開いていて、「修正」に対して閉じていなければならない` という原則です。
これだけ読んでも「拡張に対して開いて、修正に対して閉じている？　何も分からん🤔」という感じですが、
自分の中では`機能の拡張の際、既存コード修正せず、新しくコードを追加するだけで対応できる設計にすること`だと解釈しています。


以下の仕様でプログラムを書くと仮定して、OCPに違反したコード・遵守したコードを例に挙げながら、OCPを守るとどのような利点があるのか解説していきます。

```
- 従業員のボーナスを計算するシステム
- `BonusManager` クラスの `calculate` メソッドで、従業員全員のボーナス合計を計算する
- 個々のボーナスは、従業員の等級（`grade`）によって月給（`salary`）に一定の係数を乗算した値とする
```

# 👎 Bad

まずOCPに違反しているコードを。
BonusManagerでボーナスを計算する際に、reduceの内部でswitch文によって条件分岐しています。

```typescript
enum Grade {
  ANALYST = "Analyst",
  CONSULTANT = "Consultant",
}

class Employee {
  constructor(public grade: Grade, public salary: number) {}
}

class BonusManager {
  calculate(employees: Employee[]) {
    return employees.reduce((total, employee) => {
      let bonus: number;

      // 等級によってボーナスを計算
      switch (employee.grade) {
        case Grade.ANALYST:
          bonus = employee.salary * 2;
          break;
        case Grade.CONSULTANT:
          bonus = employee.salary * 3;
          break;
      }

      return total + bonus;
    }, 0);
  }
}

const employees = [
  new Employee(Grade.ANALYST, 100),
  new Employee(Grade.CONSULTANT, 200),
  new Employee(Grade.ANALYST, 150),
];

const bonusManager = new BonusManager();
console.log(bonusManager.calculate(employees));
```

ありがちなコードですが、実はこちらはOCPに違反しています。

例えば、新たにEmployeeの等級に`partner`を追加しようとした場合、`BonusManager#calculate`のswitch文に新たなcaseの追加が必要になります。
この部分は既存コードの重要なロジック部分です。
`既存コードに修正は加えず` というのが守れていません。

`break`文の書き忘れなど、凡ミスで今まで動いていたコードが壊れる可能性もあります。
影響範囲の確認も大変です。

（等級を追加した場合）

```typescript
class BonusManager {
  calculate(employees: Employee[]) {
    return employees.reduce((total, employee) => {
      let bonus: number;

      // 等級によってボーナスを計算
      switch (employee.grade) {
        case Grade.ANALYST:
          bonus = employee.salary * 2;
          break;
        case Grade.CONSULTANT:
          bonus = employee.salary * 3;
          break;
        // パートナーの条件分岐を追加。既存のコードに修正が必要
        case Grade.PARTNER:
          bonus = employee.salary * 3;
          break;
      }

      return total + bonus;
    }, 0);
  }
}
```

# 👍 Good
次にOCPを遵守したコードです。

```typescript
enum Grade {
  ANALYST = "Analyst",
  CONSULTANT = "Consultant",
}

// Employeeのインターフェース
interface IEmployee {
  grade: Grade;
  salary: number;
  calculateBonus: () => number;
}

// IEmployeeを実装したコンサルタントのクラス
class Consultant implements IEmployee {
  private BONUS_COEFFICIENT = 3;
  grade = Grade.CONSULTANT;

  constructor(public salary: number) {}

  calculateBonus() {
    return this.salary * this.BONUS_COEFFICIENT;
  }
}

// IEmployeeを実装したアナリストのクラス
class Analyst implements IEmployee {
  private BONUS_COEFFICIENT = 2;
  grade = Grade.ANALYST;

  constructor(public salary: number) {}

  calculateBonus() {
    return this.salary * this.BONUS_COEFFICIENT;
  }
}

class BonusManager {
  calculate(employees: IEmployee[]) {
    return employees.reduce((total, employee) => {
      // 各IEmployee実装クラスのcalculateBonusをコールするだけ
      return total + employee.calculateBonus();
    }, 0);
  }
}

const employees = [new Analyst(100), new Consultant(200), new Analyst(150)];

const bonusManager = new BonusManager();
console.log(bonusManager.calculate(employees));
```

従業員のインタフェース`IEmployee`を追加し、それを実装したAnalystクラスとConsultantクラスを定義しています。
また、それぞれにcalculateBonusメソッドを作り、自身でボーナスが計算できるようになっています。

そしてBonusManagerのcalculateからswitch文は消え、
代わりにIEmployeeのbonusCalculateメソッドを呼ぶだけになりました。
クラスは増えましたが、条件分岐はなくなり大分シンプルですね。

この状態であれば新しく`partner`等級を追加する場合も、新たにIEmployeeを実装したPartnerクラスを定義し、BonusManagerに渡すだけで機能拡張が完了します。
既存コードのBonusManagerの内部のロジックに修正は必要ありません。
影響範囲の確認も楽ですね。

（等級を追加した場合）

````typescript
// IEmployeeを実装したPartnerクラスを作成して、そのままcalculateに渡すだけ。
// BonusManagerのcalculateの修正は不要
class Partner implements IEmployee {
  private BONUS_COEFFICIENT = 4;
  grade = Grade.PARTNER;

  constructor(public salary: number) {}

  calculateBonus() {
    return this.salary * this.BONUS_COEFFICIENT;
  }
}

```


# 終わりに
以上、オープン・クローズドの原則の解説でした。
if・switchでの条件分岐はやりがちですが、オープン・クローズドの原則を鑑みれば改善できるケースも多そうです。

自分の理解を深めるためにまとめてみたもののこれで合っているのか不安あります。
誤った点などあれば気軽にコメントお願いします！ :pray: 

また他のSOLID原則についても勉強次第随時まとめていきたいです。

# 参考
- [SOLID - Wikipedia](https://en.wikipedia.org/wiki/SOLID)
- [SOLID Principles: The Software Developer's Framework to Robust & Maintainable Code [with Examples] | Khalil Stemmler](https://khalilstemmler.com/articles/solid-principles/solid-typescript/)
- [オブジェクト指向設計原則とは - Qiita](https://qiita.com/UWControl/items/98671f53120ae47ff93a#%E3%83%AA%E3%82%B9%E3%82%B3%E3%83%95%E3%81%AE%E7%BD%AE%E6%8F%9B%E5%8E%9F%E5%89%87)
- [よくわかる SOLID 原則 1: S（単一責任の原則）｜ erukiti ｜ note](https://note.com/erukiti/n/n67b323d1f7c5)
- [オープン・クローズドの原則の重要性について \- Eureka Engineering \- Medium](https://medium.com/eureka-engineering/go-open-closed-principle-977f1b5d3db0)- [Design Patterns: Open Closed Principle Explained Practically in C# (The O in SOLID) - YouTube](https://www.youtube.com/watch?v=VFlk43QGEgc)
- [「SOLID の原則ってどんなふうに使うの？オープン・クローズドの原則編 拡大版」　 後藤英宣 - YouTube](https://www.youtube.com/watch?v=cUV1nXPfjFY)