👁️

デザインパターンを4冊の本+Udemyで学習〜Observerパターン〜 オブジェクト指向原則を学ぶためにも知っておくべき内容だった

2022/08/10に公開約11,600字

学習ソース

デザインパターンを複数の視点で学ぶため、以下の書籍とUdemyで学習している。パターン毎に上から順に進めている。

  • Head Firstデザインパターン 第2版 ―頭とからだで覚えるデザインパターンの基本
    • 飽きない工夫がされており、非常に読みやすい。630ページほどあり、かなり分厚いが、イラストや画像も多いためサクサク進む。ふざけているようにも見えるが、誤った実装例から始まり、デザインパターンを使ってより変更容易等に改良していくことが分かりやすく説明されている。オブジェクト指向(OO)初心者に対しても、OOの原則等も解説しているためOOの初心者に対してもお勧めできる。元々はGame Programming Patterns, Java言語で学ぶデザインパターン, Udemyで学習していたが途中で購入してまずはこちらをメインに進めることに変更した。
  • Game Programming Patterns ソフトウェア開発の問題解決メニュー
    Game Programming Patterns (English Edition)
    • ゲームを題材としているが、Web開発や組込み等、どのようなジャンルにも使える知識が掲載されている本。ゲームが例のためイメージしやすいと評判。デザインパターンは万能ではないということも説明している。GoFのデザインパターンだけに留まらず、他の本とは異なる視点で他の設計方法などとの関連も説明してくれている。ここが重要で、視野が広がるので買って良かったと思う。言語はC++だが、C++ユーザーでなくても読めるように配慮されている。
  • Java言語で学ぶデザインパターン入門第3版
    • デザインパターンを学ぶなら、まずこれが最初に上がるような名著。QiitaやZenn、技術ブログ等でも多数紹介されている。最近第3版が出版された。言語はJavaだがJavaユーザーでない自分にも読みやすい。教科書的な本。
  • Udemy「Python デザインパターンマスター講座~Pythonの基本文法、コーディング規約、命名規約、プログラミング技術~」
    • Pythonでデザインパターンを学べる教材は少ない中見つけた講義。コーディング規約やSOLID等のプログラミングの基本も扱っている。
  • オブジェクト指向のこころ
    • Amazonでかなり高評価の本。古めの本だがデザインパターンが何のためにあるかを学ぶのに適した本。

Observerパターンとは?

GoFの定義

オブジェクト間に一対多の依存関係を定義する。
これにより、あるオブジェクトが状態を変えた時に、依存関係にあるすべてのオブジェクトに自動的にその変化が知らされ、必要な更新が行われるようにする。

Java言語で学ぶデザインパターン入門による定義

observerとは、観察(observer)する人、すなわち「観察者」という意味です。
Obsererパターンでは、観察対象の状態が変化したことが、観察者に通知されます。
Observerパターンは、状態変化に応じた処理を記述する時に有効です。

オブジェクト指向のこころによる定義

あるイベント(出来事)が発生した際に、関連するオブジェクト群にそのイベントの発生を通知したい場合がしばしば出てきます。こういった通知を自動的に行わせようというわけです。しかし、通知を受け取るオブジェクト一式に変更が発生するたびに通知を行うオブジェクトを変更したくはないはずです。(これではカーラジオを搭載した自動車が入ってくるたびに、放送局に変更を加えるようなものです。)つまり、通知する側と通知される側を分割することになるわけです。

Game Programming Patternsによる定義

厳密に定義されているわけではないため抜粋。
 ゲームに達成認定システムを追加しようとしている。プレーヤーは「100匹の敵を退治」や「橋からの落下」などの特定の重体な事柄をやり遂げることでバッジを獲得できる。認定の対象項目は多岐に渡り、あらゆる種類のいろいろな行動によってバッジが与えられるため、すっきりと実装するにはコツがいる。無造作にやろうとすると、達成認定システムのコードはあちこちに入り込んでしまう。
求められているのは、ある側面に関わるすべてのコードを一箇所にすっきりとまとめること。達成認定のコードをゲームのそこらじゅうのコードと結合させることなく、実現するためにはどうすればよいか?オブザーバーパターンはそのためにある。一片のコードによって、関心のある事象が発生したことを「その通知の受け取り先がどこであるかも気にすることなく」知らせることができる。達成認定システムは、そうした通知を送信された時にいつでも受け取れるように受信登録する。別に受け取り手がいなくてもよい。

