概要
「Rubyによるデザインパターン」を読んでデザインパターンを勉強中。
Javaをやっていた人間としての目線で情報を整理してみます。
####他のデザインパターン
Template Method Pattern
Strategy Pattern
Observer Pattern
- Subject(観測対象) のオブジェクトは Observer(観測者) オブジェクトのコレクションを保持
- Subject に対して変更が加えられた場合などに Observer に対して通知する
Java での実装
Subject
public interface Subject {
public void addObserver(Observer observer);
public void notifyObservers();
public String getStatus(); // Subject からの情報取得を簡単にするためこれもインターフェースに入れておきます
}
Observer
public interface Observer {
public void update(Subject subject);
}
ConcreteSubject
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
public class ConcreteObserver implements Observer {
// 通知されたら Subject のステータスを表示
public void update(Subject subject) {
System.out.printf("<Subject value=%s>%n", subject.getStatus());
}
}
呼び出し
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