そのコード、ベンダーが変わっただけで全員残業になるやつじゃん💔

そのコード、ベンダーが変わっただけで全員残業になるやつじゃん💔

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

  • 「決済サービスを乗り換えようとしたら、注文処理のクラスまるごと書き直しになった」
  • 「ログの出力先をファイルからSlackに変えたら、ビジネスロジックの核心部分が巻き添えを食った」
  • 「テスト書こうとしたら、外部APIに繋がないと何もテストできない設計になってた」
  • 「新しいメンバーが『このクラス、何に依存してるんですか?』って聞いてきて、自分も答えられなかった」

これがTight Coupling(密結合)、以降「ベタ付き」って呼ぶね、が原因なんだよね。


てかシンプルに言うとねてかシンプルに言うとね

ベタ付きとは、**「あるモジュールが別の具体的なモジュールに直接依存しちゃってる状態」**のこと。

「それの何がまずいの?ってなる気持ち、わかる!」動いてるし、テストもなんとか通ってるし。

でもね、「変えようとした瞬間に全部が崩れる」コードって、借金みたいにどんどん溜まっていくの。相手が変わったら?こっちも変えなきゃいけなくなる。それがベタ付きなんだよね。


ちょっと待って、先に全然関係ない話していい?🏠ちょっと待って、先に全然関係ない話していい?🏠

山田建設っていう中堅の建設会社があったんだけどさ。資材の調達は全部、創業当初からつきあいがある「タカハシ建材」一択。発注書のフォーマットも、請求書の処理も、在庫確認のシステムも、全部タカハシ建材専用に作られてたの。

ある日、調達担当の佐々木さんに本社から連絡が来たんだよね。

「コスト削減で、資材の一部をミナミ商事に切り替えます。来月から対応よろしく。」

佐々木さんの頭が真っ白になった。「発注書のフォーマット、全然違うじゃん。請求処理のシステム、タカハシ建材のコード体系で組んであるし。在庫確認のAPIもタカハシ建材専用だし……」

翌週、佐々木さんは総務・経理・システム担当の3部署を巻き込んで緊急対応を始めたんだよね。発注書テンプレートの作り直し、経理システムの直し、在庫確認のやり方を手作業に切り替え。あっちもこっちも全部。

「佐々木さん、ミナミ商事への最初の発注いつになりますか?」と部長が聞く。

「……3週間後には」

「は?来月頭じゃなかったの?」

「だって全部タカハシ建材に合わせて作ってたんだもん……」

本社が決めた切り替えスケジュールは守れなかった。工期が2週間ズレて、取引先から「訴えるぞ」って脅されたんだって。佐々木さんは翌月、他社に転職した。えぐくない?

これが本番で起きてるやつが、ベタ付きってこと。


じゃあコードで見てみよっか👀じゃあコードで見てみよっか👀

# 地獄のベタ付きコード

import requests

class TakahashiMaterialClient:
    def get_stock(self, item_code: str) -> int:
        # タカハシ建材専用のAPIを直接叩く
        response = requests.get(f"https://api.takahashi-zai.co.jp/stock/{item_code}")
        return response.json()["takahashi_stock_count"]  # タカハシ独自のキー名

class OrderProcessor:
    def __init__(self):
        # 具体的なクラスを直接インスタンス化している
        self.material_client = TakahashiMaterialClient()

    def process_order(self, item_code: str, quantity: int):
        stock = self.material_client.get_stock(item_code)
        if stock < quantity:
            raise Exception("在庫不足")
        print(f"{item_code}を{quantity}個発注しました(タカハシ建材経由)")

「ミナミ商事に切り替えて」って言われたとき何が起きるか見てみて。

# ミナミ商事クライアントを新しく作る
class MinamiTradingClient:
    def get_available_count(self, product_id: str) -> int:  # メソッド名が違う
        response = requests.get(f"https://minami-trading.jp/api/inventory/{product_id}")
        return response.json()["available"]  # レスポンスのキー名も違う

# OrderProcessorを変えないといけない
class OrderProcessor:
    def __init__(self, use_minami: bool = False):
        if use_minami:
            self.material_client = MinamiTradingClient()
        else:
            self.material_client = TakahashiMaterialClient()

    def process_order(self, item_code: str, quantity: int):
        # isinstance()で分岐するハメになる
        if isinstance(self.material_client, MinamiTradingClient):
            stock = self.material_client.get_available_count(item_code)
        else:
            stock = self.material_client.get_stock(item_code)
        if stock < quantity:
            raise Exception("在庫不足")
        print(f"{item_code}を{quantity}個発注しました")

