銀行員からのRailsエンジニア

銀行員からのRailsエンジニア

銀行員から転身したサービス作りが大好きなRailsエンジニアのブログです。個人で開発したサービスをいくつか運営しており、今も新しいサービスを開発しています。転職して日々感じていること、個人開発サービス運営のことなどを等身大で書いていきます。

当ブログではアフィリエイト広告を利用しています

【Rubyによるデザインパターンまとめ3】オブザーバーパターン

コードの品質向上のため、Rubyデザインパターンを解説した名著である Rubyによるデザインパターン で紹介されているデザインパターンを1つずつまとめており、今回が第3弾です。(毎週1つが目標です!)

前回の記事(ストラテジーパターンのまとめ)はこちらです。
ysk-pro.hatenablog.com

この本で紹介されているサンプルコードをそのまま使うのは面白くないので、オリジナルのコードで説明しています。

今回はObserverパターンについてまとめました。
f:id:ysk_pro:20191110123459p:plain

Observerパターンとは

あるオブジェクトの状態が変化した時に、そのオブジェクトが変化したことを知る必要があるオブジェクトに通知をすデザインパターンです。

Rubyによるデザインパターン では次のように説明されていました。

GoFは「何らかのオブジェクトが変化した」というニュースの発信者と消費者の間に綺麗なインターフェイスを作るアイデアを、Observerパターンと呼んでいます。

GoFとは、ギャング オブ フォーの略でデザインパターンを広めた「オブジェクト指向における再利用のためのデザインパターン」の4人の著者のことです)

自らが変化したことを通知するオブジェクトのことを「Subject」(話題になっている事柄)と呼び、Subjectの通知を受け取るオブジェクトのことを「Observer」(観察者)と呼ぶことから、Observerパターンと名付けられました。

言葉での説明よりも実際のコードを見た方が分かりやすいと思うので、以下のサンプルコードをご覧ください。

サンプルコード

商品価格が変更された時に、様々な処理を行う必要があるプログラムを考えてみます。(ECサイトなどで実際によくあるケースだと思います)

まずは、商品価格が変わった時に、ユーザへの通知のみを行うプログラムを、Observerパターンを使わずに実装してみます。

class Item
  attr_reader :name, :price, :notification

  def initialize(name, price, notification)
    @name = name
    @price = price
    @notification = notification
  end

  def price=(new_price)
    @price = new_price
    notification.update(self) # 通知の処理は、Notificationクラスに委譲している
  end
end

class Notification
  def update(changed_item)
    puts "#{changed_item.name}の値段が#{changed_item.price}円になったよ!"
  end
end

商品価格が変わった時に呼ばれる price= メソッドの中で、Notificationクラスのupdateメソッドに通知処理を委譲しています。

次のように実行します。
Itemクラスのインスタンスを作る際に、Notificationクラスのインスタンスを渡す必要があります。

item = Item.new('ダンベル', 3000, Notification.new)
item.price = 2500

実行結果は、次のようになります。

ダンベルの値段が2500円になったよ!

ここで、価格変更をした時に、ユーザへの通知のみではなく、配送方法を変える処理も必要になった場合を考えてみましょう。

先ほどのコードに追加で、Itemクラスのインスタンス作成時に配送方法を変える処理を行うクラスのインスタンスを渡し、price= メソッド内で新しく作ったクラスに配送方法変更の処理を委譲することで実現できると思います。

しかしこのやり方だと、ItemクラスはNotificationクラスや新しく追加したクラスのことを知る必要があり(それぞれのクラスのどのメソッドを呼び出す必要があるかなど)、価格変更した後に行う処理を追加する度に、Itemクラスを修正する必要が出てきてしまいます。

商品の価格変更をした後に行うというだけで、直接商品とは関係ない処理を追加するにも関わらず Itemクラスの修正が必要になってしまうのは保守性の観点で良くないと言えるでしょう。(商品の価格変更した後の処理を追加する際に、Itemクラスを壊してしまうおそれがあります)

このコードを、Observerパターンを使って書き換えてみます。

