1
0

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.

「HeadFirstデザインパターン」と「Rubyによるデザインパターン」を読んで Observer パターン

Last updated at Posted at 2018-07-16

何番煎じか判りませんがお勉強メモを残します

HeadFirstデザインパターン」第2章
Rubyによるデザインパターン」第5章

Observer パターン

「HeadFirstデザインパターン」でのJavaコードは(だいたい)こんな感じ

  • 気象観測所からのデータを取得するIFとしてWeatherDataクラスが存在する
  • WeatherDataオブジェクトは以下のようなメソッドを持っている
    • getTemperature => 現在の温度を返す
    • getHumidity => 現在の湿度を返す
    • getPressure => 現在の気圧を返す
    • measurementsChanged => 情報が更新されたら呼び出される(どのように呼び出されるかは認知しない)
  • 以下の出力画面を持つ
    • 現在の気象情報 (CurrentConditionsDisplay)
    • 統計データ (StatisticsDisplay)
    • 予報 (ForecastDisplay)
  • WeatherDataオブジェクトが更新されるたびに、出力画面は更新される
  • 新たに出力画面が増える事が予定されている

これはアカンパターン

public void measurementsChanged() {
  float temp     = getTemperature();
  float humidity = getHumidity();
  float pressure = getPressure();

  CurrentConditionsDisplay.update(temp, humidity, pressure);
  StatisticsDisplay.update(temp, humidity, pressure);
  ForecastDisplay.update(temp, humidity, pressure);
}

「あーだこーだ」があって
Observer パターン の出番だ!
「あーだこーだ」は「インターフェースに対してではなく、具象実装に対してコーディングしている」とかそういう感じ

Observer パターンは
オブジェクト間の1対多の依存関係を定義し、オブジェクトの状態が変化するとそれに依存しているすべてのオブジェクトに自動的に通知される
実装にはいくつか方法があるが、殆どはSubject-Observerなパターン

  • Subject (配信者、発行者、パブリッシャ)
  • Observer (購読者、サブスクライバ、孤独の観測者)

SubjectとObserverが疎結合となる設計を提供する それぞれ独立して再利用、変更できる

// 配信者は購読申込受付、解約受付、配信する!メソッドを保持している
public interface Subject {}
  public void addObserver(Observer o);
  public void removeObesrver(Observer o);
  public void notifyObservers(Observer o);
}

// 購読者は配信者からの配信を受けるメソッドを保持している
public interface Observer {
  public void update(float temp, float humidity, float pressure);
}

// 出力画面
public interface DisplayElement() {
  public void display();
}
public class WeatherData implements Subject {
  private ArrayList obervers;
  private float temprature;
  private float humidity;
  private float pressure;

  public void WeatherData() {
    observers = new ArrayList();
  }

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

  public void removeObesrver(Observer o) {
    int i = observer.indexOf(o);

    if (i >= 0) {
      observers.remove(i)
    }
  }

  public void notifyObservers() {
    for (int i = 0, s = observers.size(); i < s; i++) {
      // この辺り、Javaのダサさが爆発
      Observer observer = (Observer)observers.get(i);
      observer.update(temprature, humidity, pressure);
    }
  }

  public void measurementsChanged() {
    notifyObservers();
  }

  // テスト用メソッド
  public void setMesurements(float temprature, float humidity, float pressure) {
    this.temprature = temprature;
    this.humidity   = humidity;
    this.pressure   = pressure;
  
    measurementsChanged();
  }
}
// 2つのinterfaceを実装する
public class CurrentConditionsDisplay implements Observer, DisplayElement {
  private float temprature;
  private float humidity;
  private Subject weatherData;

  public CurrentConditionsDisplay(Subject weatherData) {
    // オブザーバを保持している理由は、将来的に購読を解約(removeObesrver)するときに便利かな程度
    this.weatherData = weatherData;

    // weatherData(配信者)さん!私を購読者にしてください!
    weatherData.addObserver(this); 
  }

  // 配信時に呼び出される
  public void update(float temprature, float humidity, float pressure) {
    this.temprature = temprature;
    this.humidity   = humidity;
    display();
  }

  // 現在の気象情報表示画面
  public void display() {
    System.out.println("CurrentConditions[" + temprature + "][" + humidity + "]")
  }
}

実行予想(気づいてるかと思いますが、わたくし Javaソースはコンパイルすらしていません)

WeatherData weatherData = new WeatherData();

CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
// StatisticsDisplay statisticsDisplay     = new StatisticsDisplay(weatherData);
// ForecastDisplay forecastDisplay         = new ForecastDisplay(weatherData);

