LoginSignup
5

More than 3 years have passed since last update.

TypeScriptでデザインパターンを理解する ②Observerパターン

Last updated at Posted at 2020-10-21

前回の記事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

型やインターフェースの定義

lib.d.ts
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の実装

weatherData.ts
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の実装

display.ts
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")
        }
}

実行ファイル

index.ts
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を見ていきましょう。WeatherDisplayElementについては説明はいらないですね。今回の例でたまたま登場するだけだからです。
では、重要な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メソッドが実行されると登録されている全てのObserverupdateメソッドが実行されるという算段なのです。
notifyObserversメソッドの引数は空ですね。そうですObserverupdateメソッドはSubjectの状態を引数として受け取るのです。今回の場合はweatherSubjectのプロパティです。

では、Subjectインターフェースを満たすWeatherDataクラスの実装を見ていきましょう。

Subjectを満たすクラスの実装

WeatherDataクラスは内部状態としてobservers:Observer[]weather:Weatherを持ちます。observersは登録されているObserverのリストです。weatherはその時点での最新の天気です。
先ほど説明した通りregisterObserver(o:Observer)Observerを新しく登録し、removeObserver(o:Observer)で指定したObserverを取り除き、notifyObservers()observersの中に入っている全てのObserverupdateメソッドを呼び出していることがわかりますね。

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:WeatherweatherData: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クラスと大差ありませんね。

lib.d.ts
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を見てみましょう!

subjectClass.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に持たせたいのですが良い案はないでしょうか?(そもそもそうすべきなのでしょうか?今回の場合では特に困ることはないのですが...)

weatherData.ts
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クラスを見てみます。(見なくても良いです)

parentWeatherData.ts
export class ParentWeatherData{

    constructor(){
        this.firstAction()
    }

    firstAction(){
        console.log("Parent Weather Data")
    }
}

次はCurrentConditionsDisplayクラスを見てみましょう。
nextメソッドを追加した以外は先ほどとほとんど変わりありません。

display.ts
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がいます。display1display2です。
まず、display1が宣言されてweatherDataobserveers(正確にはweatherDatasubjectobserveers)に登録されます。その後、一度weatherData.setValueで呼び出されたあとに新しくdisplay2が登録されます。display1.nextによって、display1display2の両方のupdateメソッドが呼び出されるのがわかります。そのあとの挙動は何となく予想がつくのではないでしょうか。

index.ts
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パターンについて書く予定です。

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
5