💎

設計課題から理解するデザインパターンの本質 #11 Mediator

に公開

🎯設計課題

多対多のオブジェクト間依存を排除し、やり取りを仲介者に集約することで、オブジェクト同士を疎結合に保ちつつ処理の見通しをよくしたい。

例えば認証ダイアログでは、チェックボックス・ログインフォーム・登録フォームが互いに連動する。これを各オブジェクトが直接参照し合うかたちで実装すると、依存が絡み合って変更が困難になる。

🧩登場する要素と役割

  • MediatorAuthDialog): Colleague からのイベントを受け取り、他の Colleague を制御する
  • ColleagueColleague): Mediator への参照を持つ基底クラス
  • ConcreteColleagueLoginCheckbox, LoginForm, RegisterForm): 自身の操作後に Mediator へイベントを通知する
  • NullComponent: 初期化時のプレースホルダー。enable/disable を no-op にする Null Object

🪶サンプルコード

テスト

test/mediator/auth_dialog_test.rb
require_relative "../test_helper"
require_relative "../../lib/mediator/auth_dialog"

class AuthDialogTest < Minitest::Test
  def test_check_checkbox
    auth_dialog = AuthDialog.new
    refute auth_dialog.login_checkbox.checked?
    refute auth_dialog.login_form.enabled?
    assert auth_dialog.register_form.enabled?

    auth_dialog.login_checkbox.check

    assert auth_dialog.login_checkbox.checked?
    assert auth_dialog.login_form.enabled?
    refute auth_dialog.register_form.enabled?
  end

  def test_uncheck_checkbox
    auth_dialog = AuthDialog.new
    auth_dialog.login_checkbox.check
    assert auth_dialog.login_checkbox.checked?
    assert auth_dialog.login_form.enabled?
    refute auth_dialog.register_form.enabled?

    auth_dialog.login_checkbox.uncheck

    refute auth_dialog.login_checkbox.checked?
    refute auth_dialog.login_form.enabled?
    assert auth_dialog.register_form.enabled?
  end

  def test_submit_register_form
    auth_dialog = AuthDialog.new
    refute auth_dialog.login_checkbox.checked?
    refute auth_dialog.login_form.enabled?
    assert auth_dialog.register_form.enabled?

    auth_dialog.register_form.submit

    assert auth_dialog.login_checkbox.checked?
    assert auth_dialog.login_form.enabled?
    refute auth_dialog.register_form.enabled?
  end
end

アプリケーション

lib/mediator/component.rb
module Component
  def enable   = @enabled = true
  def disable  = @enabled = false
  def enabled? = @enabled
end
lib/mediator/null_component.rb
require_relative "component"

class NullComponent
  include Component
  def enable;  end
  def disable; end
  def enabled? = false
end
lib/mediator/colleague.rb
class Colleague
  def initialize(mediator:)
    @mediator = mediator
  end
end
lib/mediator/login_checkbox.rb
require_relative "colleague"

class LoginCheckbox < Colleague
  def check
    @checked = true
    @mediator.event_from(self, type: :check)
    self
  end

  def uncheck
    @checked = false
    @mediator.event_from(self, type: :uncheck)
    self
  end

  def checked? = @checked
end
lib/mediator/login_form.rb
require_relative "colleague"
require_relative "component"

class LoginForm < Colleague
  include Component
end
lib/mediator/register_form.rb
require_relative "colleague"
require_relative "component"

class RegisterForm < Colleague
  include Component

  def submit
    @mediator.event_from(self, type: :submit)
  end
end
lib/mediator/auth_dialog.rb
require_relative "null_component"
require_relative "login_checkbox"
require_relative "login_form"
require_relative "register_form"

class AuthDialog
  attr_reader :login_checkbox, :login_form, :register_form

  def initialize
    @login_checkbox = NullComponent.new
    @login_form     = NullComponent.new
    @register_form  = NullComponent.new

    @login_checkbox = LoginCheckbox.new(mediator: self).uncheck
    @login_form     = LoginForm.new(mediator: self).tap(&:disable)
    @register_form  = RegisterForm.new(mediator: self).tap(&:enable)
  end

  def event_from(object, type:)
    case [object.class, type]
    in [LoginCheckbox, :check]
      @login_form.enable
      @register_form.disable
    in [LoginCheckbox, :uncheck]
      @login_form.disable
      @register_form.enable
    in [RegisterForm, :submit]
      @login_checkbox.check
      @login_form.enable
    end
  end
end

💎パターンの本質

LoginCheckboxLoginForm を知らない。RegisterFormLoginCheckbox を知らない。各 Colleague は「自分に何かが起きたら Mediator に通知する」だけで、その後の連動ロジックはすべて AuthDialog#event_from に集まっている。Colleague 同士を直接依存させず、やり取りをすべて Mediator に集約することが Mediator パターンの本質。

Discussion