はかせのラボ

私の頭の中を書いていく雑記ブログです

C++ タスクシステム⑧ ~オブザーバーパターン参戦!~

あいさつ

どうも、はかせです。
前回までデリゲート+コルーチンでタスクを作ろうとしてました。
ただ要は処理を登録して任意のタイミングで発火できれば良いのでは?
という考えに至りました。

「それなら馴染みのあるやつがあるぞ!
オブザーバーパターンだ!」

オブザーバーパターン

実装の前にオブザーバーパターンって何よ?ってところから始まります。
いつも通り簡単に言うと変化を通知し処理を走らせることです。

オブザーバーパターンには大きく分けて2種類の登場人物がいます。
通知するものと通知を受けるものです。

通知を受けるものはObserverと呼ばれることが多いです。
Observerとは観測者という意味でこいつは変化の通知を受けて
応じた処理を行います。

通知するものはSubjectと呼ばれることが多いです。
こいつはObserverのリストを保持していて
自身に変化があった際に保持しているObserverに通知を送ります。

オブザーバーパターンの強みは
①SubjectとObserverが1:多の関係になることが出来る
②関係が単純なので変更が容易で変更に強くなりやすい

この2つだと思っています。

例えば、こんな関係のものがあったとして
f:id:hakase0274:20181226225943p:plain
こんな風に
f:id:hakase0274:20181226230328p:plain
別の処理を追加できます。

また、この追加によって最初に登録していた
「通知された値を表示する」処理に影響を与えていません。

実装

考え自体は説明したので早速コードから行きます。

namespace MyObservarPattern 
{
	/*
	T型を引数とする戻り値無しの関数を登録できるクラス
	*/
	template <class T>
	class Subject
	{
	public:
		Subject() {};
		//受け取った値を登録されている関数に渡して実行
		virtual void OnNext(T value);
		//正常終了処理
		virtual void OnCompleted();
		//異常終了処理
		virtual void OnError(std::exception& e);
		/*
		登録処理
		実行関数登録
		正常終了処理登録
		異常終了処理登録
		*/
		virtual void Subscribe(std::function<void(T value)> next);
		virtual void Subscribe(std::function<void(T value)> next, std::function<void()> completed);
		virtual void Subscribe(std::function<void(T value)> next, std::function<void()> completed, std::function<void()> error);
	private:
		//実行関数リスト
		std::list<std::function<void(T value)>> mOnNextFunctionList;
		//正常終了処理リスト
		std::list<std::function<void()>> mOnCompletedFunctionList;
		//異常終了処理リスト
		std::list<std::function<void()>> mOnErrorFunctionList;
		//デストラクタをprivateにして外部から破棄できなくする
		~Subject()
		{
			mOnNextFunctionList.clear();
			mOnCompletedFunctionList.clear();
			mOnErrorFunctionList.clear();
			std::cout << "Delete This!" << std::endl;
		};
	};
	template<class T>
	inline void Subject<T>::OnNext(T value)
	{
		for (auto function : mOnNextFunctionList)
		{
			function(value);
		}
	}
	template<class T>
	inline void Subject<T>::OnCompleted()
	{
		for(auto function:mOnCompletedFunctionList)
		{
			function();
		}
		delete this;
	}
	template<class T>
	inline void Subject<T>::OnError(std::exception & e)
	{
		for (auto function : mOnErrorFunctionList)
		{
			function();
		}
		delete this;
	}
	template<class T>
	inline void Subject<T>::Subscribe(std::function<void(T value)> next)
	{
		mOnNextFunctionList.push_back(next);
	}
	template<class T>
	inline void Subject<T>::Subscribe(std::function<void(T value)> next, std::function<void()> completed)
	{
		mOnNextFunctionList.push_back(next);
		mOnCompletedFunctionList.push_back(completed);
	}
	template<class T>
	inline void Subject<T>::Subscribe(std::function<void(T value)> next, std::function<void()> completed, std::function<void()> error)
	{
		mOnNextFunctionList.push_back(next);
		mOnCompletedFunctionList.push_back(completed);
		mOnErrorFunctionList.push_back(error);
	}
}

このコードではObserverはそのまま処理を登録しています。
本当はObserverクラスを作り登録すべきなんでしょうが、
わざわざそこにワンクッション挟むより直接処理を渡した方が
私は使いやすいのでそうしています。

また関数はラムダ式や関数ポインタで渡すので戻り値無しで定義しています。
何か値を操作したいときはラムダ式で内部変数をキャプチャするなりして使います。

値の通知

OnNext、OnCompleted、OnErrorという3つメソッドを使って行います。

OnNextは自身の値の変化を通知します。
Subjectが内部的に通知する形もありますが、
今回は外部からSubjectを変更しその値を通知しています。

OnCompletedはSubjectの正常終了を通知します。
OnCompletedが発行されたらOnCompleted時に実行するよう登録された
処理を実行し自身のインスタンスを破棄します。

OnErrorはSubjectのエラーを通知します。
OnErrorが発行されたらOnError時に実行するよう登録された
処理を実行し自身のインスタンスを破棄します。

使い方

int main(int argc, char const* argv[])
{
#if _DEBUG
	_CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF | _CRTDBG_ALLOC_MEM_DF);
#endif
	auto num = 0;
	//std::string型を引数とする関数を登録できる
	auto stringSub = new Subject<std::string>();
	//int型を引数とする関数を登録できる
	auto intSub = new Subject<int>();
	getchar();
	//std::string型を引数とする関数を登録①
	stringSub->Subscribe
	(
		[&](std::string msg) -> void { std::cout << "OnNext1!" << msg << std::endl; },
		[&]() -> void {std::cout << "OnCompleted1!" << std::endl; },
		[&]() -> void {std::cout << "OnError1!" << std::endl; }
	);
	//std::string型を引数とする関数を登録②
	stringSub->Subscribe([&](std::string msg) -> void {std::cout << "SubScribe2!" << msg << std::endl; });
	//int型を引数とする関数を登録
	intSub->Subscribe
	(
		[&](int value)->void { num += value; std::cout << "Add:" << value << std::endl; },
		[&]() -> void {std::cout << "Value:" << num << std::endl; },
		[&]() -> void {std::cout << "(エラー処理なんだけどテストだから許してくださいorz)Value:" << num << std::endl; }
	);
	//登録したstd::string型を引数とする関数に引数として"OnNext!"を渡して実行
	stringSub->OnNext("OnNext!");
	//登録した関数を全て解除し破棄
	stringSub->OnCompleted();
	//登録したint型を引数とする関数に引数として5を渡して実行
	intSub->OnNext(5);
	//登録したint型を引数とする関数に引数として10を渡して実行
	intSub->OnNext(10);
	//適当にエラーを作る
	auto error = std::runtime_error("");
	//エラー処理
	intSub->OnError(error);
	
	getchar();
}

実行結果です。
f:id:hakase0274:20181226231307p:plain

あとがき

今回はオブザーバーパターン実装でした。
今回実装したSubjectは受動的で外部から働きかけて動かしました。
この形は例えば敵を撃破した際に撃破したという通知を発行させて
処理を動かしたりするのに使えます。

他にも能動的に動くSubjectもあります。
例は今回使った図にあるようなものです。
これはタイマーであったり、
今まで頑張って作ろうとしていた処理を行いその報告(通知)を行う
といった処理が出来ます。

次回はこの能動的に動くSubjectを作ってみようと思います。

それでは今回はこの辺でノシ