わたしも昔これやったから言えるんだけど、isinstance()が出てきた瞬間、もうベタ付きの手遅れ状態なんだよね。第3のベンダーが来るたびにifが増える。発注処理の一番大事なロジックが「どのベンダーか」っていう別の話でどんどんぐちゃぐちゃになってく。

本番でこれが問題になるとき?ベンダー切り替えの当日、深夜2時に「注文が通らない」ってSlackが来て、isinstanceの分岐を全部追いかける羽目になるやつ。


やばいのまだあんだけど、バグに気づけない問題😭やばいのまだあんだけど、バグに気づけない問題😭

「壊れやすい」だけじゃないんだよね。「壊れてても気づけない」のがヤバいとこ。

さっきの山田建設で考えてみて? 佐々木さんが「タカハシ建材で動作確認済み!」と言っても、ミナミ商事での動作は全く別の話じゃん。確認のパターンが爆発するんだよね。

  • タカハシ建材 × 在庫あり × 通常発注
  • タカハシ建材 × 在庫なし × 緊急発注
  • ミナミ商事 × 在庫あり × 通常発注
  • ミナミ商事 × 在庫なし × 緊急発注
  • (将来の第3ベンダー × …)

ベンダーが増えるたびに確認パターンが倍になるじゃん。コードでも同じことが起きるんだよね。

# ベタ付きのテストは組み合わせが爆発する

def test_order_with_takahashi_in_stock():
    processor = OrderProcessor(use_minami=False)
    # タカハシ建材のAPIをモックする(ミナミ商事とは別にセットアップが必要)
    ...

def test_order_with_takahashi_out_of_stock():
    processor = OrderProcessor(use_minami=False)
    ...

def test_order_with_minami_in_stock():
    processor = OrderProcessor(use_minami=True)
    # ミナミ商事のAPIをモックする(タカハシ建材とは別にセットアップが必要)
    ...

def test_order_with_minami_out_of_stock():
    processor = OrderProcessor(use_minami=True)
    ...

# 第3のベンダーが来たらまたこのセットが増える
# しかも「在庫不足なら発注しない」というOrderProcessorのコアロジックのテストが
# ベンダーの実装の違いに引きずられて何度も書き直しになる

OrderProcessorの「在庫不足なら発注しない」っていう一番大事な部分は何も変えてないのに、テストがベンダーの数だけ増えて壊れるんだよね。しかもバグったとき、ロジックが悪いのかベンダー対応が悪いのかも分かんなくなる。

ベタ付きは壊れやすいだけじゃなく、バグを隠す設計でもあるってこと。


分けてた会社の話もするね〜✨分けてた会社の話もするね〜✨

同じ建設業界に「ナカムラ工務店」ってとこがあったんだけどさ。ここも資材調達はタカハシ建材メインだったけど、調達フローの設計が全然違ったの。

社内の発注システムは「資材業者インターフェース」っていう仕様書通りに動いてたんだよね。「在庫数を返すこと」「発注を受け付けること」、それだけ。具体的にタカハシ建材がどう実装してるかは、システムは知らない。

本社から「ミナミ商事に切り替えます」と連絡が来た日、担当の中野さんはこう言ったの。

「わかりました。ミナミ商事側に、うちの仕様書通りのアダプター作ってもらえますか?」

3日後、ミナミ商事からアダプターが届いた。既存の発注システムはそのまま。中野さんは定時で帰ったんだって。

コードで見るとこういうことなんだよね。ちょっと見てみて?

from abc import ABC, abstractmethod
import requests

# 「こういう機能を持ってるなら誰でもOK」という仕様(抽象)
class MaterialSupplier(ABC):
    @abstractmethod
    def get_stock(self, item_code: str) -> int:
        pass

# タカハシ建材は仕様に合わせてくる
class TakahashiMaterialClient(MaterialSupplier):
    def get_stock(self, item_code: str) -> int:
        response = requests.get(f"https://api.takahashi-zai.co.jp/stock/{item_code}")
        return response.json()["takahashi_stock_count"]

# ミナミ商事も仕様に合わせてくる(内部の差異はアダプターで吸収)
class MinamiTradingAdapter(MaterialSupplier):
    def get_stock(self, item_code: str) -> int:
        response = requests.get(f"https://minami-trading.jp/api/inventory/{item_code}")
        return response.json()["available"]  # 内部の違いはここで吸収

