開発では「あるイベントが起きたら複数の処理を同時に呼び出す」場面がよくあります。
しかし、がむしゃらに記述を行うと密結合になってしまい、変更に弱いコードが生まれてしまいます。
そこでオブザーバーパターンを使用することで、疎結合を保ちつつ呼び出し処理を実現することができます。
本記事ではC++でのオブザーバーパターンの使用方法について紹介します。
前提知識
-
C++の基礎文法を理解している -
オブジェクト指向について知っている
オブザーバーパターン/ObserverPatternとは
オブザーバーパターン(Observer Pattern)とは、 「あるオブジェクトの状態変化を、依存している複数のオブジェクトに自動的に通知する仕組み」を提供するデザインパターンです。
コードの依存関係を減らせ、拡張性があるというメリットがあります。
その反面、乱用すると処理が重くなったり、優先順位を工夫する必要が出てきます。
オブザーバーパターンの構成と用語
オブザーバーパターンは以下のようなクラスで構成されています。
-
Subject/通知元
- 状態を持ち、変化が起きたらObserverへ通知する
-
インターフェースとして作り、ConcreteSubjectに継承させる
-
Observer(監視者)
- Subjectから通知を受け取り、処理を行う
-
インターフェースとして作り、ConcreteObserverに継承させる
-
ConcreteSubject(具体的通知元)
-
Subjectを実装したクラス - 状態を管理し、通知を実際に行う
-
-
ConcreteObserver(具体的監視者)
-
Observerを実装したクラス - 通知に応じて具体的な処理を行う
-
構造の図解
コード例
プレイヤーがダメージを受けたことを通知するコード
//----------------------------------------------------------------------------
//! @file ISubject.h
//! @brief Subjectインターフェースの定義
//! @author つきの
//----------------------------------------------------------------------------
#pragma once
class IObserver;
#include <string>
//----------------------------------------------------------------------------
//! @class ISubject
//! @brief Subjectインターフェース
//! @note ObserverパターンにおけるSubjectのインターフェース定義
//----------------------------------------------------------------------------
class ISubject {
public:
//----------------------------------------------------------------------------
//! @brief デストラクタ
//----------------------------------------------------------------------------
virtual ~ISubject() = default;
//----------------------------------------------------------------------------
//! @brief Observerを追加する関数
//! @param observer [in] 追加するObserver
//----------------------------------------------------------------------------
virtual void addObserver(IObserver* observer) = 0;
//----------------------------------------------------------------------------
//! @brief Observerを削除する関数
//! @param observer [in] 削除するObserver
//----------------------------------------------------------------------------
virtual void removeObserver(IObserver* observer) = 0;
//----------------------------------------------------------------------------
//! @brief 登録されているObserverに通知を送る関数
//! @param event [in] イベント名
//! @param value [in] イベントに関連する値
//----------------------------------------------------------------------------
virtual void notify(const std::string& event, int value) = 0;
};
//----------------------------------------------------------------------------
//! @file IObserver.h
//! @brief Observerインターフェースの定義
//! @author つきの
//----------------------------------------------------------------------------
#pragma once
#include <string>
//----------------------------------------------------------------------------
//! @class IObserver
//! @brief Observerインターフェース
//! @note ObserverパターンにおけるObserverのインターフェース定義
//----------------------------------------------------------------------------
class IObserver {
public:
//----------------------------------------------------------------------------
//! @brief デストラクタ
//----------------------------------------------------------------------------
virtual ~IObserver() = default;
//----------------------------------------------------------------------------
//! @brief 通知を受け取る処理
//! @param event [in] イベント名
//! @param value [in] イベントに関連する値
//----------------------------------------------------------------------------
virtual void onNotify(const std::string& event, int value) = 0;
};
//----------------------------------------------------------------------------
//! @file Player.h
//! @brief Playerクラスの定義
//! @author つきの
//----------------------------------------------------------------------------
#pragma once
#include "ISubject.h"
#include <vector>
//----------------------------------------------------------------------------
//! @class Player
//! @brief プレイヤークラス
//! @note ObserverパターンにおけるConcreteSubjectの役割
//----------------------------------------------------------------------------
class Player : public ISubject {
private:
int hp; // !< プレイヤーのHP
std::vector<IObserver*> observers_; // !< 登録されているObserverのリスト
public:
//----------------------------------------------------------------------------
// コンストラクタ
//! @param hp [in] 初期HP
//----------------------------------------------------------------------------
Player(int hp);
//----------------------------------------------------------------------------
// Observerリストに追加を行う関数
//! @param observer [in] 追加/削除するObserver
//----------------------------------------------------------------------------
void addObserver(IObserver* observer) override;
//----------------------------------------------------------------------------
// Observerリストから削除を行う関数
//! @param observer [in] 追加/削除するObserver
//----------------------------------------------------------------------------
void removeObserver(IObserver* observer) override;
//----------------------------------------------------------------------------
// 登録されているObserverに通知を送る関数
//! @param event [in] イベント名
//! @param value [in] イベントに関連する値
//----------------------------------------------------------------------------
void notify(const std::string& event, int value) override;
//----------------------------------------------------------------------------
// ダメージを受ける関数
//! @param damage [in] 受けるダメージ量
//! @note HPが変化した際にObserverに通知を送る
//----------------------------------------------------------------------------
void takeDamage(int damage);
};
//----------------------------------------------------------------------------
//! @file Player.cpp
//! @brief Playerクラスの実装
//! @author つきの
//----------------------------------------------------------------------------
#include "Player.h"
#include "IObserver.h"
//----------------------------------------------------------------------------
//! @brief コンストラクタ
//----------------------------------------------------------------------------
Player::Player(int hp)
: hp(hp) {
}
//----------------------------------------------------------------------------
//! @brief Observerリストに追加を行う関数
//----------------------------------------------------------------------------
void Player::addObserver(IObserver* observer) {
observers_.push_back(observer);
}
//----------------------------------------------------------------------------
//! @brief Observerリストから削除を行う関数
//----------------------------------------------------------------------------
void Player::removeObserver(IObserver* observer) {
// 指定されたObserverをリストから削除
observers_.erase(std::remove(observers_.begin(), observers_.end(), observer), observers_.end());
}
//----------------------------------------------------------------------------
//! @brief 登録されているObserverに通知を送る関数
//----------------------------------------------------------------------------
void Player::notify(const std::string& event, int value) {
for (auto* obs : observers_) {
obs->onNotify(event, value); // 各Observerに通知を送る
}
}
//----------------------------------------------------------------------------
//! @brief ダメージを受ける関数
//----------------------------------------------------------------------------
void Player::takeDamage(int damage) {
hp -= damage; // ダメージ分HPを減らす
if (hp < 0) hp = 0; // HPが0未満にならないようにする
notify("HP_CHANGED", hp); // HPが変化したことを通知
}
//----------------------------------------------------------------------------
//! @file Logger.h
//! @brief Loggerクラスの定義
//! @author つきの
//----------------------------------------------------------------------------
#pragma once
#include "IObserver.h"
//----------------------------------------------------------------------------
//! @class Logger
//! @brief ログ出力クラス
//! @note ObserverパターンにおけるConcreteObserverの役割
//----------------------------------------------------------------------------
class Logger : public IObserver {
public:
//----------------------------------------------------------------------------
// 通知を受け取る処理
//! @param event [in] イベント名
//! @param value [in] イベントに関連する値
//----------------------------------------------------------------------------
void onNotify(const std::string& event, int value) override;
};
//----------------------------------------------------------------------------
//! @file Logger.cpp
//! @brief Loggerクラスの実装
//! @author つきの
//----------------------------------------------------------------------------
#include "Logger.h"
#include <iostream>
//----------------------------------------------------------------------------
//! @brief 通知を受け取る処理
//----------------------------------------------------------------------------
void Logger::onNotify(const std::string& event, int value) {
std::cout << "[Log] Event=" << event << ", Value=" << value << std::endl;
}
//----------------------------------------------------------------------------
//! @file SoundSystem.h
//! @brief SoundSystemクラスの定義
//! @author つきの
//----------------------------------------------------------------------------
#pragma once
#include "IObserver.h"
//----------------------------------------------------------------------------
//! @class SoundSystem
//! @brief サウンドシステムクラス
//! @note ObserverパターンにおけるConcreteObserverの役割
//----------------------------------------------------------------------------
class SoundSystem : public IObserver {
public:
//----------------------------------------------------------------------------
// 通知を受け取る処理
//! @param event [in] イベント名
//! @param value [in] イベントに関連する値
//----------------------------------------------------------------------------
void onNotify(const std::string& event, int value) override;
};
//----------------------------------------------------------------------------
//! @file SoundSystem.cpp
//! @brief SoundSystemクラスの実装
//! @author つきの
//----------------------------------------------------------------------------
#include "SoundSystem.h"
#include <iostream>
//----------------------------------------------------------------------------
//! @brief 通知を受け取る処理
//----------------------------------------------------------------------------
void SoundSystem::onNotify(const std::string& event, int value) {
if (event == "HP_CHANGED") {
std::cout << "[Sound] 再生: HP_CHANGED " << value << std::endl;
}
}
//----------------------------------------------------------------------------
//! @file main.cpp
//! @brief オブザーバーパターンのサンプルコード
//! @author つきの
//----------------------------------------------------------------------------
#include "Player.h"
#include "Logger.h"
#include "SoundSystem.h"
#include <memory>
// エントリポイント
int main() {
// Playerオブジェクトの作成
std::unique_ptr<Player> player = std::make_unique<Player>(100);
// Observerオブジェクトの作成
std::unique_ptr<Logger> logger = std::make_unique<Logger>();
std::unique_ptr<SoundSystem> sound_system = std::make_unique<SoundSystem>();
// Observerの登録
player->addObserver(logger.get()); // Loggerを登録
player->addObserver(sound_system.get()); // SoundSystemを登録
// ダメージを受けてみる
player->takeDamage(20);
// 終了
return 0;
}
[Log] Event=HP_CHANGED, Value=80
[Sound] 再生: HP_CHANGED 80
通知を送れたようです。
クラス図
onNotify/イベントの通知を受け取る関数
Observerにイベントの通知を受け取って処理を行う関数を作ることで、イベント処理を可能としています。
IObserverに純粋仮想関数(本記事ではonNotify())を作成しておくことで、派生クラスで同名の関数があることを保証します。
//----------------------------------------------------------------------------
//! @brief 通知を受け取る処理
//! @param event [in] イベント名
//! @param value [in] イベントに関連する値
//----------------------------------------------------------------------------
virtual void onNotify(const std::string& event, int value) = 0;
//----------------------------------------------------------------------------
//! @brief 通知を受け取る処理
//----------------------------------------------------------------------------
void SoundSystem::onNotify(const std::string& event, int value) {
if (event == "HP_CHANGED") {
std::cout << "[Sound] 再生: HP_CHANGED " << value << std::endl;
}
}
//----------------------------------------------------------------------------
//! @brief 通知を受け取る処理
//----------------------------------------------------------------------------
void Logger::onNotify(const std::string& event, int value) {
std::cout << "[Log] Event=" << event << ", Value=" << value << std::endl;
}
addObserver/オブザーバーの追加
Subject/通知元でObserver/監視者のポインタを持つことで、イベントが起こった際に監視者の通知を受け取る関数(本記事ではonNotify())を発火させます。
std::vector<IObserver*> observers_; // !< 登録されているObserverのリスト
//----------------------------------------------------------------------------
//中略
//----------------------------------------------------------------------------
//----------------------------------------------------------------------------
// Observerリストに追加を行う関数
//! @param observer [in] 追加/削除するObserver
//----------------------------------------------------------------------------
void addObserver(IObserver* observer) override;
//----------------------------------------------------------------------------
//! @brief 登録されているObserverに通知を送る関数
//----------------------------------------------------------------------------
void Player::notify(const std::string& event, int value) {
for (auto* obs : observers_) {
obs->onNotify(event, value); // 各Observerに通知を送る
}
}
//----------------------------------------------------------------------------
//! @brief ダメージを受ける関数
//----------------------------------------------------------------------------
void Player::takeDamage(int damage) {
hp -= damage; // ダメージ分HPを減らす
if (hp < 0) hp = 0; // HPが0未満にならないようにする
notify("HP_CHANGED", hp); // HPが変化したことを通知
}
発展形
発展形にはEventBusというものがあります。
興味のある方はぜひ以下の記事ものぞいてみて下さい。
総括
-
Observerパターンを使用することで、クラス間の疎結合を保ちながら拡張性の高い設計が行える -
Subject側で通知を受け取りたいObserverのポインタを管理することで、イベント駆動設計を実現している - 乱用すると処理が重くなるというデメリットがある
- 処理順序をつけたい場合には設計に工夫をする必要がある