10
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

概要

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

####他のデザインパターン
Template Method Pattern
Strategy Pattern

Observer Pattern

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

png.png

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 に通知されるようにして、画面表示を更新する仕組みになっています。

step1.png
step2.png

参考

Olsen, R. 2007. Design Patterns in Ruby

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

10
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?