Head Firstはイラストでわかりやすく説明されているが、画像掲載はできないため割愛する。

Observerパターンの登場人物


UML図はWikipediaより。

Java言語で学ぶデザインパターン入門参考。

  • Subject(被験者)
    • 「観察される側」。観察者であるObserver役を登録・削除するメソッドを持つ。
  • ConcereteSubject(具体的な被験者)
    • 具体的な「観察される側」を表現する役。状態が変化したら、そのことを登録されているObserver役に伝える。
  • Observer(観察者)
    • Subject役から「状態が変化した」と教えてもらう役。そのためのメソッドがupdate。Wikipediaの図ではnotifyとなっているが、updateが一般的。
  • ConcreteObserver(具体的な観察者)
    • updateメソッドを呼ばれると、Subject役の現在の状態を取得して、何らかの処理をする。

Observerパターンと関連するパターン

  • Model/View/Controller (MVC)
  • イベントキュー(イベント駆動、メッセージ)

Observerパターンが使える場面

Observerパターンは、GoFのデザインパターンの中でもかなり使われている部類のパターンで、多くのライブラリやフレームワークで使われている。Observerパターンを理解すれば、ライブラリの設計の背後にある同期を認識して理解できることらしい(Head Firstより)。

  • JDK (Java Development Kit)ではJavaBeansとSwingライブラリ
  • Javascriptのイベント
  • SwiftのKVO(Key-Value Observing)
  • Flutterのprovider, riverpodもObserverパターンと思われる

どういうときにObserverパターンが使えるか?
Game Programming Patternsに良い注意点が書いてある。
Observerパターンは同期的である。Subjectは、Observerを直接呼び出すが、これはつまり、すべてのObserverの通知メソッドから戻ってくるまで自分の仕事に戻れないということ。処理の長くかかるObserverがあると、Subjectの処理をストップさせてしまう。では、長い時間のかかる処理をやらせたい場合にどうすればよいか?イベントキューを使用すれば良い(イベント駆動型・メッセージ等とも呼ばれる。ここはいずれ別記事を書く予定)。イベントキューは非同期に処理する。
 逆に言えば、同期的に実行したい場合はObserverパターンが使える。

また、下記原則の通り、状態を持つオブジェクトが、状態の変化を通知したいが通知後の処理については知りたくない場合(上位層が下位層に依存したくない場合)に有効に使える。ただし、その処理は同期的に行われる。

ボタン等のGUIコンポーネントに一連のObserverを登録しておくと、ボタンがクリックされた際に登録済みのObserverに通知され、通知を受け取ったObserverが特定のアクションを実行できる。

Observerパターンにおけるオブジェクト指向原則

オブジェクト指向のこころより

オブジェクトは自らに対して責任を持つ

さまざまな種類のオブザーバ(Observer)が存在しますが、すべてのオブザーバは観察対象(Subject)から必要となる情報を集め、自らの責任を全うするために適切な処理を行います。

抽象クラス

Observerクラスは、通知を受ける必要のあるオブジェクトの概念を表現したものです。これにより観察対象からObserverに通知を行うための共通インタフェースが定義されます。

ポリモーフィズムによるカプセル化

観察対象は、どういった種類のオブザーバと通信しているのかについて関知しません。Observerクラスは、特定のObserverの存在を実質的にカプセル化しているのです。つまり、将来的に新たなObserverを追加する必要が出てきたとしても、Subjectを変更する必要はないというわけです。

Head Firstより

変化する部分をカプセル化する。

変化する部分はSubjectの状態とObserverの数と種類。このObserverパターンを使えば、Subjectを変更せずに、Subjectの状態に依存するオブジェクトを変更できる。

継承よりコンポジションのほうが好ましい。

継承は「is-a」、コンポジションは「has-a」。単に機能を持たせたい場合は継承ではなくコンポジションとすることが推奨される。実行時に振る舞いを変更することが可能となり、柔軟性が向上する。
Observerパターンは、コンポジションを使ってSubjectと任意の数のObserverを構成する。コンポジションにより実行時に構築される。

実装に対してではなくインタフェースに対してプログラミングする。

SubjectとObserverはどちらもインタフェースを使用する。SubjectはObserverインタフェースを実装するオブジェクトを管理し、ObserverはSubjectインタフェースを使って登録し、通知を受ける。

