
代表的なデザインパターンの一つである"Observer(オブザーバ)"は、
あるオブジェクト(=Subject/被観察者)の状態が変化したときに、
それを監視している複数のObserver(監視者)へ自動的に通知する仕組みです。
役割(登場人物)
| 役割 |
説明 |
| Subject(被観察者) |
状態を持ち、Observerの登録・削除・通知を行う。 |
| Observer(監視者) |
状態変化を受け取り、何らかの処理を実行する。 |
ソースコード
Subject
class Subject {
constructor() {
this.observers = [];
}
// 監視者を登録
addObserver(observer) {
this.observers.push(observer);
}
// 監視者を削除
removeObserver(observer) {
this.observers = this.observers.filter(o => o !== observer);
}
// 全監視者に通知
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
Observer
class Observer {
constructor(name) {
this.name = name;
}
update(data) {
console.log(`${this.name}が通知を受け取りました:`, data);
}
}
実行例
const news = new Subject();
const alice = new Observer("Alice");
const bob = new Observer("Bob");
news.addObserver(alice);
news.addObserver(bob);
news.notify("新しいニュースが届きました!");
// → Aliceが通知を受け取りました:新しいニュースが届きました!
// → Bobが通知を受け取りました:新しいニュースが届きました!
news.removeObserver(bob);
news.notify("続報:イベントが開催されます!");
// → Aliceが通知を受け取りました:続報:イベントが開催されます!
ポイント開設
Subjectが状態の中心にセットされます。
ObserverはSubjectに登録(購読)しておき、Subjectの変化時にnotify()で通知される。
Subjectの中に、updateメソッドを書きたいかもしれませんが、Observerと分けることで、拡張性が高く保てます。
メリット
・状態の変更を疎結合で伝達できる。
・Observerの数を増減してもSubject側のコードを変更しなくて済む。
・イベント駆動やリアクティブUIの基礎になる。
デメリット
・Observerの数が増えると通知処理が重くなる。
・通知のタイミングが複雑になるとデバッグが難しい。
・無限ループ(Observerが再度Subjectを更新するケース)に注意が必要。
実際の応用例
・DOMイベントリスナー(addEventListener())
・RxJSやVueのリアクティブシステム
・WebSocket通知やFirebase Realtime Databaseの購読
あとがき
Javascriptは、ネイティブ関数でもobserver機能を持っているので、DOM構造が非同期に変化したタイミングをイベントキャッチ(observerキャッチ)することができて、便利に使えます。
でも、使いすぎると、Webサイト事態の処理負荷が高くなり、ページが重くなる可能性もあるので、全体設計をしっかりと見据える必要があるのが注意ポイントですね。
他にも、アプリ内のバッジ処理や、リアルタイムチャット機能などでの、ユーザーの状態変更などの監視にも、このオブザーバ・パターンを使うと効率的にシステム設計ができますね。
おまけ1 : リアルタイム風ニュース通知 (HTML)
以下は Observerパターン をブラウザで体験できる簡単なデモです。
サブスクライブ(購読)すると通知欄にリアルタイムでニュースが届き、解除も可能です。
ローカルに保存してブラウザで開くだけで動きます(外部ネット不要)。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>Observerパターン - リアルタイムニュースデモ</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; max-width:900px; }
.panel { border:1px solid #ddd; padding:12px; margin-bottom:12px; border-radius:6px; }
button { margin-right:6px; padding:6px 10px; }
#subscribers { display:flex; gap:8px; flex-wrap:wrap; }
.subscriber { padding:6px 8px; border-radius:4px; border:1px solid #ccc; background:#f9f9f9; }
#newsList { max-height:200px; overflow:auto; background:#fff; padding:8px; border:1px solid #eee; }
.news-item { padding:6px; border-bottom:1px dashed #eee; }
.badge { font-size:12px; color:#666; margin-left:6px; }
</style>
</head>
<body>
<h1>Observerパターン:リアルタイムニュース通知デモ</h1>
<div class="panel">
<strong>操作</strong>
<div style="margin-top:8px;">
<button id="subBtn">Subscribe (購読)</button>
<button id="unsubBtn">Unsubscribe (解除)</button>
<button id="clearBtn">Clear Notifications</button>
<span class="badge">自動で3〜8秒ごとにニュースが発行されます</span>
</div>
</div>
<div class="panel">
<strong>購読者</strong>
<div id="subscribers"></div>
</div>
<div class="panel">
<strong>受信トレイ(最新のニュース)</strong>
<div id="newsList"></div>
</div>
<script>
/* === Subject(ニュース配信者) === */
class NewsPublisher {
constructor() {
this.observers = [];
this.id = 0;
}
subscribe(observer) {
this.observers.push(observer);
this._logSystem(`[System] ${observer.name} が購読しました`);
}
unsubscribe(observer) {
this.observers = this.observers.filter(o => o !== observer);
this._logSystem(`[System] ${observer.name} が購読を解除しました`);
}
notify(news) {
this.observers.forEach(o => o.update(news));
}
publish(title, content) {
const item = { id: ++this.id, title, content, time: new Date().toLocaleTimeString() };
this._logSystem(`[Publish] ${title}`);
this.notify(item);
}
_logSystem(msg) {
const el = document.createElement('div');
el.className = 'news-item';
el.innerHTML = `<em style="color:#999">${new Date().toLocaleTimeString()}</em> — ${msg}`;
document.getElementById('newsList').prepend(el);
}
}
/* === Observer(購読者) === */
class Subscriber {
constructor(name) {
this.name = name;
}
update(news) {
const el = document.createElement('div');
el.className = 'news-item';
el.innerHTML = `<strong>${news.title}</strong> <small style="color:#666">(${news.time})</small><div style="margin-top:4px">${news.content}</div><div style="font-size:12px;color:#999;margin-top:6px">受信者: ${this.name}</div>`;
document.getElementById('newsList').prepend(el);
}
}
/* === UI / Wire-up === */
const publisher = new NewsPublisher();
const subscribersEl = document.getElementById('subscribers');
const newsList = document.getElementById('newsList');
let currentSubscriber = null;
let subscriberCount = 0;
function renderSubscribers() {
subscribersEl.innerHTML = '';
publisher.observers.forEach((s, idx) => {
const div = document.createElement('div');
div.className = 'subscriber';
div.textContent = `${s.name}`;
subscribersEl.appendChild(div);
});
}
/* Subscribe ボタン */
document.getElementById('subBtn').addEventListener('click', () => {
subscriberCount++;
const name = `User${subscriberCount}`;
const sub = new Subscriber(name);
publisher.subscribe(sub);
renderSubscribers();
});
/* Unsubscribe ボタン(最後に登録した人を解除) */
document.getElementById('unsubBtn').addEventListener('click', () => {
const list = publisher.observers;
if (list.length === 0) {
publisher._logSystem('[System] 購読者がいません');
return;
}
const sub = list[list.length - 1];
publisher.unsubscribe(sub);
renderSubscribers();
});
/* Clear Notifications */
document.getElementById('clearBtn').addEventListener('click', () => {
newsList.innerHTML = '';
});
/* === 自動ニュース生成(3〜8秒ごと) === */
const sampleTitles = [
"速報: サーバー負荷に変化",
"お知らせ: メンテナンス予定",
"新着: リリースノート公開",
"注意: セキュリティ警告",
"更新: ドキュメントが更新されました"
];
const sampleBodies = [
"詳細は管理コンソールを確認してください。",
"明日 02:00 〜 04:00 にメンテナンスを予定しています。",
"本日のデプロイにより新機能が追加されました。",
"不審なアクセスが検出されました。パスワード変更を推奨します。",
"APIの使い方の一部が改善されました。サンプルコードを更新してください。"
];
function randomInt(min, max){ return Math.floor(Math.random() * (max - min + 1)) + min; }
(function schedulePublish(){
const t = randomInt(3000, 8000);
setTimeout(() => {
const i = randomInt(0, sampleTitles.length - 1);
publisher.publish(sampleTitles[i], sampleBodies[i]);
schedulePublish();
}, t);
})();
</script>
</body>
</html>
おまけ2 : 言語別Observer
いくつかのプログラム言語での、Obserberパターンを書いてみました。
Python
class Subject:
def __init__(self):
self.observers = []
def subscribe(self, observer):
self.observers.append(observer)
def notify(self, data):
for obs in self.observers:
obs.update(data)
class NewsReader:
def __init__(self, name):
self.name = name
def update(self, news):
print(f"{self.name}がニュースを受信: {news}")
news_agency = Subject()
news_agency.subscribe(NewsReader("Alice"))
news_agency.subscribe(NewsReader("Bob"))
news_agency.notify("Python界の最新ニュース!")
Java
import java.util.*;
interface Observer {
void update(String news);
}
class NewsReader implements Observer {
private String name;
NewsReader(String name) { this.name = name; }
public void update(String news) {
System.out.println(name + "がニュースを受信: " + news);
}
}
class NewsAgency {
private List<Observer> observers = new ArrayList<>();
public void subscribe(Observer o) { observers.add(o); }
public void notifyAll(String news) {
for (Observer o : observers) o.update(news);
}
}
public class Main {
public static void main(String[] args) {
NewsAgency agency = new NewsAgency();
agency.subscribe(new NewsReader("Alice"));
agency.subscribe(new NewsReader("Bob"));
agency.notifyAll("Java 21リリース!");
}
}
PHP
name = $name;
}
public function update(string $news): void {
echo "{$this->name} がニュースを受信: {$news}\n";
}
}
// Subject(通知者)
class NewsAgency {
private array $observers = [];
public function subscribe(Observer $observer): void {
$this->observers[] = $observer;
}
public function notifyAll(string $news): void {
foreach ($this->observers as $observer) {
$observer->update($news);
}
}
}
// 実行例
$agency = new NewsAgency();
$agency->subscribe(new NewsReader("Alice"));
$agency->subscribe(new NewsReader("Bob"));
$agency->notifyAll("PHP 8.4 がリリースされました!");
0 件のコメント:
コメントを投稿