前回の記事TypeScriptでデザインパターンを理解する ①Strategyパターンに引き続きデザインパターンの解説をしていきます。
今回紹介するのはObserverパターンです。名前を聞いてピンと来た方もいるかもしれませんがRxJSでも基本となって使われているらしいです。らしいのところのリンクを踏んでもらってその記事読んでもらった方がわかりやすい気がすごくしますがせっかくなのでこちらも最後まで読んでいってください。
間違いや「ここをこうすればもっと綺麗にかける」という指摘を貰えると泣いて喜びます。
Observerパターンを使いたくなるシチュエーション
RxJSを使いたくなるときはだいたいObserverパターンを使いたくなるときだと思っていただいて結構なのですがRxJS何それ?な方もいるかもしれないのでシチュエーションの説明をします。
おめでとうございます!
あなたのチームは、Wather-O-Rama次世代インターネットベース気象観測所の契約を勝ち取りました。
読んでいる本からコピペしたい気持ちを押さえて自分の言葉で書くことにしましょう。
あなたは気象観測所からデータを受け取ります。気象観測所がどのようにしてデータを送る方法を実装しているかは知りませんが{temperature:number,humidity:number,pressure:number}
という形でデータが送られてくることがわかっています。
あなたが実装すべきことは受け取ったデータを表示する様々な機器の表示を更新する機能です。
複数の機器(以下ではこれらの一つをObserver
と呼ぶことにします)にデータを新しくデータを受け取ったことを通知するオブジェクトを作らないといけませんね。それをSubject
と呼ぶことをします。
Subject
はおそらくObserver
を新しく登録したり、もしくは登録を解約するメソッドを持っていた方が便利でしょう。
手元で動くコード
前回と同様に手元で動くようにコードを置いておきます。いま全部読む必要はありませんよ?小分けにして解説します。あなたの手元で動くためだけに置いているのです。
ディレクトリ構成は以下のようになっています。
root
├ dist/(コンパイルしたjsファイル)
├ src/
│ ├ lib.d.ts
│ ├ weatherData.ts
│ ├ display.ts
│ └ index.ts
├ tsconfig.json
└ package.json
型やインターフェースの定義
export type Weather = {
temperature:number
humidity:number
pressure:number
}
export interface Observer{
update(weather:Weather):void
}
export interface Subject{
registerObserver(o:Observer):void
removeObserver(o:Observer):void
notifyObservers():void
}
export interface DisplayElement{
display():void
}
Subject
の実装
import { Observer, Subject, Weather } from "./lib";
export class WeatherData implements Subject{
private observers:Observer[] =[]
private weather:Weather|undefined
public registerObserver(o:Observer){
this.observers.push(o)
}
public removeObserver(o:Observer){
let i = this.observers.indexOf(o);
if(i >=0){
this.observers.splice(i,1)
}
}
public notifyObservers(){
for(let i = 0;i<this.observers.length;i++){
let obs = this.observers[i]
if(this.weather !== undefined){
obs.update(this.weather)
}
}
}
public setMeasurements(weather:Weather){
this.weather =weather
this.notifyObservers()
}
}
Observer
の実装
import { DisplayElement, Observer, Subject, Weather } from "./lib";
export class CurrentConditionsDisplay implements Observer,DisplayElement{
private weather:Weather|undefined
private weatherData:Subject
constructor(weatherData:Subject) {
this.weatherData = weatherData
this.weatherData.registerObserver(this)
}
public update(weather:Weather){
this.weather = weather
this.display()
}
display(){
if(this.weather !== undefined){
console.log("Current conditions:"+this.weather["temperature"]
+ "F degrees and " + this.weather["humidity"] + "% humidity and "
+ this.weather["pressure"]+ "Pa")
}
}
実行ファイル
import { CurrentConditionsDisplay } from "./display";
import { WeatherData } from "./weatherData";
let weahterData = new WeatherData()
let currentConditionDisplay = new CurrentConditionsDisplay(weahterData)
weahterData.setMeasurements({temperature:100,humidity:40,pressure:1015})
weahterData.setMeasurements({temperature:99,humidity:50,pressure:1000})
//currentConditionDisplayをweatherDataのObserversから除外
weahterData.removeObserver(currentConditionDisplay)
//ここから下の情報は出力されないことがわかる
weahterData.setMeasurements({temperature:90,humidity:45,pressure:1111})
Subject
インターフェースとObject
インターフェース
lib.d.ts
を見ていきましょう。Weather
とDisplayElement
については説明はいらないですね。今回の例でたまたま登場するだけだからです。
では、重要なSubject
インターフェースとObject
インターフェースを見てみましょう。
export interface Observer{
update(weather:Weather):void
}
export interface Subject{
registerObserver(o:Observer):void
removeObserver(o:Observer):void
notifyObservers():void
}
Subject
インターフェースはObserver
を登録したり削除するメソッドregisterObserver
メソッドとremoveObserver
メソッドを持っています。
そして、notifyObservers
メソッドが実行されると登録されている全てのObserver
のupdate
メソッドが実行されるという算段なのです。
notifyObservers
メソッドの引数は空ですね。そうですObserver
のupdate
メソッドはSubject
の状態を引数として受け取るのです。今回の場合はweather
がSubject
のプロパティです。
では、Subject
インターフェースを満たすWeatherData
クラスの実装を見ていきましょう。
Subject
を満たすクラスの実装
WeatherData
クラスは内部状態としてobservers:Observer[]
とweather:Weather
を持ちます。observers
は登録されているObserver
のリストです。weather
はその時点での最新の天気です。
先ほど説明した通りregisterObserver(o:Observer)
でObserver
を新しく登録し、removeObserver(o:Observer)
で指定したObserver
を取り除き、notifyObservers()
でobservers
の中に入っている全てのObserver
のupdate
メソッドを呼び出していることがわかりますね。
export class WeatherData implements Subject{
private observers:Observer[] =[]
private weather:Weather|undefined
public registerObserver(o:Observer){
this.observers.push(o)
}
public removeObserver(o:Observer){
let i = this.observers.indexOf(o);
if(i >=0){
this.observers.splice(i,1)
}
}
public notifyObservers(){
for(let i = 0;i<this.observers.length;i++){
let obs = this.observers[i]
if(this.weather !== undefined){
obs.update(this.weather)
}
}
}
public setMeasurements(weather:Weather){
this.weather =weather
this.notifyObservers()
}
}
setMeasurements(weather:Weather)
メソッドは最新の天気データを受け取って更新したあとnotifyObservers()
を呼び出します。
次はObserver
の実装を見てみましょう。
Observer
を満たすクラスの実装
内部状態としてweather:Weather
とweatherData:Subject
を持っています。(実はweatherData
をプロパティとして持つ必要はありません。どうすれば省略できるでしょうか?あとで拡張しやすいように持たせているだけです!あとで実際にnext
メソッドを追加してみます。)
こちらは何てことありません。update(weather:Weather)
メソッドはweather
を受け取ってthis.weather
を更新してdisplay()
メソッドを呼び出すだけです。
export class CurrentConditionsDisplay implements Observer,DisplayElement{
private weather:Weather|undefined
private weatherData:Subject
constructor(weatherData:Subject) {
this.weatherData = weatherData
this.weatherData.registerObserver(this)
}
public update(weather:Weather){
this.weather = weather
this.display()
}
display(){
if(this.weather !== undefined){
console.log("Current conditions:"+this.weather["temperature"]
+ "F degrees and " + this.weather["humidity"] + "% humidity and "
+ this.weather["pressure"]+ "Pa")
}
}
実際に動かしてみる
では、実際に動かして動作を確認してみましょう。
どのような出力になるか予想してから動かしてみてください!
let weahterData = new WeatherData()
let currentConditionDisplay = new CurrentConditionsDisplay(weahterData)
weahterData.setMeasurements({temperature:100,humidity:40,pressure:1015})
weahterData.setMeasurements({temperature:99,humidity:50,pressure:1000})
weahterData.removeObserver(currentConditionDisplay)
weahterData.setMeasurements({temperature:90,humidity:45,pressure:1111})
予想通りでしたか?
答えは
Current conditions:100F degrees and 40% humidity and 1015Pa
Current conditions:99F degrees and 50% humidity and 1000Pa
ですね。
まとめ
今回のObserverパターンは同じことをしようと思うと他の実装はなかなか思いつかないなって感じますね。
いろいろな種類があるらしいですが自力で思いついてみたいものですね。
Subject
と言っておきながらnext
メソッドが実装されていないぞという声が聞こえてきそうなので最後に私が実装してみましょう。
next
メソッドを追加してみる
せっかく自分で実装するのなら設計も少し弄ってみたくなっちゃいますよね。
あなたもWeatherData
クラスの実装を読んで「registerObserver
メソッドもremoveObserver
メソッドもnotifyObservers
メソッドもクラスごとに毎回書く必要ある?」と思ったかもしれません。この部分はおそらく変化することはなさそうなのでSubject
クラスを新しく作成してWeatherData
に継承しても良さそうですね。
ちょっと待ってください!WeatherData
が他のスーパークラスから拡張して作られていたら困ってしまいますよね?ここではStrategyパターンを使って実装してみましょう。(私自身で実装したのでおそらくベストな設計ではない。)
手元で動くコード
ディレクトリ構成は以下のようになっています。
root
├ dist/(コンパイルしたjsファイル)
├ src/
│ ├ lib.d.ts
│ ├ weatherData.ts
│ ├ display.ts
│ ├ subjectClass.ts
│ ├ parentWeatherData.ts
│ └ index.ts
├ tsconfig.json
└ package.json
まずはインターフェースから見ていきましょう。Subject
インターフェースにsetValue
メソッドをObserver
インターフェースにnext
メソッドを追加しています。
ジェネリック型を使っていることとsetValue
メソッドがnotifyObservers
メソッドを呼び出さない以外は先ほどのWeatherData
クラスと大差ありませんね。
export type Weather = {
temperature:number
humidity:number
pressure:number
}
export interface Observer<T>{
update(value:T):void
next(value:T):void
}
export interface Subject<T>{
registerObserver(o:Observer):void
removeObserver(o:Observer):void
setValue(value:T):void
notifyObservers():void
}
次にsujectClass.ts
を見てみましょう!
import { Observer, Subject } from "./lib";
export class SubjectClass<T> implements Subject<T>{
public value:T|undefined
public observers:Observer<T>[] = []
registerObserver(o:Observer<T>){
this.observers.push(o)
}
removeObserver(o:Observer<T>){
let i = this.observers.indexOf(o);
if(i >=0){
this.observers.splice(i,1)
}
}
notifyObservers(){
for(let i = 0;i<this.observers.length;i++){
let obs = this.observers[i]
if(this.value !== undefined){
obs.update(this.value)
}
}
}
setValue(value:T){
this.value = value
}
}
では、次にWeatherData
クラスがどうなっているか見てみましょう。
実際にParentWeatherData
クラスからの継承がうまく動いていることがわかります。
しかし、状態を全てsubject
プロパティが管理していますね。ここが私が気に入っていない部分です。SubjectClass
クラスの再利用可能性を最大化しつつ状態はWeatherData
に持たせたいのですが良い案はないでしょうか?(そもそもそうすべきなのでしょうか?今回の場合では特に困ることはないのですが...)
import { Observer, Subject, Weather } from "./lib";
import { ParentWeatherData } from "./parentWeatherData";
import { SubjectClass } from "./subjectClass";
export class WeatherData extends ParentWeatherData implements Subject<Weather>{
subject:Subject<Weather>
constructor(){
super()
this.subject = new SubjectClass<Weather>()
}
registerObserver(o:Observer<Weather>){
this.subject.registerObserver(o)
}
removeObserver(o:Observer<Weather>){
this.subject.removeObserver(o)
}
notifyObservers(){
this.subject.notifyObservers()
}
setValue(weather:Weather){
this.subject.setValue(weather)
this.notifyObservers()
}
}
継承がうまくいくことを確認するためだけに作ったParentWeatherData
クラスを見てみます。(見なくても良いです)
export class ParentWeatherData{
constructor(){
this.firstAction()
}
firstAction(){
console.log("Parent Weather Data")
}
}
次はCurrentConditionsDisplay
クラスを見てみましょう。
next
メソッドを追加した以外は先ほどとほとんど変わりありません。
import { Observer, Weather ,Subject} from "./lib";
export class CurrentConditonsDisplay implements Observer<Weather>{
weather:Weather|undefined
weatherData:Subject<Weather>
constructor(weatherData:Subject<Weather>){
this.weatherData = weatherData
this.weatherData.registerObserver(this)
}
public update(weather:Weather){
this.weather = weather
this.display()
}
public next(weather:Weather){
this.weatherData.setValue(weather)
}
public display(){
if(this.weather !== undefined){
console.log("Current conditions:"+this.weather["temperature"]
+ "F degrees and " + this.weather["humidity"] + "% humidity and "
+ this.weather["pressure"]+ "Pa")
}
}
}
では、最後に実行ファイルを書けば終わりです!拙いコードにも関わらず最後まで付いてきてくれてありがとうございます。
今回は前回と違って二つのObserver
がいます。display1
とdisplay2
です。
まず、display1
が宣言されてweatherData
のobserveers
(正確にはweatherData
のsubject
のobserveers
)に登録されます。その後、一度weatherData.setValue
で呼び出されたあとに新しくdisplay2
が登録されます。display1.next
によって、display1
とdisplay2
の両方のupdate
メソッドが呼び出されるのがわかります。そのあとの挙動は何となく予想がつくのではないでしょうか。
import { CurrentConditonsDisplay } from "./display";
import { WeatherData } from "./weatherData";
let weatherData = new WeatherData()
let display1 = new CurrentConditonsDisplay(weatherData)
weatherData.setValue({temperature:100,humidity:40,pressure:1015})
let display2 = new CurrentConditonsDisplay(weatherData)
display1.next({temperature:101,humidity:41,pressure:1016})
weatherData.removeObserver(display2)
weatherData.setValue({temperature:102,humidity:42,pressure:1017})
出力は以下のようになります。
Parent Weather Data
Current conditions:100F degrees and 40% humidity and 1015Pa
Current conditions:101F degrees and 41% humidity and 1016Pa
Current conditions:101F degrees and 41% humidity and 1016Pa
Current conditions:102F degrees and 42% humidity and 1017Pa
期待した挙動の通りになっていますね。
2度目のまとめ
やはり、自分で実装するとなかなか綺麗に書けないなと感じて嫌になってしまいます。
RxJSのコードを読んで勉強したい気持ちになりました。
引き続きデザインパターンについて勉強してもっと綺麗にコードを書けるようになりたいです。
次はDecoretorパターンについて書く予定です。