開放閉鎖の原則とリスコフ置換(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)が求める設計として正しいものはどれか?