class Item
  attr_reader :name, :price, :observers

  def initialize(name, price)
    @name = name
    @price = price
    @observers = []
  end

  def price=(new_price)
    @price = new_price
    notify_observers
  end

  def notify_observers
    observers.each do |observer|
      observer.update(self) # Observerそれぞれに処理を委譲している
    end
  end

  def add_observer(observer)
    observers << observer
  end
end

class Notification
  def update(changed_item)
    puts "#{changed_item.name}の値段が#{changed_item.price}円になったよ!"
  end
end

class DeliveryMethod
  def update(changed_item)
    puts "#{changed_item.name}の値段によって配送方法を分ける処理を書くよ"
  end
end

Itemクラスが自らに変化があったことを通知するSubjectクラスで、Notificationクラス・DeliveryMethodクラスが通知を受け取るObserverクラスになっています。

Itemクラスに @observers というインスタンス変数を作り、その中に価格変更後に通知を受け取るObserverクラスのインスタンスを格納することで、Itemクラスは @observers に価格変更があったことを通知することだけを知っていればよく、Notification, DeliveryMethod クラスについて知る必要がなくなりました。

次のように実行します。
add_observer メソッドで、observer を追加しています。

item = Item.new('ダンベル', 3000)
item.add_observer(Notification.new)
item.add_observer(DeliveryMethod.new)
item.price = 2500

実行結果は、次のようになります。

ダンベルの値段が2500円になったよ!
ダンベルの値段によって配送方法を分ける処理を書くよ

価格変更時に新たな処理を加えたい場合は、observerを追加するだけでよく、Itemクラスを修正する必要がなくなりました。

また、Rubyの標準ライブラリには、Observerパターンを実装するためのObservableモジュールがあり、このモジュールをSubjectクラスにincludeすることで、Observerパターンをシンプルに実装することができます。

先ほどのコードを、Observableモジュールを使って書き換えると次のようになります。

require 'observer'

class Item
  include Observable

  attr_reader :name, :price, :observers

  def initialize(name, price)
    @name = name
    @price = price
  end

  def price=(new_price)
    @price = new_price
    changed # changedメソッドを呼ぶことで、オブジェクトに変更があることを伝えている
    notify_observers(self)
  end
end

class Notification
  def update(changed_item)
    puts "#{changed_item.name}の値段が#{changed_item.price}になったよ!"
  end
end

class DeliveryMethod
  def update(changed_item)
    puts "#{changed_item.name}の値段によって配送方法を分ける処理を書くよ"
  end
end

先ほどと同じ呼び出し方で、同じ実行結果になります。

おわりに

ここまで読んでいただきありがとうございます。

前回書いたStrategyパターンの記事を読んでいただいた方は気づいたかもしれませんが、ObserverパターンはStrategyパターンと少し似ている気がするなぁと思っていたら、Rubyにおけるデザインパターンに次の記載がありました。

ObserverパターンとStrategyパターンは少し似ています。どちらも、あるオブジェクトが、他のオブジェクトを呼び出すという特徴があります。ほとんどの場合、違いは目的という一点だけです。Observerパターンの場合、発信側のオブジェクトで発生しているイベントを他のオブジェクトに伝えています。Strategyパターンの場合、何かの処理を行うためにオブジェクトを取得します。

TemplateメソッドパターンとStrategyパターンも同じ目的のために、違う手段で実装していましたが、デザインパターン同士で似ているところなどの関連があって面白いですね。
違いを認識しておくことで、それぞれのデザインパターンへの理解がより深まりそうです。

Rubyによるデザインパターン の中では、従業員の給与が変わった時に、経理部門や税務署員に通知するというサンプルコードを用いて説明がされていて非常に分かりやすかったので、ご興味ある方は是非合わせてご覧ください。

Rubyによるデザインパターン

Rubyによるデザインパターン

次回は、Compositeパターンをまとめます。どんなパターンか楽しみです。

来週も頑張ります!

(追記)
Compositeパターンについてまとめました!
是非合わせてご覧ください。
ysk-pro.hatenablog.com