コンピューターサイエンス入門:理論編
疎結合と密結合 — 依存性注入で設計を変える
テストを流したら、本物のメールが飛んだ
注文を保存してお礼メールを送る処理を書いたとします。テストを 1 回走らせるたびに、テスト用の宛先ではなく実在するアドレスへメールが飛びます。CI が 1 日 30 回まわれば 30 通です。データベースのほうも同じで、テストのたびに本番と同じ MySQL につなぎに行きます。
原因はテストの書き方ではありません。呼ばれている側にあります。
Python
class OrderService:
def __init__(self):
self.repo = MySQLRepo() # ここで相手を決め打ちしている
self.sender = SmtpSender() # ここも
def place(self, order):
self.repo.save(order)
self.sender.send(order.email, "ご注文ありがとうございます")使う側から見れば OrderService() と書くだけの部品ですが、その 1 行の裏で MySQL と SMTP への接続先が確定します。差し込む隙間がありません。この「相手の実物を自分で名指ししている」状態が密結合です。
実物を作る責任が、差し替えを塞いでいる
困るのはテストだけではありません。データベースを PostgreSQL に替えるとき、メール送信を外部サービスの API に替えるとき、そのたびに OrderService の中を開くことになります。注文処理そのものは 1 文字も変わっていないのにです。
塞いでいるのは MySQLRepo() という 1 行、つまり実物を作る責任を自分で持ってしまっていることです。この責任だけを外に出します。
Python
class OrderService:
def __init__(self, repo, sender): # 作らずに、受け取る
self.repo = repo
self.sender = sender
def place(self, order):
self.repo.save(order)
self.sender.send(order.email, "ご注文ありがとうございます")place の中身は 1 行も変わっていません。変えたのは、誰が実物を用意するかだけです。
Python
service = OrderService(MySQLRepo(), SmtpSender()) # 本番
service = OrderService(InMemoryRepo(), FakeSender()) # テストこれでテストは、FakeSender に「誰に何通送ろうとしたか」を記録させるだけで済みます。メールは 1 通も飛びません。この「実物を外から渡す」やり方を依存性注入(DI)と呼びます。専用のフレームワークが要ると思われがちですが、引数で渡すだけで成立します。
interface を切っただけでは、何も変わらない
ここでよくある取り違えがあります。repo の型を interface にすれば疎結合になった、という思い込みです。OrderService の中で MySQLRepo() と書いてあれば、その型が interface だろうと差し替えられないままです。効いているのは interface ではなく、作る場所を外に出したことのほうです。
そして、外に出すことにもコストがあります。OrderService のコードを読んでも repo の正体が分からず、組み立てている場所まで辿らないと実際に何が動くのか見えません。
| 決め打ち | 外から渡す | |
|---|---|---|
| テスト | 本物につながる | 差し替えられる |
| 読みやすさ | その場で分かる | 渡す場所を辿る |
| 書く量 | 少ない | 組み立てる場所が要る |
判断の目安は、その相手が外の世界に触れるかどうかです。データベース、メール、外部 API、ファイル、そして現在時刻。これらはテストのたびに本物を動かしたくないものなので、外から渡す価値があります。逆に、受け取った数値を計算して返すだけの関数まで包むと、読みにくくなるだけで何も得られません。