
新機能追加のたびに、なんか動いてたコードが死にかけてない?🫠
これ絶対あるじゃん??
- 「PayPay対応にして」って言われて決済クラス開いたら、
if payment_type ==から始まるelif文が6行並んでた - 「楽天Pay追加して」→「LINE Pay追加して」→そのたびに同じファイルを開いて書き足してる
- テスト全部通したのに、本番で別の支払い方法が突然壊れた
- 機能を「追加」したつもりが、触ってない既存機能を「変えちゃってた」
これがOCP Violation(開放閉鎖原則違反)、以降「上書き地獄」って呼ぶね、が原因なんだよね。
てかシンプルに言うとね
上書き地獄(Open/Closed Principle違反。以降、上書き地獄って呼ぶね)ってのは、**「新しい機能を足すたびに、すでに動いてるコードを書き換えなきゃいけなくなっちゃってる状態」**のことなんだよね。
それの何がまずいの?ってなる気持ち、わかる!書き換えてるだけじゃん、って思うよね。でもさ、「動いてたコードに手を入れる」って、そのたびに「壊していいですよ」って宣言してるのと同じなんだよね。
ちょっと待って、先に全然関係ない話していい?🏠
東京の恵比寿に「ビストロ田中」っていうレストランがあったんだけどさ。
オーナーの田中さんは料理が得意で、2019年にオープン。最初はパスタとピザの2メニューだけ。注文を受けたらシェフが厨房で作る、シンプルな仕組みだったの。
そのうちお客さんから「リゾットもあれば」って声が出たじゃん。田中さん「じゃあ追加しよう」ってなって、厨房を改修した。リゾット用の鍋を置く場所を作るために、ピザ窯をちょっと動かして、作業台を1枚外したんだよね。
「田中さん、なんかパスタの火加減が前と違う気がするんですけど…」
「え?作業台外しただけなのに…」
微調整して解決。まあいっか、ってなった。
翌年、テイクアウト需要が出たんだよね。「弁当メニューも作ろう」。またリフォーム。弁当用の保温ケースを入れるために、リゾット用の鍋の位置をずらして、ガスの配管も変えた。
「田中さん!リゾットが焦げ付くようになったんですけど!!」
その夜、田中さんは深夜まで厨房に残った。ガス圧を調整しながら、*「保温ケース入れただけなのに、なんでリゾットが壊れるんだ…」*と思いながら。
2022年、デリバリー対応。またリフォーム。バイク配達用に素早くパッケージする台が必要で、今度は……
Googleレビューに書かれた。「最近、料理の質が落ちた気がします。以前は完璧だったのに。」
田中さんは半年後に辞めたんだって。料理じゃなくて、厨房の修理しかしてなかったから。料理したくて始めたのに、修理しかしてないってもう逆じゃん。
これが本番で起きてるやつが、上書き地獄ってこと。
じゃあコードで見てみよっか👀
# 上書き地獄バージョン
class PaymentProcessor:
def process(self, payment_type: str, amount: int):
if payment_type == "cash":
print(f"現金 {amount}円 を受領")
elif payment_type == "credit_card":
print(f"クレジットカードで {amount}円 を決済")
elif payment_type == "paypay":
print(f"PayPayで {amount}円 を決済")
# 「楽天Pay追加して」→ ここにelifを足す
# 「LINE Pay追加して」→ またここにelifを足す
# 「メルペイ追加して」→ またここに…
楽天Pay追加のとき、何が起きるか見てみて?
PaymentProcessorを開く。elifを書き足す。cashとcredit_cardの処理も同じファイルに入ってる。うっかりインデントがずれる。
クレカ払いが全員エラー。深夜のSlack通知。「決済できないんですけど!?」の問い合わせが30件。翌朝、顧客に謝罪文を書く。えぐくない?
わたしも昔これやったから言えるんだけど、「elifひとつ足しただけ」って思ってても、同じファイルの他の処理を普通に壊す。
やばいのまだあんだけど、バグに気づけない問題😭
「壊れやすい」だけじゃないんよ。「壊れても気づけない」のがほんとにヤバいとこ。
田中さんの厨房の話に戻るとさ、「リゾットの火加減が変わった」に最初に気づいたのはお客さんだったじゃん。田中さんは保温ケースを入れたことしか確認してないから、リゾットが壊れてるのが見えてないんだよね。
コードでも同じことが起きるの。
def test_楽天pay追加後():
processor = PaymentProcessor()
processor.process("rakuten_pay", 1000)
# これだけ通っても、クレカが壊れてるかもしれない
# 安全にするには全パターンのテストが必要
def test_全支払いパターン():
# cash × 全金額パターン
# credit_card × 全金額パターン
# paypay × 全金額パターン
# rakuten_pay × 全金額パターン
# line_pay × 全金額パターン
# merpay × 全金額パターン
# → 支払い方法が1個増えるたびに、全組み合わせのテストが膨らむ
pass
支払い方法が6種類になると、確認しないといけない組み合わせも6倍の方向に増えるじゃん。しかも「新しいelifを足したとき、古い処理が壊れてないか」を全部追わないといけないから、どこかで絶対見落とすんだよね。
上書き地獄は壊れやすいだけじゃなく、バグを隠す設計でもあるってこと。
分けてた会社の話もするね〜✨
同じ恵比寿に、別の「ビストロ鈴木」があったんだけどさ。
鈴木さんはオープンのとき、こう決めてたの。「厨房の基本レイアウトは変えない。新メニューは、専用の調理ステーションを追加する形で対応する」って。
リゾット対応 → リゾット専用ステーションを厨房の端に追加。既存の作業台は動かさなかった。
テイクアウト対応 → 保温ステーションを追加。リゾットのステーションには触れなかった。
デリバリー対応 → パッケージングステーションを追加。保温もリゾットもピザも、何もいじらなかった。
Googleレビュー:「何年経っても料理の質が変わらない。すごい。」
深夜の修理がなかった。クレームがなかった。既存メニューを確認し直す必要もなかった。マジで何も壊れてないの。
コードで見ると、こういうことなんだよね。
from abc import ABC, abstractmethod
# 「支払い処理の型」を定義する(ここはいじらない)
class PaymentMethod(ABC):
@abstractmethod
def pay(self, amount: int):
pass
# 各支払い方法は「追加するだけ」で既存コードに一切触らない
class CashPayment(PaymentMethod):
def pay(self, amount: int):
print(f"現金 {amount}円 を受領")
class CreditCardPayment(PaymentMethod):
def pay(self, amount: int):
print(f"クレジットカードで {amount}円 を決済")
class PayPayPayment(PaymentMethod):
def pay(self, amount: int):
print(f"PayPayで {amount}円 を決済")
# 楽天Pay追加 → 新しいクラスを作るだけ。上の3クラスは1行もいじらない
class RakutenPayPayment(PaymentMethod):
def pay(self, amount: int):
print(f"楽天Payで {amount}円 を決済")
class PaymentProcessor:
def process(self, payment_method: PaymentMethod, amount: int):
payment_method.pay(amount)
ちょっと待って、これ見覚えない?👀
第2回で作ったMaterialSupplier、覚えてる?
# 第2回のやつ(再掲)
class MaterialSupplier(ABC):
@abstractmethod
def get_stock(self, item_code: str) -> int:
pass
class TakahashiMaterialClient(MaterialSupplier):
def get_stock(self, item_code: str) -> int: ...
class MinamiTradingAdapter(MaterialSupplier):
def get_stock(self, item_code: str) -> int: ...
構造、完全に同じじゃん。
抽象クラスを真ん中に置いて、具体的な実装をそれぞれのクラスに分ける。あのとき「ベタ付きを直す」ためにやった設計が、今回の上書き地獄まで同時に解決してたんだよね。
第2回は「ベンダーが変わってもOrderProcessorが壊れない」を解決したくてあの形にした。今回は「支払い方法が増えてもPaymentProcessorを書き換えなくていい」を解決したくてあの形にした。
ゴールは全然別の話なのに、答えの形が同じになった。これがベタ付きと上書き地獄のえぐいとこなんだよね。正式名称は次のセクションで説明するね。
楽天Payを追加するとき、現金払いもクレカも、PayPayも1行もいじってない。触れてないから、壊れようがない。
ちゃんとした名前もあるから一応言うね📚
さっき「正式名称は次のセクションで」って言ったやつ、ここで説明するね。
Open/Closed Principle(以降OCP)とは、「新しい機能は追加するだけでOK、でも今動いてるコードは書き換えるな」っつーこと。これが「上書き地獄」の正式名称ね。
最初に言い出したのはBertrand Meyer(バートランド・メイヤー)って人で、1988年の本『Object-Oriented Software Construction』が最初。その後、第2回でも出てきたアンクルボブがSOLID原則の「O」としてまとめて今の形になったやつね。
で、第2回の「ベタ付きを直す」解決策はDIPだったじゃん。OCPはDIPと何が違うの?ってなるよね。
カンタンに言うと——
DIPは「誰にべったりくっついてるか」の話。
OCPは「追加するとき何をいじっちゃうか」の話。
直し方は似てるけど、モヤってるポイントが違うの。難しそうに聞こえるけど、要は「追加するとき、動いてるコードいじらなくてよくなってる?」って聞き続けるだけなんだよね。
これだけ覚えて帰って👌
合言葉はこれ。ifow(if足すのおわってる)
コードを書くとき・レビューするとき、この問いを使って。
「このクラスに変更が入るとしたら、どんなビジネスの出来事があったとき?」
上書き地獄バージョンのPaymentProcessorで考えると:
- 「新しい支払い方法が増えた」→ 変更が入る
- 「既存の支払い処理にバグがあった」→ 変更が入る
- 「決済金額の計算ルールが変わった」→ 変更が入る
3種類の出来事があるってことは、このクラスが3つの理由で変わるってこと。3種類の「壊れ方の入口」が同じ場所にまとまってる状態なんだよね。
第2回のOrderProcessorでも全く同じことをやったじゃん。「ベンダーが変わる出来事」と「発注ロジックが変わる出来事」を分けた。あのときの感覚と完全に同じ。
分割後のバージョンだと、「PayPayの処理を変えて」って出来事はPayPay専用クラスだけに届く。現金払いのクラスにも、クレカのクラスにも何も届かない。出来事と変更場所が1対1になってる。
レビューで「このクラス、何種類の出来事で変わる?」って聞いて、3個以上出てきたら分割のサインなんだよね。
まとめるとこういうことね
| OCP違反(上書き地獄) | OCP準拠(拡張で対応) | |
|---|---|---|
| 新機能追加のとき | 既存コードを開いて書き換える | 新しいクラスを追加するだけ |
| 既存機能への影響 | 同じファイルをいじるので壊れる | 既存コードは1行もいじらない |
| テストの範囲 | 全パターンを確認し直さないといけない | 追加した部分だけテストすれば済む |
| 変更の理由 | 複数の出来事が1箇所にまとまる | 出来事と変更場所が1対1 |
| DIPとの関係 | ベタ付きのまま → 上書き地獄になる | 抽象を挟む(DIP)→ OCPも同時に達成される |
「間にルールを挟む」っていうやり方はDIPでもOCPでも一緒。でも第2回は「誰にべったりか」にモヤってて、今回は「追加するたびにどこまで巻き込まれるか」にモヤってた。同じ薬が、別の症状に効いたっつーこと。
沼りたい人はこっちも読んで📖
権威ある書籍
- Robert C. Martin『Clean Architecture』(2017) — OCPを含むSOLID原則を設計全体の文脈で解説。第2回と合わせて読むと「D」と「O」のつながりが立体的に見えてくる
- Bertrand Meyer『Object-Oriented Software Construction』(1988) — OCPの原典。概念を深く追いたい人向け
- Eric Freeman・Elisabeth Robson『Head First Design Patterns』(2020) — StrategyパターンなどOCPを実現するパターンを図解で学べる
一次資料
- Robert C. Martin, "The Open-Closed Principle" (1996):
https://www.cs.utexas.edu/~downing/papers/OCP-1996.pdf
無料で読める入門記事
- DigitalOcean「SOLID Design Principles Explained: Building Better Software Architecture」— 図が豊富で英語でも読みやすい
https://www.digitalocean.com/community/conceptual-articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design - Zenn・Qiitaで「OCP」「開放閉鎖原則」で検索すると日本語の実装例が充実している
Dev-Here「ギャルでも分かる設計の勘ドコロ!」シリーズ 第3回