Observerパターン(GoF)

参考書籍一覧(Amazon アソシエイトリンク)

イベントハンドラの仕組みを実現したい場合に適用するパターン。

ECサイトで商品の発送または配達が行われたらユーザーに通知する処理を実装しているとする。

def dispatch
  # 発送時の他の処理...
  send_dispatched_notice_mail # メールでユーザーに発送された通知をする
end

def deliver
  # 配達時の他の処理...
  send_delivered_notice_mail # メールでユーザーに配達された通知をする
end

この実装をした後で、WEB UI上のお知らせ機能でも通知することになり、さらに、ユーザー設定で通知先を切り替えられるように追加要求が発生したとする。

def dispatch
  # 発送時の他の処理...
  if user.notice_setting.email?
    send_dispatched_notice_mail
  end
  if user.notice_setting.web_notice?
    send_dispatched_notice_web
  end
end

def deliver
  # 同じように条件分岐が追加される...
end

さらに通知先が増えたり条件が複雑化すれば、dispatchdeliverの実装が食い違ってしまったりして破綻するのは簡単に予想できる。

立ち止まって考えると、dispatchdeliverメソッドにとってメールを出すのかWEB UIに通知を出すのかは重要ではなく、「(具体的なところに興味はないが)通知を出す」ということに関心がある。したがって、以下のようになっている方が自然である。

class PackageTracking
  def initialize(notifiers)
    @notifiers = Array(notifiers)
  end

  def dispatch
    # 発送時の他の処理...
    # 通知すべきかどうかと何に通知するかは
    # @notifiersのそれぞれのインスタンスが知っている
    @notifiers.each { |notifier| notifier.notify(:dispatched) }
  end

  def deliver
    # 配達時の他の処理...
    @notifiers.each { |notifier| notifier.notify(:delivered) }
  end
end

class AbstractNotifier
  def initialize(notice_setting)
    @notice_setting = notice_setting
  end
end

class EmailNotifier < AbstractNotifier
  def notify(target)
    return unless @notice_setting.email?
    # targetごとに処理を振り分けてメールを送信...
  end
end

class WebUINotifier < AbstractNotifier
  def notify(target)
    return unless @notice_setting.web_notice?
    # targetごとに処理を振り分けてWEB UIで通知...
  end
end

# サンプルコード
notifier = EmailNotifier.new(user.notice_setting)
# メール通知だけユーザー設定によっては通知する
PackageTracking.new(notifier)

この時、EmailNotifierWebUINotifierクラスがObserverと呼ばれるクラスになる。

Observerパターンを適用することで、サンプルコードのように実行するObserverを動的に切り替えられるようになった。単体テストを実行する際、最初のコードではメールが送信されてしまっていたが、モックのObserverクラスを渡すことができるようになったため、通知が出されたかどうかだけをテストすることが可能になった。

余談ではあるが、EmailNotifier#notifyメソッドはシンボルではなく、DispatchEventクラスなどを用意した上でそのインスタンスを受け取るようにした方が良いだろう。それらのインスタンスに委譲すれば、EmailNotifier#notifyメソッドでの振り分けが不要になる。