■概要
プログラムを作っていると、「メール通知をSMSにも対応したい」「Slackにも送りたい」といった仕様変更がよく起こる。このとき、部品同士が強く結びついていると、1か所の変更のために関係ないクラスまで書き換える必要が出てくる。これを防ぐ設計の考え方が疎結合(Loose Coupling)である。
- 密結合(Tight Coupling):部品同士が強く結びついている状態
- 例:壁に直接はんだ付けされた電球。電球をLEDに替えたいだけなのに、壁ごと工事が必要になる
- 疎結合(Loose Coupling):部品同士のつながり(依存)が弱い状態
- 例:ソケット式の電球。ソケット(決められた形・約束事)に合えば、白熱電球でもLEDでも差し替えるだけで使える
プログラムでは、この「ソケット(約束事)」をインターフェースと呼ぶ。Pythonでは標準モジュール abc の抽象基底クラス(ABC)や typing.Protocol で表現できる。
疎結合を実現するポイントは次の2つ。
- インターフェースに依存する:具体的なクラス(
EmailNotifierなど)ではなく、約束事(Notifier)を型として扱う - 依存性の注入(Dependency Injection / DI):使う部品をクラスの中で生成せず、コンストラクタの引数などで「外から受け取る」
また、@abstractmethod を付けたメソッドを子クラスで実装し忘れると、インスタンス化の時点で TypeError になる。そのため、約束事を守っていない部品が紛れ込むのを早い段階で防げる。
■実装するサンプル処理の概要
「注文が完了したらお客様に通知を送る」という処理を、密結合と疎結合の2パターンで実装して比較する。
- 密結合の例
TightlyCoupledOrderServiceがクラスの中でEmailSender()を直接生成し、メールで通知する- 通知手段を変えたい場合は、
TightlyCoupledOrderService自体を書き換える必要がある
- 疎結合の例
- 通知手段の約束事として抽象基底クラス
Notifier(send(to, message)を必ず持つ)を定義する Notifierを継承したEmailNotifier/SmsNotifier/SlackNotifierを用意するOrderServiceは通知部品をコンストラクタで受け取り、send()を呼ぶだけにする- 同じ
OrderServiceに渡す部品を変えるだけで、通知手段が切り替わることを確認する
- 通知手段の約束事として抽象基底クラス
- 約束事を守らない部品はエラーになる
send()を実装していないBrokenNotifierをインスタンス化し、TypeErrorになることを確認する
■フローチャート
全体の流れ
- 密結合の例を実行する
- 疎結合の例を実行する
- 約束事(抽象メソッド)を守らない部品がエラーになることを確認する
1. 密結合の例
TightlyCoupledOrderServiceを生成する- コンストラクタの中で
EmailSender()を直接生成しているため、通知手段はメールに固定される(ここが密結合の原因) place_orderで注文受付メッセージを出力し、send_emailでメール通知を出力する- メソッド名
send_emailにまで依存しているため、SMSに変えるならこの呼び出し部分も修正が必要になる
- メソッド名
2. 疎結合の例
- 通知部品(
EmailNotifier/SmsNotifier/SlackNotifier)のリストを作成する - リストから部品を1つずつ取り出し、クラス名を出力する
- 取り出した部品を
OrderServiceのコンストラクタに渡して生成する(依存性の注入) place_orderを呼び出す。これをすべての部品について繰り返す- どの部品を使うかは「使う側(
main関数)」が決めており、OrderServiceは何も知らない
- どの部品を使うかは「使う側(
OrderService.place_order
- 注文受付メッセージを出力する
- 通知メッセージを作成する
- 約束事で決めた
send()だけを呼ぶ- 実際にどの形式で通知されるかは、注入された部品の
send()の実装次第
- 実際にどの形式で通知されるかは、注入された部品の
- テストしやすいように、送ったメッセージを戻り値で返す
3. 約束事を守らない部品はエラーになる
Notifierを継承しているがsend()を実装していないBrokenNotifierを定義するBrokenNotifier()をインスタンス化する- 抽象メソッド
sendが実装されていないためTypeErrorが発生し、exceptで捕捉してエラーメッセージを出力する
■サンプルコード
loose_coupling.py
"""
疎結合(Loose Coupling)のサンプルコード
【疎結合とは?】
プログラムの部品(クラスや関数)同士の「つながり(依存)」を弱くする設計の考え方です。
反対の言葉は「密結合(Tight Coupling)」で、部品同士が強く結びついている状態を指します。
【例】
- 密結合:壁に直接はんだ付けされた電球。電球を LED に替えたいだけなのに、壁ごと工事が必要。
- 疎結合:ソケット式の電球。ソケット(=決められた形・約束事)に合えば、
白熱電球でも LED でも差し替えるだけで使える。
プログラムでは、この「ソケット(約束事)」を「インターフェース」と呼びます。
Python では abc モジュールの抽象基底クラス(ABC)や、typing.Protocol で表現できます。
【このサンプルの題材】
「注文が完了したらお客様に通知を送る」という処理を、
1. 密結合で書いた場合
2. 疎結合で書いた場合
の 2 パターンで実装し、違いを比べます。
"""
# abc: 抽象基底クラス(Abstract Base Class)を作るための標準モジュール
# ABC : 抽象クラスを作るときに継承する親クラス
# abstractmethod : 「子クラスで必ず実装してね」という印(デコレータ)
from abc import ABC, abstractmethod
# =============================================================================
# 1. 密結合(悪い例)
# =============================================================================
class EmailSender:
"""メールを送信するクラス(本物の送信の代わりに print で表現)"""
def send_email(self, address: str, message: str) -> None:
print(f"[メール] {address} 宛て: {message}")
class TightlyCoupledOrderService:
"""
注文処理クラス(密結合版)
【問題点】
- クラスの中で EmailSender() を直接生成している(=EmailSender に強く依存)
- 「メールではなく SMS で通知したい」となったら、このクラスのコードを書き換える必要がある
- テストを実行するたびに(本物なら)実際にメールが送られてしまう
"""
def __init__(self) -> None:
# ★ここが密結合の原因:使う部品を自分で決め打ちで作っている
self.sender = EmailSender()
def place_order(self, customer: str, item: str) -> None:
print(f"注文受付: {customer} 様 / 商品: {item}")
# メソッド名 send_email にまで依存している
self.sender.send_email(customer, f"{item} のご注文ありがとうございます")
# =============================================================================
# 2. 疎結合(良い例)
# =============================================================================
# -----------------------------------------------------------------------------
# 2-1. インターフェース(約束事)を定義する
# -----------------------------------------------------------------------------
class Notifier(ABC):
"""
通知手段の「約束事」を表す抽象クラス
「通知する部品なら、send(宛先, メッセージ) というメソッドを必ず持っていること」
というルールだけを決めています。
“どうやって送るか” は一切書きません。それは子クラスの仕事です。
"""
@abstractmethod
def send(self, to: str, message: str) -> None:
# 中身は書かない(pass でも ... でもよい)
# @abstractmethod を付けたメソッドを実装していない子クラスは、
# インスタンスを作ろうとした時点でエラー(TypeError)になります。
...
# -----------------------------------------------------------------------------
# 2-2. 約束事に従った「具体的な部品」を作る
# -----------------------------------------------------------------------------
class EmailNotifier(Notifier):
"""メールで通知する部品"""
def send(self, to: str, message: str) -> None:
print(f"[メール] {to} 宛て: {message}")
class SmsNotifier(Notifier):
"""SMS で通知する部品"""
def send(self, to: str, message: str) -> None:
print(f"[SMS] {to} 宛て: {message}")
class SlackNotifier(Notifier):
"""Slack で通知する部品(後から追加しても OrderService の修正は不要!)"""
def __init__(self, channel: str) -> None:
self.channel = channel
def send(self, to: str, message: str) -> None:
print(f"[Slack #{self.channel}] @{to}: {message}")
# -----------------------------------------------------------------------------
# 2-3. 部品を「外から受け取る」クラスを作る
# -----------------------------------------------------------------------------
class OrderService:
"""
注文処理クラス(疎結合版)
【ポイント】
- 通知の部品を自分で作らず、コンストラクタの引数で「外から受け取る」
→ この手法を「依存性の注入(Dependency Injection / DI)」と呼びます
- 型ヒントは具体的なクラス(EmailNotifier など)ではなく、
約束事(Notifier)にしている
→ 「Notifier の約束を守っていれば何でも OK」という意味になる
- OrderService はメールなのか SMS なのかを知らない(知る必要がない)
"""
def __init__(self, notifier: Notifier) -> None:
# ★外から渡された部品をそのまま保持するだけ
self.notifier = notifier
def place_order(self, customer: str, item: str) -> str:
print(f"注文受付: {customer} 様 / 商品: {item}")
message = f"{item} のご注文ありがとうございます"
# 約束事で決めた send() だけを呼ぶ。中身がどう動くかは部品次第
self.notifier.send(customer, message)
# テストしやすいように、送ったメッセージを戻り値で返しておく
return message
# =============================================================================
# 3. 動作確認
# =============================================================================
def main() -> None:
print("=" * 60)
print("1. 密結合の例")
print("=" * 60)
# 通知手段はメールに固定。変えたければ TightlyCoupledOrderService を書き換えるしかない
tight_service = TightlyCoupledOrderService()
tight_service.place_order("tanaka@example.com", "ノートPC")
print()
print("=" * 60)
print("2. 疎結合の例")
print("=" * 60)
# 同じ OrderService クラスのまま、渡す部品を変えるだけで通知手段を切り替えられる
# どの部品を使うかを「使う側(ここでは main 関数)」が決めているのがポイント
notifiers: list[Notifier] = [
EmailNotifier(),
SmsNotifier(),
SlackNotifier(channel="orders"),
]
for notifier in notifiers:
# クラス名を表示して、どの部品を差し込んだか分かるようにする
print(f"--- 差し込んだ部品: {type(notifier).__name__} ---")
service = OrderService(notifier)
service.place_order("tanaka", "ノートPC")
print()
print("=" * 60)
print("3. 約束事を守らない部品はエラーになる")
print("=" * 60)
# send() を実装し忘れた(約束を守っていない)クラス
class BrokenNotifier(Notifier):
pass
try:
BrokenNotifier()
except TypeError as e:
# 「抽象メソッド send を実装していないのでインスタンス化できない」というエラー
print(f"TypeError: {e}")
# このファイルを直接実行したときだけ main() を呼ぶ
# (他のファイルから import されたときは実行されない)
if __name__ == "__main__":
main()
■実行結果
============================================================
1. 密結合の例
============================================================
注文受付: tanaka@example.com 様 / 商品: ノートPC
[メール] tanaka@example.com 宛て: ノートPC のご注文ありがとうございます
============================================================
2. 疎結合の例
============================================================
--- 差し込んだ部品: EmailNotifier ---
注文受付: tanaka 様 / 商品: ノートPC
[メール] tanaka 宛て: ノートPC のご注文ありがとうございます
--- 差し込んだ部品: SmsNotifier ---
注文受付: tanaka 様 / 商品: ノートPC
[SMS] tanaka 宛て: ノートPC のご注文ありがとうございます
--- 差し込んだ部品: SlackNotifier ---
注文受付: tanaka 様 / 商品: ノートPC
[Slack #orders] @tanaka: ノートPC のご注文ありがとうございます
============================================================
3. 約束事を守らない部品はエラーになる
============================================================
TypeError: Can't instantiate abstract class BrokenNotifier without an implementation for abstract method 'send'
密結合の例では、TightlyCoupledOrderService の中で通知手段がメールに固定されているため、メール通知しか送れない。
疎結合の例では、OrderService のコードは1行も変えずに、渡す部品を変えるだけでメール・SMS・Slackと通知手段を切り替えられた。後から追加した SlackNotifier のように、チャンネル名などの独自の設定を持つ部品でも、send(to, message) の約束さえ守っていれば同じように差し込める。また、テスト時には本物の送信を行わないダミーの部品を渡せるため、テストもしやすくなる。
最後に、send() を実装していない BrokenNotifier は、インスタンス化した時点でCan't instantiate abstract class BrokenNotifier without an implementation for abstract method 'send' という TypeError になった。抽象基底クラスを使うことで、約束事を守らない部品を実行前の早い段階で検出できる。
コメント