デザインパターンの一つであるObserverパターンについて学習したので、フロントエンド目線での所感をまとめてみます。
Observerパターンとは
観察する側(Observer)、観察される側(Subject)に処理を分割し、Subjectの状態が変化した際にObserverに通知し、通知毎に定義した処理を実行するパターンです。フロントエンド界隈においては、JavaScriptのaddEventListenerとdispatchEvent、jQueryのonとtriggerをイメージするとわかりやすいかもしれません。
どういった時に使うと良さそうか
Observerパターンを使うことによってObserverとSubject間の処理が疎結合になります。
Subjectは状態によってObserverに通知することが主な目的となり、Observerがどのような処理をしているのか関心がありません。
またObserverはSubjectが通知した際に必要な処理を実行することが主な目的となり、Subjectの状態がいつどのように変化するのか関心がありません。お互いの関心が薄くなり疎結合になります。
フロントエンドではユーザー操作、API通信結果など状態によってUIを変更するといったことが多々あるかと思いますが、状態管理とUI変更の処理を分離したいとなった場合にObserverのパターンが上手くハマりそうです。
(状態管理 → Subject, UI変更 → Observer)
SubjectとObserverの関係は1対多の関係であり、Observerは後からいくらでも追加することができます。
状態が変化した際に更新するUIが膨大に存在している、今後どんどん増えることが予想されるといった状況であればObserverパターンを導入することによって、拡張性に優れた保守しやすいコードを記載することが可能だと思います。
また副産物として状態管理とUI変更を分離することにより、テストコードを記載しやすくなるメリットもあるかと思いました。
Jestなどテスティングフレームワークを使用し、フロントエンドのコードをユニットテストする際、UI変更のユニットテストをするには労力がかかりますが、DOM操作を必要としない状態管理のみのテストは比較的に労力がかかりません。
Observerパターンを導入することにより、テストしやすい箇所・しにくい箇所を分離し、しやすい箇所に対して進んでテストコードを記載していくといったことが可能です。
具体的には下記のようなUIを作る際にObserverのパターンを導入すると良さそうです。
- ファイルダウンロードとインジケータ
- ダウンロードの状態(Subject)によってインジケータ(Observer)を更新する
- API通信とエラーメッセージ表示
- API通信と状態(Subject)によってエラーメッセージ(Observer)を更新する
サンプルコード
API通信、UI変更を1ファイルで記載したものをObserverパターンに合わせて分割してみたいと思います。長くなるためUI変更の詳細なコードは割愛します。
1ファイルにまとめたコードのサンプル
const fetchProductList = async () => {
try {
const response = await axios.get("https://***");
if (response.status === 200) {
// 商品情報リストを生成しHTMLに挿入する
console.log("success");
return;
}
} catch (e) {
if (e.response.status === 403) {
// ステータスコード403時のエラーメッセージの表示
console.log("error 403");
return;
}
if (e.response.status === 500) {
// ステータスコード500時のエラーメッセージの表示
console.log("error 500");
return;
}
}
};
fetchProductList();
SubjectとObserverのインターフェースを作成しimplementsする形で実装していきます。
Observerのインスタンスを生成後、Subjectの addObserver
メソッドを呼び出すことによってObserverの参照を保持しておき、必要に応じて notify
メソッドを呼び出し全Observerに通知します。
type EventType = "success" | "error"
type NotifyParameter = {
eventType: EventType,
statusCode: number,
data?: [string: any]
}
interface Subject {
addObserver: (observer: Observer) => void;
notify: (params: NotifyParameter) => void;
}
interface Observer {
update: (params: NotifyParameter) => void;
}
実装したSubjectは下記になります。
通信が成功・失敗した際に notify
メソッドを呼び出していますが、成功・失敗後の具体的な処理は記載していません。
class ProductListSubject implements Subject {
private observers: Observer[] = []
public addObserver(observer: Observer) {
this.observers.push(observer);
}
public notify(params: NotifyParameter) {
this.observers.forEach(observer => {
observer.update(params);
});
}
public async fetch() {
try {
const response = await axios.get("https://***");
this.notify({
eventType: "success",
statusCode: response.status,
data: response.data
})
} catch(e) {
this.notify({
eventType: "error",
statusCode: e.response.status
});
}
}
}
実装したObserverは下記になります。
Observerはいつ実行されるのかの関心はありません。update
メソッドを実行した際に必要な処理を記載しているのみです。
class ProductListUpdateObserver implements Observer {
public update(params: NotifyParameter) {
if (params.statusCode === 200) {
console.log("success! response data: ", params.data);
// 商品情報リストを生成しHTMLに挿入する
}
}
}
class ErrorMessageObserver implements Observer {
public update(params: NotifyParameter) {
if (params.statusCode === 500) {
console.log("error! statusCode: ", params.statusCode);
// ステータスコード500時のエラーメッセージの表示
}
if (params.statusCode === 403) {
console.log("error! statusCode: ", params.statusCode);
// ステータスコード403時のエラーメッセージの表示
}
}
}
Subjectのインスタンスを生成後、Observerを登録しSubjectの fetch
メソッドを呼び出します。
API通信後、成功・失敗に応じて notify
メソッドが呼び出され、Observerの update
メソッドが呼び出されます。
const execute = () => {
const subject = new ProductListSubject();
subject.addObserver(new ProductListUpdateObserver());
subject.addObserver(new ErrorMessageObserver());
subject.fetch();
}
execute();
API通信後にUI変更の処理を増やしたいとなった際はObserserを新しく作成し addObserver
を呼び出すだけです。最終的なコード量は分割前より多くなっていますが、拡張性に優れている & 保守しやすいコードになっていると思います。
コードを書いてから思いましたが、通知処理にEventEmitterを使うとなお良いかもしれません。
EventEmitterを使用する場合、任意のイベントを複数emitできるため、statusCode毎に別々のイベントをemitすればObserver内のstatusCodeごとの分岐を削除できそうです。
おわり