凝集度・結合度とSOLID原則の総整理
ここまでの章で、単一責任の原則(第4章)と依存性逆転の原則(第6章)を見てきました。 この2つは、SOLIDと呼ばれる5つの原則のうちの2つです。 この章では、残る3つ(開放閉鎖・リスコフ置換・インターフェース分離)を補い、最後に5つ全体を共通の物差しで振り返ります。
開放閉鎖の原則(OCP)
第5章と同じく、決済方法を題材にします。 決済方法をif文で分岐すると、次のようになります。
class PaymentProcessor
{
public function process(string $method, int $amount): void
{
if ($method === 'credit_card') {
// クレジットカード決済処理(省略)
} elseif ($method === 'bank_transfer') {
// 銀行振込処理(省略)
}
// 新しい決済方法を追加するたびに、この分岐を修正する必要がある
}
}
新しい決済方法を追加するたびに、PaymentProcessor自体を書き換える必要があります。
第6章で見たインターフェースの考え方を使うと、次のように書き換えられます。
interface PaymentMethod
{
public function process(int $amount): void;
}
class CreditCardPayment implements PaymentMethod
{
public function process(int $amount): void { /* クレジットカード決済処理(省略) */ }
}
class PaymentProcessor
{
public function process(PaymentMethod $method, int $amount): void
{
$method->process($amount);
}
}
// 新しい決済方法を追加しても、PaymentProcessorは変更しない
class BankTransferPayment implements PaymentMethod
{
public function process(int $amount): void { /* 銀行振込処理(省略) */ }
}
新しい決済方法(BankTransferPayment)を追加しても、PaymentProcessorのコードは1行も変わりません。
機能の拡張には開かれているが、既存コードの修正には閉じている、というこの性質を開放閉鎖の原則(Open-Closed Principle、OCP)と呼びます1。
リスコフの置換原則(LSP)
次のクラスを見てください。 第5章のNotificationChannelを実装したクラスです。
interface NotificationChannel
{
public function send(string $message): void;
}
class EmailChannel implements NotificationChannel
{
public function send(string $message): void { /* メール送信処理(省略) */ }
}
class ReadOnlyChannel implements NotificationChannel
{
public function send(string $message): void
{
throw new RuntimeException('この通知チャネルは送信できません');
}
}
ReadOnlyChannelはNotificationChannelを実装していますが、sendを呼ぶと必ず例外を投げます。
NotificationChannel型を受け取るコードは、「送信できる」ことを前提にできるはずですが、ReadOnlyChannelを渡すとその前提が崩れます。
基底の型(この場合はインターフェース)を使う側が期待する振る舞いを、派生型が裏切ってはいけないという原則を、リスコフの置換原則(Liskov Substitution Principle、LSP)と呼びます2。
NotificationChannelを実装するクラスは、実際に送信できるものに限るべきです。
送信できないチャネルを表現したいなら、そもそも別のインターフェースに分けるべきです。
インターフェース分離の原則(ISP)
「別のインターフェースに分ける」を、実際にやってみます。
interface NotificationChannel
{
public function send(string $message): void;
public function receive(): array;
public function markAsRead(int $id): void;
}
class EmailChannel implements NotificationChannel
{
public function send(string $message): void { /* ... */ }
public function receive(): array { /* ... */ }
public function markAsRead(int $id): void { /* ... */ }
}
class SmsChannel implements NotificationChannel
{
public function send(string $message): void { /* ... */ }
public function receive(): array
{
throw new RuntimeException('SMSチャネルは受信に対応していません');
}
public function markAsRead(int $id): void
{
throw new RuntimeException('SMSチャネルは既読管理に対応していません');
}
}
SmsChannelは、自分が使わないreceiveとmarkAsReadまで実装することを強制されています。
これが、先ほどのLSP違反(使えないメソッドで例外を投げる実装)を引き起こす原因にもなっています。
interface Sendable
{
public function send(string $message): void;
}
interface Receivable
{
public function receive(): array;
public function markAsRead(int $id): void;
}
class EmailChannel implements Sendable, Receivable
{
public function send(string $message): void { /* ... */ }
public function receive(): array { /* ... */ }
public function markAsRead(int $id): void { /* ... */ }
}
class SmsChannel implements Sendable
{
public function send(string $message): void { /* ... */ }
// receiveとmarkAsReadは実装しなくてよい
}
SmsChannelは、自分が使うSendableだけを実装すればよくなりました。
クライアントが使わないメソッドへの依存を強制されるべきではない、という考え方を、インターフェース分離の原則(Interface Segregation Principle、ISP)と呼びます3。
ISPを守ることで、直前のLSP違反を構造的に避けられます。
凝集度と結合度という物差し
ここまで、単一責任・開放閉鎖・リスコフ置換・インターフェース分離・依存性逆転という、SOLIDの5原則を見てきました。 これらは、凝集度(cohesion)と結合度(coupling)という、2つの物差しで統一的に説明できます4。
- 凝集度: 1つのクラスの中に集まっている要素が、どれだけ強く関連しているか。凝集度が高いほど、クラスの責務が絞られている
- 結合度: クラス同士が、どれだけ強く依存し合っているか。結合度が低いほど、一方の変更が他方に影響しにくい
第4章の単一責任の原則は、凝集度を高く保つための原則でした。 第6章の依存性逆転の原則、そしてこの章のOCPとISPは、結合度を下げるための原則です。 LSPは、結合度を下げる工夫が成り立つための前提条件にあたります。 抽象への依存によって結合度を下げても、その抽象を実装するクラスがどれも期待どおりに振る舞わなければ、依存先を安心して差し替えられないからです。
まとめ
- OCPは、拡張に開き、修正に閉じているべきという原則。分岐を実装の追加で解決する
- LSPは、派生型が基底型の期待する振る舞いを裏切ってはいけないという原則
- ISPは、クライアントが使わないメソッドへの依存を強制されるべきではないという原則
- SOLIDの5原則は、凝集度を高く保ち、結合度を低く保つための具体的な手段としてまとめられる
次に読む
次章から第2部です。 Laravelのモデル・サービスクラス設計に、ここまでの原則を応用していきます。
練習問題
次のFileStorageクラスが、LSPとISPのどちらに違反しているか、理由とともに答えてください
interface FileStorage
{
public function upload(string $path, string $content): void;
public function download(string $path): string;
public function delete(string $path): void;
}
class S3Storage implements FileStorage
{
public function upload(string $path, string $content): void { /* ... */ }
public function download(string $path): string { /* ... */ }
public function delete(string $path): void { /* ... */ }
}
class ReadOnlyArchiveStorage implements FileStorage
{
public function upload(string $path, string $content): void
{
throw new RuntimeException('アーカイブストレージは書き込みできません');
}
public function download(string $path): string { /* ... */ }
public function delete(string $path): void
{
throw new RuntimeException('アーカイブストレージは削除できません');
}
}
解答例
両方に違反しています。
- LSP違反:
FileStorage型を受け取るコードは、uploadやdeleteが正常に動作することを前提にできますが、ReadOnlyArchiveStorageを渡すと例外が投げられ、その前提が裏切られます。 - ISP違反:
ReadOnlyArchiveStorageは、自分が使わない(使えない)uploadとdeleteまで実装することを強制されています。
FileStorageをReadable(downloadのみ)とWritable(upload・deleteのみ)に分割すれば、ReadOnlyArchiveStorageはReadableだけを実装すればよくなり、両方の違反を同時に解消できます。