📘

Rubyにインターフェースは不要か

に公開

はじめに

私がPHPから、転職を経てプロジェクトでRubyを扱うようになった際、「PHPであったインターフェースはRubyにはないのか」という疑問を抱きました。
Rubyがインターフェースなしでなぜ成り立っているのか、調べたことをまとめてみました。

PHPになぜインターフェースが必要なのか

インターフェースの本質

PHPでインターフェースが必要とされる本質的な理由はポリモーフィズム(多態性)を実現するためです。
上位レイヤーはインターフェースだけを見て実装でき、下位レイヤーの実装を気にしなくて済むようになります。
上位レイヤーは下位レイヤーの実装に依存していない、と言えます。

Notifier を実装したクラスなら、どれを渡しても同じように扱える。
これがインターフェースの コアの役割です。

<?php

interface Notifier {
    public function send(string $to, string $message): void;
}

class MailNotifier implements Notifier {
    public function send(string $to, string $message): void {
        // 実際はメール送信処理
        echo "[MAIL] to: {$to}, message: {$message}\n";
    }
}

class LineNotifier implements Notifier {
    public function send(string $to, string $message): void {
        // 実際は LINE 送信処理
        echo "[LINE] to: {$to}, message: {$message}\n";
    }
}

Notifier インターフェースが 「send メソッドを持つこと」 を契約として保証

class UserService {
    private Notifier $notifier;

    public function __construct(Notifier $notifier) {
        $this->notifier = $notifier;
    }

    public function register(string $email): void {
        // ユーザー登録の処理(DB保存など)

        $this->notifier->send($email, "Welcome!");
    }
}

→UserService は Notifier インターフェースにだけ依存

付加価値として得られるもの

インターフェースがあることで、実務上とても大きな恩恵が生まれます。

(1) テスト時のモック差し替えが容易になる

PHPUnit ではインターフェースを指定するだけで簡単にモックが作れます。

class UserServiceTest extends TestCase
{
    public function test_register_sends_notification()
    {
        $notifier = $this->createMock(Notifier::class);
        $notifier->expects($this->once())
                 ->method('send')
                 ->with('user@example.com', 'Welcome!');

        $service = new UserService($notifier);
        $service->register('user@example.com');
    }
}

(2) 複数実装に同じ振る舞いを保証できる(契約の保証・明示化)

Notifier を実装したクラスは必ず send() を持つ。
これは 言語レベルで保証される契約(Contract) になります。

Rubyにはなぜインターフェースが不要なのか

Rubyのポリモーフィズムの実現

Rubyでポリモーフィズムの実現となると、話に上がるのはダックタイピングです。

ダックタイピングとは

「もしもそれがアヒルのように歩き、アヒルのように鳴くなら、それはアヒルであろう」

という言葉のように、オブジェクトがどの型どのクラスかは、オブジェクトがどのような振る舞いをするかで決定するというものです。
具体的に見ていきましょう。
先ほどまでPHPで書いていたコードをRubyのダックタイピングを使って表します。

class MailNotifier
  def send(to, message)
    # 実際はメール送信処理
    puts "[MAIL] to: #{to}, message: #{message}"
  end
end

class LineNotifier
  def send(to, message)
    # 実際は LINE 送信処理
    puts "[LINE] to: #{to}, message: #{message}"
  end
end

先ほどのPHPと同じく、どちらもsendという共通のメソッドを持ったクラスです。
これを上位レイヤーで呼び出す際に以下のように書くことができます。

class UserService
  def initialize(notifier:)
    @notifier = notifier
  end

  def register(email)
    # ユーザー登録処理(DB保存など)
    # ...

    @notifier.send(email, "Welcome!")
  end
end

PHPとほぼ同じ実装ですが、違うのはNotifierがインターフェースではなく「sendメソッドを持つオブジェクト」が来る前提の実装であることです。
このダックタイピングにより、上位レイヤーは下位レイヤーの実装を気にしなくとも実装できます。
上位レイヤーは下位レイヤーの実装に依存していない状態となり、ポリモーフィズムが実現できています。
これがRubyにインターフェースが不要という根拠となります。

PHPほどの付加価値があるのか

インターフェースの本質はポリモーフィズムの実現です。
この時点でRubyはインターフェースが持つ役割を十分に担っていることが分かります。
ただこれがPHPのインターフェースほどの付加価値があるのかどうか、という点で見ていきたいと思います。

