メインコンテンツまでスキップ

インターフェースと抽象への依存

第5章では、NotificationChannelというインターフェースを使いました。 この章では、インターフェースが何のためにあるのかを掘り下げます。

インターフェースとは

インターフェースは、メソッドの名前と引数・戻り値の型だけを定義し、中身の実装を持たない仕組みです。 PHPではinterfaceキーワードで定義します。

interface Logger
{
public function log(string $message): void;
}

このインターフェースは、「logという名前のメソッドを持ち、文字列を受け取り、何も返さない」という契約を定義しています。 契約を満たすクラスは、implementsを使って実装します。

class FileLogger implements Logger
{
public function log(string $message): void
{
// ファイルへの書き込み(省略)
}
}

具象クラスに依存する問題

次のOrderServiceクラスを見てください。

❌ Bad: 具象クラスに直接依存する
class OrderService
{
private FileLogger $logger;

public function __construct()
{
$this->logger = new FileLogger();
}

public function placeOrder(Order $order): void
{
// 注文処理(省略)
$this->logger->log('注文を受け付けました');
}
}

OrderServiceは、FileLoggerという具象クラスに直接依存しています。 ログの出力先をファイルからCloudWatchやSlackに変更したくなったとき、OrderServiceのコンストラクタまで書き換える必要があります。 ログの出力先という、注文処理そのものとは無関係な都合が、OrderServiceの変更理由になってしまっています。

抽象への依存

FileLoggerという具象クラスの代わりに、Loggerインターフェースに依存する形に変えます。

✅ Good: インターフェースに依存する
class OrderService
{
public function __construct(
private readonly Logger $logger,
) {
}

public function placeOrder(Order $order): void
{
// 注文処理(省略)
$this->logger->log('注文を受け付けました');
}
}

OrderServiceLoggerという契約にだけ依存しており、実際に渡されるのがFileLoggerなのか、他の実装なのかを知りません。 FileLoggerの代わりにCloudWatchLoggerを渡せば、OrderServiceのコードを変更せずにログの出力先を切り替えられます。

「上位のクラスが、具象クラスでなく抽象(インターフェース)に依存する」というこの考え方を、依存性逆転の原則(Dependency Inversion Principle、DIP)と呼びます1。 「逆転」と呼ばれるのは、「上位のクラス(OrderService)が下位のクラス(FileLogger)に直接依存する」という発想を、「両方とも、抽象(Logger)に依存する」という形に置き換えるためです。

Loggerの実装を、OrderServiceの外からコンストラクタ引数として渡す仕組みは、依存性注入(Dependency Injection)と呼ばれます。 第11章で、Laravelのコンテナと合わせて詳しく扱います。

まとめ

  • インターフェースは、実装を持たない「契約」を定義する
  • 具象クラスに直接依存すると、実装を差し替えたいときに依存元のクラスまで書き換える必要がある
  • インターフェースに依存すると、実装の詳細を知らずに済み、差し替えが容易になる
  • この考え方を依存性逆転の原則と呼ぶ。実際に依存先を外から渡す仕組みは依存性注入と呼ばれ、第11章で扱う

次に読む

次章では、凝集度・結合度とSOLID原則の総整理を扱います。 ここまでの章のまとめとして、見てきた原則を物差しで振り返ります。

練習問題

次のPaymentProcessorクラスを、インターフェースに依存する形に書き換えてください
class StripeGateway
{
public function charge(int $amount): void
{
// Stripe API呼び出し(省略)
}
}

class PaymentProcessor
{
private StripeGateway $gateway;

public function __construct()
{
$this->gateway = new StripeGateway();
}

public function process(int $amount): void
{
$this->gateway->charge($amount);
}
}

解答例

interface PaymentGateway
{
public function charge(int $amount): void;
}

class StripeGateway implements PaymentGateway
{
public function charge(int $amount): void
{
// Stripe API呼び出し(省略)
}
}

class PaymentProcessor
{
public function __construct(
private readonly PaymentGateway $gateway,
) {
}

public function process(int $amount): void
{
$this->gateway->charge($amount);
}
}

PaymentProcessorPaymentGatewayという契約にのみ依存しています。 決済代行会社をStripeから別のサービスに変更したいときも、PaymentGatewayを実装した新しいクラスを渡すだけで済みます。


Footnotes

  1. 出典: Dependency inversion principle(Wikipedia)。Robert C. Martinが1996年に提唱した原則で、「上位モジュールは下位モジュールに依存すべきでない。両方とも抽象に依存すべきである」と定義されています。