エンジニアがRubyでデザインパターンを学ぶ - Observer Pattern


概要

「Rubyによるデザインパターン」を読んでデザインパターンを勉強中。
Javaをやっていた人間としての目線で情報を整理してみます。

他のデザインパターン

Template Method Pattern
Strategy Pattern

Observer Pattern

  • Subject(観測対象) のオブジェクトは Observer(観測者) オブジェクトのコレクションを保持
  • Subject に対して変更が加えられた場合などに Observer に対して通知する

Java での実装

Subject

Subject.java
public interface Subject {
  public void addObserver(Observer observer);
  public void notifyObservers();
  public String getStatus(); // Subject からの情報取得を簡単にするためこれもインターフェースに入れておきます
}

Observer

Observer.java
public interface Observer {
  public void update(Subject subject);
}

ConcreteSubject

ConcreteSubject.java
import java.util.Set;
import java.util.HashSet;

public class ConcreteSubject implements Subject {
  private Set<Observer> observers;

  public enum Status {
    NORMAL, ERROR
  }
  private Status status;

  public void setStatus(Status status) {
    this.status = status;
    notifyObservers(); // status フィールドを更新したら Observer に通知するようにしておきます
  }

  public String getStatus() {
    return status.toString();
  }

  public ConcreteSubject() {
    observers = new HashSet<Observer>();
    status = Status.NORMAL;
  }

  public void addObserver(Observer observer) {
    observers.add(observer);
  }

  // Observer に通知を行う
  // Observer 側で ConcreteSubject の情報を取得するために this を渡しています
  public void notifyObservers() {
    for (Observer observer : observers) {
      observer.update(this);
    }
  }
}

ConcreteObserver

ConcreteObserver.java
public class ConcreteObserver implements Observer {
    // 通知されたら Subject のステータスを表示
    public void update(Subject subject) {
      System.out.printf("<Subject value=%s>%n", subject.getStatus());
    }
}

呼び出し

Main.java
import java.util.Scanner;

public class Main {
  public static void main(String[] args) {
    ConcreteSubject subject = new ConcreteSubject();

    // Observer を指定
    subject.addObserver(new ConcreteObserver());

    // Subject を変更を加える度にすぐさま Observer に変更が通知される
    subject.setStatus(ConcreteSubject.Status.NORMAL);
    subject.setStatus(ConcreteSubject.Status.ERROR);
  }
}

実行結果

$> java Main
<Subject value=NORMAL>
<Subject value=ERROR>

Ruby での実装

基本的に Java と同じように書けます。

Subject

  • Observer のコレクションを内部に持つ
  • 全ての Observer に対して通知を行う(Observer#update メソッドを呼び出す)

という処理をモジュールとして実装し、それを include する形にします。
※Ruby の標準添付ライブラリ(observer)に Observable というモジュールが存在するので、実際にはそれを使うことが多いと思います

module Subject
  def initialize
    @observers = []
  end

  def add_observer(observer)
    @observers << observer
  end

  def notify_observers
    @observers.each do |obs|
      obs.update(self)
    end
  end
end

Observer

不要。
各具象クラスで update メソッドを実装していれば良い。

ConcreteSubject

class ConcreteSubject
  include Subject

  attr_accessor :status

  module Status
    NORMAL = "NORMAL"
    ERROR = "ERROR"
  end

  def status=(status)
    @status = status
    notify_observers
  end
end

ConcreteObserver

class ConcreteObserver
  def update(subject)
    puts "<Subject status=#{subject.status}>"
  end
end

呼び出し

subject = ConcreteSubject.new
subject.add_observer(ConcreteObserver.new)

# status を更新すると ConcreteObserver に通知されます
subject.status = ConcreteSubject::Status::NORMAL
subject.status = ConcreteSubject::Status::ERROR

Ruby での実装 (よりRubyらしく)

Strategy Pattern でもそうでしたが、Observer 側の処理がシンプルならばわざわざクラスを定義せず、ブロックとして渡してしまう手もあります。

Subject

module Subject
  def initialize
    @observers = []
  end

  def add_observer(&observer)  # observer は Proc オブジェクトとして保持
    @observers << observer
  end

  def notify_observers
    @observers.each do |obs|
      obs.call(self)           # Proc#call で処理呼び出し
    end
  end
end

呼び出し

subject = ConcreteSubject.new

# ブロックとして observer を登録
# 実行時は obs.call(self) として渡した self が changed_subject に入ります
subject.add_observer do |changed_subject|
  puts "<Subject status=#{changed_subject.status}>"
end

subject.status = ConcreteSubject::Status::NORMAL
subject.status = ConcreteSubject::Status::ERROR

Sample Code

Curses という、TUI(テキストユーザインターフェース)のアプリを作ることができるライブラリを使って、簡単な表計算を行うプログラムを書いてみました。
思ったよりコード量が多くなってしまったので全量は こちら に置いています。

module ObservableHash
  include Observable

  def []=(k, v)
    super(k, v)
    changed
    notify_observers
  end
end

prices = {
  pencil: 30,
  book: 1500,
  stapler: 980
}
prices.extend(ObservableHash)

というように、Hash が更新されたら Observer に通知されるようにして、画面表示を更新する仕組みになっています。


参考

Olsen, R. 2007. Design Patterns in Ruby

Curses についてはこちらを参考にさせてもらいました。
Curses for Ruby|やまいも|note