(1) テスト時のモック差し替え

RSpecでモックを使ったテストを紹介します。

# spec/user_service_spec.rb
RSpec.describe UserService do
  describe "#register" do
    let(:notifier) { double("notifier") }

    subject { UserService.new(notifier: notifier) }

    it "登録完了時にWelcome通知を1回だけ送信する" do
      expect(notifier).to receive(:send_welcome_notification)
        .with("user@example.com", "Welcome!")
        .once

      subject.register("user@example.com")
    end
  end
end

double("notifier")は実体クラスがなくても作れるダブルです。
Rubyではこのように、インターフェースがなくてもモック差し替えは簡単にできるのです。

(2)契約の保証・明示化

この部分がPHPのインターフェースにあって、Rubyでは暗黙的になりがちなポイントです。

class UserService
  def initialize(notifier:)
    @notifier = notifier
  end

  def register(email)
    # ユーザー登録処理(DB保存など)
    # ...

    @notifier.send(email, "Welcome!")
  end
end

先ほどの上位レイヤーのコードです。
ここでNotifierはsendメソッドを持つオブジェクトと書かれていますが、下位レイヤーでNotifierの実装にsendメソッドを実装しなくてはいけないことが、直感的に分かりにくいです。
そしてsendメソッドのないNotifierを実装しても、そのコードを実行するまでエラーに気づけません。

私はRubyに初めて触れたとき、契約の保証・明示化がないという点がどうにも腑に落ちませんでした。
それはインターフェースの役割の一つに契約の保証・明示化があると考えており、ダックタイピングではそこを解消できていないと思っていたからです。
ただ前述した通り、インターフェースの本質はポリモーフィズムの実現です。
そう考えるとダックタイピングはインターフェースの役割を担っており、Rubyにインターフェースが不要であることも腑に落ちました。

ですが契約の保証・明示化についてRubyが暗黙的であることは見過ごせません。
この部分を指摘する記事も多く存在します。
アプローチとして、Rubyにインターフェース的なモジュールを作成したり、メソッドの存在を保証する条件を追加したりという記事を見かけました。
確かに契約を保証・明示化するのに的確なアプローチと思いますが、私としてはシンプルで直感的なRubyの特性に少しノイズかなとも思ってしまいます。

私はインターフェースをコードに落とし込むのではなく、次に書くように開発者が暗黙的な契約をいかに伝えるか、汲み取るかというアプローチが必要と思っています。

Rubyで「暗黙的な契約」を意識した実装

Rubyコミュニティで語られていたアプローチも含め、いくつか紹介いたします。

(1)コードで契約を伝える

https://stackoverflow.com/questions/177080/ruby-and-duck-typing-design-by-contract-impossible?utm_source=chatgpt.com

ここではJavaとRubyのコードを比較し、Rubyの場合メソッドの引数に何を渡すべきか、戻り値で何を受け取るべきかわからないことを問題視しています。
インターフェースが存在しないため、呼び出し元が把握できるように呼び出す可能性のあるメソッドを全て列挙する必要があるのではないか?という議題です。

ここで一番支持を得ている回答に、

You can't enforce contracts in ruby like you can in java, but this is a subset of the wider point, which is that you can't enforce anything in ruby like you can in java. Because of ruby's more expressive syntax, you instead get to more clearly write english-like code which tells other people what your contract is (therein saving you several thousand angle brackets).
RubyではJavaのように契約を強制できませんが、これはより広範な問題の一部に過ぎません。つまりRubyではJavaのように何も強制できないのです。Rubyの表現力豊かな構文のおかげで、代わりに英語のようなコードをより明確に記述でき、他の人に契約内容を伝えることができます(これにより数千もの角括弧を節約できるのです)。

と書かれています。
この回答には前半部分にメソッド名を指摘する箇所がありました。
メソッド名を明確にし、返却される型が何なのか、どこから取得するのか、このような暗黙的な契約を他の実装者に伝えることができます。

先ほどのコードに落とし込んでみましょう。

class MailNotifier
  # 登録完了時の「通知」を送る役割であることが名前から分かる
  def send_welcome_notification(to, message)
    puts "[MAIL] to: #{to}, message: #{message}"
  end
end