相互にやり取りするオブジェクト間には、疎結合設計を使用する。

  • SubjectはObserverがObserverインタフェースを実装していること以外はObserverに関して何も知らない。
  • 新しいObserverをいつでも追加できる。
  • 新しい種類のObserverを追加するのに、Subjectを変更する必要は全くない。
  • SubjectやObserverをそれぞれ独立して再利用できる。
  • SubjectまたはObserverのどちらを変更しても、他方に影響を与えない。

Observerパターンを使わなかった時のコード例

Head Firstより。

public class WeatherData {
  public void measurementsChanged() {
    float temp = getTemperature();
    float humidity = getHumidity();
    float pressure = getPressure();
    
    currentConditionDisplay.update(temp, humidity, pressure);
    statisticsDisplay.update(temp, humidity, pressure);
    forecastDisplay.update(temp, humidity, pressure);
  }
  // 他のメソッド
}

このコードは、*.updateの部分で、具象実装に対してコーディングしている。これではコードを変更せずに他の表示要素の追加や削除を行う方法がない。

実行時に表示の追加や削除を行えない。それはハードコーディングになってしまっているため。

Observerパターンを使ったコード例(Push型)

Head Firstより。

まずはインタフェース。

public interface Subject {
  public void registerObserver(Observer o);
  public void removeObserver(Observer o);
  public void notifyObservers();
}

public interface Observer {
  public void update(float temp, float humidity, float pressure);
}

public interface DisplayElement {
  public void display();
}

ConcreteSubjectを実装する。

public class WeatherData implements Subject {
  private List<Observer> observers;
  private float temperature;
  private float humidity;
  private float pressure;
  
  public WeatherData() {
    observers = new ArrayList<Observer>();
  }
  
  public void registerObserver(Observer o) {
    observers.add(o);
  }
  
  public void removeObserver(Observer o) {
    observers.remove(o);
  }
  
  public void notifyObservers() {
    for (Observer observer : observers) {
      observer.update(temperature, humidity, pressure);
    }
  }
  
  public void measurementsChanged() {
    notifyObservers();
  }
  
  public void setMeasurements(float temperature, float humidity, float pressure) {
    this.temperature = temperature;
    this.humidity = humidity;
    this.pressure = pressure;
    measurementsChanged();
  }
  
  // 他のメソッド
}

ConcreteObserverを実装する。

public class CurrentConditionDisplay implements Observer, DisplayElement {
  private float temperature;
  private float humidity;
  private WeatherData weatherData;
  
  public CurrentConditionsDisplay(WeatherData weatherData) {
    this.weatherData = weatherData;
    weatherData.registerObserver(this);
  }
  
  public void update(float temperature, float humidity, float pressure) {
    this.temperature = temperature;
    this.humidity = humidity;
    display();
  }
  
  public void display() {
    System.out.println("Current Temp:" + temperature + "degree , Humid:" + humidity + "%");
  }
}

あとは、すべてをつなぎ合わせるコード。

public class WeatherStation {
  public static void main(String[] args) {
    WeatherData weatherData = new WeatherData();
    
    CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
    
    weatherData.setMeasurements(80, 65, 30.4f);
    weatherData.setMeasurements(82, 70, 29.2f);
  }
}

Observerパターンを使わない時と比べると、変更容易で柔軟な設計になった。
しかし、1つ問題がある。それはObserverがすべての値を必要としていないのに、update()メソッドは3つのデータを送り出している(Pushしている)。後で別のデータ(風速など)を追加したくなったらどうなるか?その場合、ほぼ全てのObserverが風速データを必要としないのに、すべてのupdate()メソッドを変更しないといけない。

そこで、update()メソッドでObserverにどんどんデータを渡すよりも、Observerに必要なデータを取得させる(Pullする)方が良いと考えられる。

Observerパターンを使ったコード例(Pull型)

WeatherDataのnotifyObservers()メソッドを修正する。

  public void notifyObservers() {
    for (Observer observer : observers) {
      observer.update();
    }
  }

Observerインタフェースのupdate()メソッドを修正する。

public interface Observer {
  public void update();
}

ConcreteObserverのupdate()メソッドを修正する。

  public void update() {
    this.temperature = weatherData.getTemperature();
    this.humidity = weatherData.getHumidity();
    display();
  }

これで、データの追加も容易になった。

Push型とPull型の使い分け方法は?Pull型にすべき?

Head Firstでは、Pullの方が適切と考えられていると記載されている。