# OrderProcessorは「仕様(抽象)」にしか依存しない
class OrderProcessor:
    def __init__(self, supplier: MaterialSupplier):  # 具体クラスを知らない
        self.supplier = supplier

    def process_order(self, item_code: str, quantity: int):
        stock = self.supplier.get_stock(item_code)  # 誰が来ても同じ呼び方
        if stock < quantity:
            raise Exception("在庫不足")
        print(f"{item_code}を{quantity}個発注しました")

# 切り替えはここだけ。OrderProcessorは1行も変えない
processor = OrderProcessor(supplier=MinamiTradingAdapter())

OrderProcessorは何も変えてないじゃん。テストも、ロジックも、そのまま。isinstanceも消えた。ヤバくない?

# テストもシンプルになる

class MockSupplier(MaterialSupplier):
    def __init__(self, stock: int):
        self._stock = stock

    def get_stock(self, item_code: str) -> int:
        return self._stock

def test_order_rejected_when_out_of_stock():
    # ベンダーが誰であっても関係ない。ロジックだけテストできる
    processor = OrderProcessor(supplier=MockSupplier(stock=0))
    # 在庫0のときに例外が出ることだけ確認すればいい
    ...

def test_order_success_when_in_stock():
    processor = OrderProcessor(supplier=MockSupplier(stock=100))
    ...

ベンダーが何社増えても、このテストは変わらないんだよね。


ちゃんとした名前もあるから一応言うね📚ちゃんとした名前もあるから一応言うね📚

依存性の逆転(Dependency Inversion Principle。以降DIP)とは、**「呼ぶ側も呼ばれる側も、"こういう機能を持ってる誰か"っていうルールに頼れ。具体的な相手の名前にべったりくっつくな」**っつーこと。

アンクルボブ(Robert C. Martin)が1996年に言い出して、2002年の本でまとめたやつ。SOLIDの「D」がこれね。

難しそうに聞こえるけど、要は「具体的な相手の名前を直接書かず、"こういう機能を持ってる誰かに頼む"という形で書くだけ」を問い続けるだけなんだよね。


で、実務だとどういう名前のファイルに書くの?🌸

DIPって考え方はわかった。でも実際にコード書くとき「このクラス、何て名前のファイルに置けばいいの?」ってなるじゃん?

現場でよく使われる名前を、今回の建設会社の話に全部対応させて説明するね。


Gateway ― 「外の世界への出口」

一言で言うと:「社外のサービスと話す専用の窓口」

ナカムラ工務店でいうと、タカハシ建材やミナミ商事と実際に電話するオペレーターさん。「在庫いくつありますか?」って外に聞く役割だけ持ってる。

# suppliers/takahashi_gateway.py

class TakahashiMaterialGateway(MaterialSupplier):
    """タカハシ建材のAPIと実際に話す人。外部APIの詳細はここに全部閉じ込める。"""
    def get_stock(self, item_code: str) -> int:
        response = requests.get(f"https://api.takahashi-zai.co.jp/stock/{item_code}")
        return response.json()["takahashi_stock_count"]

使う場面: Stripe・Twilio・SendGrid・外部の在庫APIなど、「社外のサービスを叩く」ときは全部Gateway。


Repository ― 「自分の倉庫の管理人」

一言で言うと:「自社のDB・ストレージに読み書きする専用の人」

Gatewayが「社外と話す人」なのに対して、Repositoryは「自社の倉庫(DB)を管理する人」。どのDBを使ってるか(PostgreSQLかMySQLか)は、呼び出し側は知らなくていい。

# repositories/order_repository.py

from abc import ABC, abstractmethod

class OrderRepository(ABC):
    """発注記録をDBに保存・取得する人の仕様書。"""
    @abstractmethod
    def save(self, order: Order) -> None:
        pass

    @abstractmethod
    def find_by_id(self, order_id: str) -> Order:
        pass

class PostgresOrderRepository(OrderRepository):
    """PostgreSQLに実際に書き込む人。DBの詳細はここだけが知ってる。"""
    def save(self, order: Order) -> None:
        # PostgreSQL専用の処理
        ...

    def find_by_id(self, order_id: str) -> Order:
        ...

使う場面: 「DBに保存する」「DBから検索する」処理は全部Repository。


Adapter ― 「言葉を翻訳する人」

一言で言うと:「相手のクセをこちらの仕様書に合わせて変換する人」

ミナミ商事のAPIは"available"っていうキーで在庫数を返してくる。でも社内の仕様書はget_stock()で在庫数が取れることを求めてる。その差を「翻訳」するのがAdapter。

