新機能追加のたびに、なんか動いてたコードが死にかけてない?🫠

新機能追加のたびに、なんか動いてたコードが死にかけてない?🫠

これ絶対あるじゃん??これ絶対あるじゃん??

  • 「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を書き足す。cashcredit_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を実現するパターンを図解で学べる

一次資料

無料で読める入門記事


Dev-Here「ギャルでも分かる設計の勘ドコロ!」シリーズ 第3回