Push型

  • メリット:ObserverがSubjectのどの状態が変更されたのかを直接知ることができるのが特徴。update()の引数で渡されてきたものが変更された状態のため。
  • デメリット:update()メソッドに引数をとる形でインタフェースを定義するため、一般化には向かない。

Pull型

  • メリット:一般化したupdate()メソッドを定義できるため、より柔軟な設計となる。
  • デメリット:Subjectのどの状態が変更されたのかをSubjectに問い合わせる必要がある。Subjectに状態が大量にある場合は、複雑となる。

Subjectの状態が単純で拡張もあまり考えられない場合はPush型、Subjectの状態が複雑であればPull型にすればよいと思われるが、確かにPull型を基本として良いと思った。

参考:実践デザパタ-その14:Observerパターン

Observerパターンの注意点

Game Programming Pattersには、次の注意点が書かれていた。

他方、プログラムがうまく動かず、オブザーバのつながりのどこかにバグが紛れている場合、その通信フローを論理的に特定することは、はるかに難しくなります。明示的に結合されている場合、呼び出されるメソッドを見つけ出すのは簡単です。結合が静的であるため、平均的なIDEにとっては朝飯前です。
 しかしその結合がオブザーバリストを介して行われる場合、何が通知を受けるのかを見極める唯一の方法は、実行時にそのリストにどのオブザーバが入っているのかを見ることです。プログラムの通信構造を静的には確定できないので、命令の流れと、動的な振る舞いから原因を特定しなければなりません。

Observerパターンは、ほぼ関係のない2つの塊を1つの大きな塊にせずに、相互に話ができるようにするのに良い方法。1つの機能や局面に特化した、一塊になったコードの内部ではそれほど役には立たない。

Java言語で学ぶデザインパターン入門には、次の注意点が書かれていた。

Observerの行為がSubjectに影響を与えてしまうと、メソッド呼び出しループに陥る可能性がある。

Subjectの状態が変化

Observerへ通知

ObserverがSubjectのメソッドを呼び出す

それによってSubjectの状態が変化

Observerへ通知

:

この本では、

現在通知の処理中かどうかをObserver役が判断したり、通知を送るタイミングをSubject役が考慮したりといった対策が必要になるでしょう。

と記載があった。しかし、基本的にはObserverとSubjectは疎結合にすべきものなので、ObserverがSubjectの状態を変更してしまうという時点で設計がおかしいと判断した方が良いと思われる。

関数(ラムダ式)によるObserverパターン

Head Firstではラムダ式による実装が紹介されており、Game Programming Patternsには、

もっと現代的なアプローチでは、「オブザーバ」をメソッドまたは関数への単なる参照にします。ファーストクラス関数のある言語、特にクロージャが使える場合、オブザーバを実装するにはそのアプローチがはるかに一般的です。

もし私が現時点でオブザーバのシステムを設計しているとしたら、クラスベースではなく関数ベースにするでしょう。C++の場合でも、インタフェースObserverに準拠したインスタンスの代わりに、メンバ関数へのポインタをオブザーバとして登録できる方式にしたくなります。

と記載がある。

https://www.oracle.com/webfolder/technetwork/jp/javamagazine/Java-ND16-Java8Patterns.pdf
にラムダ式の例があった。

感想

Head Firstが思ったよりも楽しく、そしてわかりやすく、オブジェクト指向の勉強にも適していることが分かったため、それを主軸に他の本を読むことに決めた。初めてデザインパターンを学ぶ人は、Head Firstをおすすめする。しかし、それだけでは足りない情報もあるため、Game Programming PatternsとJava言語で学ぶデザインパターン入門も併せて読むとより相乗効果が得られると思う。

自分はJavaユーザーではないので、Pythonで書く方法をUdemyで学び(1つのパターンあたり倍速で10分程度なので時間もかからない)、Pythonでコードを書いて動かすことで理解している。その点でUdemyも有効活用している。ただ、Udemyだけだとふわっとした理解になるので他の本と併せてやるのがやはりbetterと思う。

デザインパターンはあまり使われなくなったからあまり勉強しなくて良いという人も結構いるが、そういった方々はすでにデザインパターンを理解してOOPについてもしっかりと身につけた強者の方に多いと思う。デザインパターン自体は実務であまり使わないかもしれないが、オブジェクト指向を理解したい人にとっては良い教材となると思う(Observerパターンは実務で使っている人もいた)。

デザインパターン関係で読んでいる、または読みたいものリスト

Discussion

ログインするとコメントできます