weatherData.setMesurements(29, 70, 30.2f);
weatherData.setMesurements(30, 71, 31.2f);
weatherData.setMesurements(31, 72, 32.2f);

// => CurrentConditions[29][70]
// => CurrentConditions[30][71]
// => CurrentConditions[31][72]

Rubyではどうなるんや?

Javaの例ではObserverのコンストラクタの引数にSubjectを渡していたが、
Rubyの本の方ではinitialize内ではなく、外部からadd_observer を呼び出している
複数のSubjectを購読・・なんてするのか分からないけど可能になるし、そっちの方がワシもナウいと思うのでそうする

class WeatherData
  def initialize
    @observers = []
  end

  def add_observer(observer)
    @observers << observer
  end

  def delete_obesrver(observer)
    @observers.delete(observer)
  end

  def notify_observers
    @observers.each do |observer|
      observer.update(@temprature, @humidity, @pressure)
    end
  end

  def measurements_changed
    notify_observers
  end

  # テスト用メソッド
  def set_mesurements(temprature, humidity, pressure)
    @temprature = temprature
    @humidity   = humidity
    @pressure   = pressure

    measurements_changed
  end
end
class CurrentConditionsDisplay
  # 配信時に呼び出される
  def update(temprature, humidity, pressure)
    @temprature = temprature
    @humidity   = humidity
    display
  end

  # 現在の気象情報表示画面
  def display
    p "CurrentConditions[#{@temprature}][#{@humidity}]"
  end
end
weather_data = WeatherData.new

current_display = CurrentConditionsDisplay.new
# statistics_display = StatisticsDisplay.new
# forecast_display   = ForecastDisplay.new

weather_data.add_observer(current_display)

weather_data.set_mesurements(29, 70, 30.2)
weather_data.set_mesurements(30, 71, 31.2)
weather_data.set_mesurements(31, 72, 32.2)

# => CurrentConditions[29][70]
# => CurrentConditions[30][71]
# => CurrentConditions[31][72]

moduleを使って書き換えてみます

この例では、購読者への配信(updateメソッドの引数)にweather_dataを直接渡しています
そのためWeatherDataは attr_reader :temprature, :humidity, :pressure を持ち、購読者がそれぞれのメソッドで必要な値を取り出します

module Subject
  def initialize
    @observers = []
  end

  def add_observer(observer)
    @observers << observer
  end

  def delete_obesrver(observer)
    @observers.delete(observer)
  end

  def notify_observers
    @observers.each do |observer|
      observer.update(self)
    end
  end
end
class WeatherData
  include Subject

  attr_reader :temprature, :humidity, :pressure

  def initialize
    super()
  end

  def measurements_changed
    notify_observers
  end

  # テスト用メソッド
  def set_mesurements(temprature, humidity, pressure)
    @temprature = temprature
    @humidity   = humidity
    @pressure   = pressure

    measurements_changed
  end
end
class CurrentConditionsDisplay
  # 配信時に呼び出される
  def update(weather_data)
    @temprature = weather_data.temprature
    @humidity   = weather_data.humidity
    display
  end

  # 現在の気象情報表示画面
  def display
    p "CurrentConditions[#{@temprature}][#{@humidity}]"
  end
end
weather_data = WeatherData.new

current_display = CurrentConditionsDisplay.new
# statistics_display = StatisticsDisplay.new
# forecast_display   = ForecastDisplay.new

weather_data.add_observer(current_display)

weather_data.set_mesurements(29, 70, 30.2)
weather_data.set_mesurements(30, 71, 31.2)
weather_data.set_mesurements(31, 72, 32.2)

# => CurrentConditions[29][70]
# => CurrentConditions[30][71]
# => CurrentConditions[31][72]

Javaには組み込みの

  • java.util.Observer インターフェース
  • java.util.Observable クラス

が存在してるんで、そっちを使った方法もある というか使うべきである

もちろん、Rubyにも高機能なObservableモジュールが存在している
大きな違いは notify_observers 前に chenged フラグを付けなければいけない・・ような感じ(らしい)
各処理内で notify_observers が呼び出されても、本当に配信しなければならない状況になるまでは配信を見合わせる、ような処理が可能になる


実際のところ、あるオブジェクトがあるオブジェクトのメソッドを呼び出している、に過ぎないので
購読者(Observer)が例外をraiseしたらどうするの?
配信者(Subject)側でrescueしてあげるの?raiseの流れに任せるの?
その辺りはあなたのコードにより変わってくる。考えろ

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?