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
さらに通知先が増えたり条件が複雑化すれば、dispatch
とdeliver
の実装が食い違ってしまったりして破綻するのは簡単に予想できる。
立ち止まって考えると、dispatch
やdeliver
メソッドにとってメールを出すのか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)
この時、EmailNotifier
やWebUINotifier
クラスがObserverと呼ばれるクラスになる。
Observerパターンを適用することで、サンプルコードのように実行するObserverを動的に切り替えられるようになった。単体テストを実行する際、最初のコードではメールが送信されてしまっていたが、モックのObserverクラスを渡すことができるようになったため、通知が出されたかどうかだけをテストすることが可能になった。
余談ではあるが、EmailNotifier#notify
メソッドはシンボルではなく、DispatchEvent
クラスなどを用意した上でそのインスタンスを受け取るようにした方が良いだろう。それらのインスタンスに委譲すれば、EmailNotifier#notify
メソッドでの振り分けが不要になる。