# suppliers/minami_adapter.py

class MinamiTradingAdapter(MaterialSupplier):
    """ミナミ商事のAPIを、社内仕様書に合わせて翻訳する人。"""
    def get_stock(self, item_code: str) -> int:
        response = requests.get(f"https://minami-trading.jp/api/inventory/{item_code}")
        # ミナミ商事は "available" というキー名を使う。それをここで吸収する。
        return response.json()["available"]

GatewayとAdapterって何が違うの?ってなるじゃん。

GatewayAdapter
役割外部と通信する「出口」外部の仕様の差を吸収する「翻訳」
主な関心事「どうやって通信するか」「どう変換するか」
現場での使われ方単独で使われることが多いGateway内部に組み込まれることも多い

小さいプロジェクトだとMinamiTradingGatewayの中で翻訳まで全部やっちゃうこともある。大きくなるにつれて分離していくイメージ。


Strategy ― 「やり方を差し替える人」

一言で言うと:「同じ目的に対して、やり方だけ変える人」

ナカムラ工務店が「仕入れ先によって送料の計算ルールが違う」ケースを想像して。タカハシ建材は距離×重量、ミナミ商事は重量だけで計算する。目的は同じ「送料を計算する」なのにやり方が違う。これがStrategy。

# strategies/shipping_cost_strategy.py

class ShippingCostStrategy(ABC):
    """「送料を計算する」という目的の仕様書。やり方は実装クラスが決める。"""
    @abstractmethod
    def calculate(self, weight_kg: float, distance_km: float) -> int:
        pass

class TakahashiShippingStrategy(ShippingCostStrategy):
    """タカハシ建材のやり方:距離×重量で計算する。"""
    def calculate(self, weight_kg: float, distance_km: float) -> int:
        return int(weight_kg * distance_km * 10)

class MinamiShippingStrategy(ShippingCostStrategy):
    """ミナミ商事のやり方:重量だけで計算する。"""
    def calculate(self, weight_kg: float, distance_km: float) -> int:
        return int(weight_kg * 500)

# 使う側は「やり方」を注入するだけ
class OrderProcessor:
    def __init__(self, supplier: MaterialSupplier, shipping: ShippingCostStrategy):
        self.supplier = supplier
        self.shipping = shipping  # 送料の計算方法は外から渡してもらう

    def process_order(self, item_code: str, quantity: int, distance_km: float):
        stock = self.supplier.get_stock(item_code)
        if stock < quantity:
            raise Exception("在庫不足")
        cost = self.shipping.calculate(weight_kg=quantity * 5.0, distance_km=distance_km)
        print(f"{item_code}を{quantity}個発注。送料:{cost}円")

GatewayとStrategyの違いをひとことで言うと?

  • Gatewayは「誰と話すか(外部との接続)」
  • Strategyは「どうやるか(ロジックの切り替え)」

Service ― 「仕事の段取りをする人」

一言で言うと:「複数の専門家(Gateway・Repository)を呼び出して、仕事の流れを束ねる人」

佐々木さんが「在庫確認して → 発注して → 記録する」の3ステップを段取りしてたでしょ。その段取り役がService。

# services/order_service.py

class OrderService:
    """
    発注の一連の流れを束ねる人。
    具体的にどのベンダーか・どのDBかは知らない。仕様書(抽象)だけ知ってる。
    """
    def __init__(
        self,
        supplier: MaterialSupplier,       # Gatewayの仕様書
        order_repo: OrderRepository,       # Repositoryの仕様書
        shipping: ShippingCostStrategy,    # Strategyの仕様書
    ):
        self.supplier = supplier
        self.order_repo = order_repo
        self.shipping = shipping

    def place_order(self, item_code: str, quantity: int, distance_km: float):
        # ステップ①:在庫確認(Gatewayに仕事を振る)
        stock = self.supplier.get_stock(item_code)
        if stock < quantity:
            raise Exception("在庫不足")

        # ステップ②:送料計算(Strategyに仕事を振る)
        cost = self.shipping.calculate(weight_kg=quantity * 5.0, distance_km=distance_km)

        # ステップ③:発注記録(Repositoryに仕事を振る)
        order = Order(item_code=item_code, quantity=quantity, shipping_cost=cost)
        self.order_repo.save(order)

        print(f"発注完了:{item_code} × {quantity}個 送料{cost}円")

Serviceは「段取り」だけするの。実際の作業(DB書き込み・API通信)は全部専門家に振る。だからServiceのテストはモックを渡すだけで全部動くんだよね。


