設計原則 レッスン2

開放閉鎖の原則とリスコフ置換(OCP/LSP)

拡張に開き修正に閉じるOCPと、派生型が基底型と置換可能であるべきというLSPを具体例で学ぶ

開放閉鎖の原則(OCP)とは

開放閉鎖の原則(Open/Closed Principle)は、 「拡張には開いていて、修正には閉じている」設計を目指す原則です。 新しい振る舞いを追加するとき、既存のコードを書き換えるのではなく、 コードを追加するだけで対応できる状態が理想とされます。

// 悪い例: 割引種別が増えるたびにこの関数を書き換える必要がある
function calculateDiscount(type, price) {
  if (type === "seasonal") {
    return price * 0.9
  } else if (type === "member") {
    return price * 0.85
  }
  // 新しい割引種別を追加するたびにここを修正することになる
  return price
}
// 良い例: 新しい割引ルールは既存コードを変更せず追加できる
interface DiscountRule {
  apply(price: number): number
}

class SeasonalDiscount implements DiscountRule {
  apply(price) { return price * 0.9 }
}

class MemberDiscount implements DiscountRule {
  apply(price) { return price * 0.85 }
}

// 新しい割引を追加したい場合は、新しいクラスを追加するだけでよい
class ClearanceDiscount implements DiscountRule {
  apply(price) { return price * 0.5 }
}

function calculateDiscount(rule: DiscountRule, price: number) {
  return rule.apply(price)
}

ポイント

OCPは「絶対に修正してはいけない」という意味ではありません。 将来どのような変化が起こりやすいかを見極め、変化の起きやすい部分を拡張ポイントとして 設計しておく、という実践的な指針です。

リスコフ置換の原則(LSP)とは

リスコフ置換の原則(Liskov Substitution Principle)は、 「基底型のオブジェクトを使っているコードは、その派生型に置き換えても正しく動作すべき」 という原則です。継承関係が「is-a」の形をしているかどうかだけでなく、振る舞い上の約束事が守られているかを問います。

LSP違反の古典例: 正方形と長方形

数学的には「正方形は特殊な長方形である」と言えますが、 プログラム上でRectangleを継承したSquareを作ると、しばしばLSP違反が起きます。

class Rectangle {
  setWidth(w) { this.width = w }
  setHeight(h) { this.height = h }
  area() { return this.width * this.height }
}

// Squareは幅と高さが常に等しくなければならない制約を持つ
class Square extends Rectangle {
  setWidth(w) {
    this.width = w
    this.height = w // 幅を変えると高さも変わってしまう
  }
  setHeight(h) {
    this.width = h
    this.height = h
  }
}

// Rectangleを前提にしたコード
function resize(rect) {
  rect.setWidth(4)
  rect.setHeight(5)
  console.log(rect.area()) // Rectangleなら20を期待するが、Squareを渡すと25になる
}

呼び出し側は「setWidthとsetHeightは独立して設定できる」というRectangleの前提を信じてコードを書いています。 Squareに差し替えるとその前提が崩れるため、これはLSP違反です。 継承関係を作る前に「基底型が保証している振る舞いを、派生型も本当に守れるか」を検討する必要があります。

OCPとLSPの関係

OCPは「拡張ポイントを作って新しい振る舞いを追加できるようにする」原則ですが、 その拡張(派生型やインターフェース実装)がLSPに違反していると、 拡張したはずのコードが既存の呼び出し元を壊してしまいます。 OCPを実現するための拡張は、LSPを満たして初めて安全に機能します。

ポイント

  • • OCPは「拡張には開き、修正には閉じている」設計を目指す原則
  • • 変化しやすい部分をインターフェースなどの拡張ポイントとして設計する
  • • LSPは「派生型に差し替えても呼び出し側が正しく動作すべき」という原則
  • • 継承関係は「is-a」の形だけでなく、振る舞い上の前提が守られているかで判断する
  • • OCPで作った拡張がLSPに違反していると、追加したはずのコードが呼び出し元を壊す

確認クイズ

1 / 3

開放閉鎖の原則(OCP)が求める設計として正しいものはどれか?