
そのコード、ベンダーが変わっただけで全員残業になるやつじゃん💔
これ絶対あるじゃん??
- 「決済サービスを乗り換えようとしたら、注文処理のクラスまるごと書き直しになった」
- 「ログの出力先をファイルから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って何が違うの?ってなるじゃん。
| Gateway | Adapter | |
|---|---|---|
| 役割 | 外部と通信する「出口」 | 外部の仕様の差を吸収する「翻訳」 |
| 主な関心事 | 「どうやって通信するか」 | 「どう変換するか」 |
| 現場での使われ方 | 単独で使われることが多い | 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の実践にも通じる。
一次資料
- Robert C. Martin "The Dependency Inversion Principle" (1996, C++ Report): https://www.cs.utexas.edu/~downing/papers/DIP-1996.pdf
- Wikipedia "SOLID": https://en.wikipedia.org/wiki/SOLID
無料で読める入門記事
- DigitalOcean "SOLID: The First 5 Principles of Object Oriented Design": https://www.digitalocean.com/community/conceptual-articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design
- Zenn・Qiitaで「依存性逆転の原則 Python」で検索すると日本語の実装例が豊富にある
Dev-Here「ギャルでも分かる設計の勘ドコロ!」第2回