イベントの発生を Observable により Subscriber に通知する
オブザーバパターン
オブザーバパターン (observer pattern) により、あるオブジェクト Observer を別のオブジェクト Observable に Subscribe することができます。イベントが発生すると、Observable は自身の Observer に通知します!
Observable オブジェクトは、通常 3 つの重要なパーツから構成されます:
-
Observers
: 特定のイベントが発生するたびに通知を受ける Observer の配列 -
subscribe()
: Observer を Observer のリストに追加するためのメソッド -
unsubscribe()
: Observer のリストから Observer を削除するメソッド -
notify()
: 特定のイベントが発生したときに、すべての Observer に通知するメソッド
それでは、Observable を作っていきましょう。簡単な方法としては ES6 のクラスを使うものがあります。
class Observable {
constructor() {
this.observers = [];
}
subscribe(func) {
this.observers.push(func);
}
unsubscribe(func) {
this.observers = this.observers.filter(observer => observer !== func);
}
notify(data) {
this.observers.forEach(observer => observer(data));
}
}
いい感じです!これで、subscribe メソッドにより Observer をリストに追加し、unsubscribe メソッドにより Observer を削除し、notify メソッドによりすべての Subscriber に通知できるようになりました。
この Observable を使って何か作ってみましょう。ここでは、Button
と Switch
という 2 つのコンポーネントからなる、非常に簡単なアプリケーションを考えます。
export default function App() {
return (
<div className="App">
<Button>Click me!</Button>
<FormControlLabel control={<Switch />} />
</div>
);
}
このアプリケーションとユーザーとのやり取りを記録していきます。ユーザーがボタンをクリックするか、スイッチを切り替えるたびに、タイムスタンプと一緒にイベントをログ出力したいと思います。ログを出力するだけでなく、イベントが発生したときに表示されるトーストによる通知も作成したいと思います。
本質的には、私たちがやりたいことは以下のようになります:
ユーザーが handleClick
関数または handleToggle
関数を呼び出すたびに、これらの関数は Observable の notify
メソッドを呼び出します。notify
メソッドは、handleClick
関数または handleToggle
関数によって渡されたデータをすべての Subscriber に通知します。
まず、logger
関数と toastify
関数を作成しましょう。これらの関数は、最終的に notify
メソッドから data
を受け取ります。
import { ToastContainer, toast } from "react-toastify";
function logger(data) {
console.log(`${Date.now()} ${data}`);
}
function toastify(data) {
toast(data);
}
export default function App() {
return (
<div className="App">
<Button>Click me!</Button>
<FormControlLabel control={<Switch />} />
<ToastContainer />
</div>
);
}
現在、logger
関数と toastify
関数は Observable を認識していません。つまり、Observable はまだこれらの関数に通知することができないということです。これらの関数を Observer とするためには、Observable の subscribe
メソッドを使って登録しなければなりません。
import { ToastContainer, toast } from "react-toastify";
function logger(data) {
console.log(`${Date.now()} ${data}`);
}
function toastify(data) {
toast(data);
}
observable.subscribe(logger);
observable.subscribe(toastify);
export default function App() {
return (
<div className="App">
<Button>Click me!</Button>
<FormControlLabel control={<Switch />} />
<ToastContainer />
</div>
);
}
イベントが発生するたびに、logger
関数と toastify
関数が通知を受けるようになりました。あとは、実際に Observable に通知する関数、すなわち handleClick
関数と handleToggle
関数を実装するだけです!これらの関数は、Observable の notify
メソッドを呼び出し、Observer が受け取るデータを渡す必要があります。
import { ToastContainer, toast } from "react-toastify";
function logger(data) {
console.log(`${Date.now()} ${data}`);
}
function toastify(data) {
toast(data);
}
observable.subscribe(logger);
observable.subscribe(toastify);
export default function App() {
function handleClick() {
observable.notify("User clicked button!");
}
function handleToggle() {
observable.notify("User toggled switch!");
}
return (
<div className="App">
<Button>Click me!</Button>
<FormControlLabel control={<Switch />} />
<ToastContainer />
</div>
);
}
これですべての準備が整いました。handleClick
と handleToggle
はデータと共に Observable の notify
メソッドを呼び出し、その後 Observable は Subscriber (この場合は logger
関数と toastify
関数) に通知します。
ユーザーがいずれかのコンポーネントを操作するたびに、logger
と toastify
関数の両方が notify
メソッドに渡したデータとともに通知を受けます!
class Observable {
constructor() {
this.observers = [];
}
subscribe(f) {
this.observers.push(f);
}
unsubscribe(f) {
this.observers = this.observers.filter(subscriber => subscriber !== f);
}
notify(data) {
this.observers.forEach(observer => observer(data));
}
}
export default new Observable();
import React from "react";
import { Button, Switch, FormControlLabel } from "@material-ui/core";
import { ToastContainer, toast } from "react-toastify";
import observable from "./Observable";
function handleClick() {
observable.notify("User clicked button!");
}
function handleToggle() {
observable.notify("User toggled switch!");
}
function logger(data) {
console.log(`${Date.now()} ${data}`);
}
function toastify(data) {
toast(data, {
position: toast.POSITION.BOTTOM_RIGHT,
closeButton: false,
autoClose: 2000
});
}
observable.subscribe(logger);
observable.subscribe(toastify);
export default function App() {
return (
<div className="App">
<Button variant="contained" onClick={handleClick}>
Click me!
</Button>
<FormControlLabel
control={<Switch name="" onChange={handleToggle} />}
label="Toggle me!"
/>
<ToastContainer />
</div>
);
}
オブザーバパターンにはさまざまな使用方法がありますが、非同期のイベントベースのデータを扱うときに非常に便利です。たとえば、あるデータのダウンロードが終了したときに特定のコンポーネントに通知したい場合や、ユーザーが掲示板に新しいメッセージを送ったときに他のメンバー全員に通知したい場合などが考えられます。
ケーススタディ
オブザーバパターンを使用する人気のライブラリに RxJS があります。
ReactiveX は、オブザーバパターンとイテレータパターンとを、そして、コレクションと関数型プログラミングとを組み合わせ、イベントのシーケンスを管理する理想的な方法に対するニーズを満たします。- RxJS
RxJS により、Observable を作成して、特定のイベントに Subscribe することができるのです!RxJS のドキュメントにある、ユーザーがドラッグしているかどうかのログを取る例を見てみましょう。
import React from "react";
import ReactDOM from "react-dom";
import { fromEvent, merge } from "rxjs";
import { sample, mapTo } from "rxjs/operators";
import "./styles.css";
merge(
fromEvent(document, "mousedown").pipe(mapTo(false)),
fromEvent(document, "mousemove").pipe(mapTo(true))
)
.pipe(sample(fromEvent(document, "mouseup")))
.subscribe(isDragging => {
console.log("Were you dragging?", isDragging);
});
ReactDOM.render(
<div className="App">Click or drag anywhere and check the console!</div>,
document.getElementById("root")
);
RxJS には、オブザーバパターンで動作する組み込みの機能や例が山ほどあります。
Pros
オブザーバパターンは、関心の分離と単一責任の原則を実現するための素晴らしい方法です。Observer オブジェクトは Observable オブジェクトと密結合しておらず、いつでも結合 (あるいは疎結合化) することができます。Observable オブジェクトはイベントの監視に責任をもつのに対し、Observer は受け取ったデータを処理するだけとなります。
Cons
Observer が複雑になりすぎると、すべての Subscriber に通知する際にパフォーマンスの問題が発生する可能性があります。