class LineNotifier
  def send_welcome_notification(to, message)
    puts "[LINE] to: #{to}, message: #{message}"
  end
end

class UserService
  def initialize(notifier:)
    @notifier = notifier
  end

  def register(email)
    # ユーザー登録処理(DB保存など)
    # ...

    @notifier.send_welcome_notification(email, "Welcome!")
  end
end

メソッド名をsendからsend_welcome_notificationに変更しました。
sendというメソッド名だと何を送っているかが明確ではありません。

メソッド名をsend_welcome_notificationに変更するだけで、何のための送信なのかが一目でわかるようになります。

その他にもこのコードから読み取れる契約として

  • notifier → 通知用オブジェクト
  • register → 登録時
  • send_welcome_notification → 歓迎の通知
  • email → 第一引数にメールアドレス

といったように、英語を読むかのように契約が伝わります。
Rubyの言語特性として、命名やその他コードによる契約の伝え方は意識して実装すべき部分です。

(2)テストで契約を伝える

Rubyは「テストは生きたドキュメント」を体現しており、特にRSpecでは自然言語に近い形でテストを実装できる固有言語を有しています。
テスト自体が契約を伝えるドキュメントの役割を果たしているのです。

今までの実装のテストを作成してみましょう。

# spec/user_service_spec.rb
RSpec.describe UserService do
  describe "#register" do
    let(:notifier) { double("notifier") }

    subject { UserService.new(notifier: notifier) }

    it "登録完了時にWelcome通知を1回だけ送信する" do
      expect(notifier).to receive(:send_welcome_notification)
        .with("user@example.com", "Welcome!")
        .once

      subject.register("user@example.com")
    end
  end
end

先ほどモック差し替えの解説に使用したコードです。
このテストが何を言語化しているのかというと、以下の通りです。

UserService#register は

  • "user@example.com" を引数に呼び出されたとき
  • 注入された notifier に対して
  • "Welcome!" というメッセージで
  • send_welcome_notification(email, message) を ちょうど1回だけ 呼び出す

これがそのままUserService と notifier のあいだの契約となります。

  • UserService 側のやるべきこと
  • notifier 側の提供すべきメソッドと引数

これらがテストコードから読み取れます。
テスト作成時にはドキュメントの役割を意識することが重要となります。

(3)インターフェースを"意識して"設計する

https://developers.wonderpla.net/entry/2022/03/24/110000?utm_source=chatgpt.com

ここではRubyに明示的なインターフェースはないが、設計者の頭の中では存在しているという視点を提供してくれています。
非常に重要な視点で、PHPのようにインターフェースが上位レイヤーと下位レイヤーの間に存在する設計をすることで、暗黙的な契約を整理でき実装もそれに沿ったものとなるでしょう。

今回のサンプルコードでは、最初に提示したPHPのコードをRuby実装時にも頭の中にイメージするというものになります。

<?php

interface Notifier {
    public function send(string $to, string $message): void;
}

class MailNotifier implements Notifier {
    public function send(string $to, string $message): void {
        // 実際はメール送信処理
        echo "[MAIL] to: {$to}, message: {$message}\n";
    }
}

class LineNotifier implements Notifier {
    public function send(string $to, string $message): void {
        // 実際は LINE 送信処理
        echo "[LINE] to: {$to}, message: {$message}\n";
    }
}
class UserService {
    private Notifier $notifier;

    public function __construct(Notifier $notifier) {
        $this->notifier = $notifier;
    }

    public function register(string $email): void {
        // ユーザー登録の処理(DB保存など)

        $this->notifier->send($email, "Welcome!");
    }
}

UserServiceがMailNotifierとLineNotifierの実装を持っているのではなく、UserServiceはあくまでインターフェースを持っているだけで、そのインターフェースを実装しているのがMailNotifierとLineNotifierであるという設計です。

Rubyでは当然インターフェースはないのだが、どのような引数を渡して、どのような戻り値を期待されている、どのようなメソッドが契約内容であるか、というインターフェースが存在する設計を頭の中でイメージすることが大切です。

まとめ

PHP出身の自分は、最初「インターフェースがないRuby」に強い不安を覚えました。
しかしダックタイピング+命名+テストで契約を表現する、という Ruby 独自のアプローチを知ることで、型で縛るのとは違う形でポリモーフィズムと契約を扱えることに納得感を持てるようになりました。

Discussion