まとめ:誰がどこに住んでるか

実務のフォルダ構成はこうなる。

project/
├── services/
│   └── order_service.py        ← 段取り役。ビジネスの流れを書く場所
├── suppliers/                   ← 「仕入れ先との話」専用フォルダ
│   ├── material_supplier.py     ← 仕様書(抽象クラス)
│   ├── takahashi_gateway.py     ← タカハシ建材との通信
│   └── minami_adapter.py        ← ミナミ商事との翻訳+通信
├── repositories/
│   ├── order_repository.py      ← 仕様書(抽象クラス)
│   └── postgres_order_repository.py  ← PostgreSQLへの実際の書き込み
├── strategies/
│   ├── shipping_cost_strategy.py     ← 仕様書(抽象クラス)
│   ├── takahashi_shipping_strategy.py
│   └── minami_shipping_strategy.py
└── main.py                      ← 「誰を使うか」を決める唯一の場所

そして「誰を使うか」を決めるmain.pyがこれ。

# main.py ― ここだけが「具体的な名前」を知っている

from suppliers.takahashi_gateway import TakahashiMaterialGateway
from repositories.postgres_order_repository import PostgresOrderRepository
from strategies.takahashi_shipping_strategy import TakahashiShippingStrategy
from services.order_service import OrderService

# 「今日はタカハシ建材を使う」という決定はここだけがする
service = OrderService(
    supplier=TakahashiMaterialGateway(),
    order_repo=PostgresOrderRepository(),
    shipping=TakahashiShippingStrategy(),
)

service.place_order(item_code="鉄骨A", quantity=10, distance_km=50)

「ミナミ商事に切り替えて」って言われたとき、変えるのはmain.pyの3行だけ。OrderServiceは1行も変えない。これがDIPを実務で使った姿なんだよね。


これだけ覚えて帰って👌これだけ覚えて帰って👌

合言葉はこれ。btk(べったりキモ)

コードを書くとき・レビューするとき、この問いを使って。

「このクラスが変わるとしたら、どんな出来事が起きたとき?」

さっきのOrderProcessorで試してみて。

  • 出来事①:「ベンダーをタカハシからミナミに変えた」
    → ベタ付きならOrderProcessorが変わる。でもこれ、「ベンダー側の話」だよね?
  • 出来事②:「在庫不足のときに例外じゃなくてログを出すようにした」
    → これはOrderProcessorのロジックが変わっていい。出来事の主語がOrderProcessorだから。
  • 出来事③:「発注APIのエンドポイントURLが変わった」
    → ベタ付きならOrderProcessorが変わる。でもこれも「ベンダー側の話」。

①と③で「ベンダーが変わっても注文処理のロジックは変わらないはず」という直感がある。その直感をコードで表現するのがDIPなんだよね。

OrderProcessorを書き直すきっかけ」が3種類あるなら、やることが3つ混ざってるってこと。やることが混ざってるなら、べったりくっついてるってこと。つまりベタ付いてる。


まとめるとこういうことねまとめるとこういうことね

観点密結合(ベタ付き)依存性の逆転(DIP)
依存の向き上位モジュールが具体クラスに直接依存上位も下位も抽象(インターフェース)に依存
変更の影響範囲依存先が変わると依存元も変わるインターフェースが守られれば依存元は無変更
テストのしやすさ外部サービス・DBが必要モックに差し替えるだけでテスト可能
拡張のしやすさisinstance分岐が増え続ける新しい実装クラスを追加するだけ
バグの発見しやすさベンダーごとにテストの組み合わせが爆発ロジックと実装を独立してテストできる

コードは「誰にべったりくっついてるか」じゃなくて「どんな約束に頼ってるか」で考えるっつーこと。


沼りたい人はこっちも読んで📖沼りたい人はこっちも読んで📖

権威ある書籍

  • Robert C. Martin『Agile Software Development: Principles, Patterns, and Practices』(2002)― SOLIDの提唱者による原典。DIPの章は必読。
  • Robert C. Martin『Clean Architecture』(2017)― DIPを「プラグインアーキテクチャ」として大規模設計に展開した一冊。邦訳あり。
  • Eric Freeman, Elisabeth Robson, Bert Bates, Kathy Sierra『Head First Design Patterns』(2004)― StrategyパターンをはじめOOのデザインパターンを図解で学べる入門書。DIPの実践にも通じる。

一次資料

無料で読める入門記事


Dev-Here「ギャルでも分かる設計の勘ドコロ!」第2回