觀察者模式 (Observer pattern) 3

上一篇我們推導出需要 Observer 抽象基礎類別的原因,本篇我們來探討需要 Subject 抽象基礎類別的原因。

Observer pattern

主題通知觀察者資料更新有兩種方式,一種是直接將資料推 (push) 送給觀察者;另一種是先通知觀察者,等觀察者有時間再回主題拉 (pull) 資料。

如果是將資料推 (push) 送給觀察者,Update 需要修改為傳遞資料參數,如下:

class IObserver
{
public:
	virtual ~IObserver() = 0
	{}

public:
	virtual void Update(double dPrice) = 0;
};

// 技術分析
class CTAViewObserver : public IObserver
{
public:
	virtual void Update(double dPrice) override
	{}
};

開發看盤軟體時,單一頁面觀察者不會只顯示一檔股票, 如果我們的觀察者對多個股票主題有興趣會怎麼樣呢?假設現在關注的主題有兩個,台積電與鴻海。

class CTSMCStockSubject final // 台積電
{
public:
	void Subscribe(const std::shared_ptr<IObserver>& ob)
	{
		m_observers.emplace(ob);
	}

	void Unsubscribe(const std::shared_ptr<IObserver>& ob)
	{
		m_observers.erase(ob);
	}

	void Notify()
	{
		for (auto& ob : m_observers)
		{
			double dPrice = 300;
			ob->Update(dPrice);
		}
	}

private:
	std::set<std::shared_ptr<IObserver>> m_observers;
};

class CFoxconnStockSubject final // 鴻海
{
public:
	void Notify()
	{
		for (auto& ob : m_observers)
		{
			double dPrice = 70;
			ob->Update(dPrice);
		}
	}

	// 其他程式碼省略...
};

注意 Notify 內呼叫的 Update 並沒有傳識別參數,那麼觀察者又怎麼知道是那個主題通知它呢?因此我們可能會修改為多傳一個股票代碼 (sStockID) 給 Update。

class IObserver
{
public:
	virtual ~IObserver() = 0
	{}

public:
	virtual void Update(const std::string& stockID, double dPrice) = 0;
};

// 技術分析
class CTAViewObserver : public IObserver
{
public:
	virtual void Update(const std::string& stockID, double dPrice) override
	{}
};

那如果是先通知,等有時間再回主題拉 (pull) 資料呢?勢必 Update 就要傳入不同主題類別的指標,因此引進 Subject 抽象基礎類別就有必要。

class ISubject
{
public:
	virtual ~ISubject() = 0
	{}

public:
	virtual void Subscribe(const std::shared_ptr<IObserver>& ob) = 0;
	virtual void Unsubscribe(const std::shared_ptr<IObserver>& ob) = 0;
	virtual void Notify() = 0;

public:
	virtual std::string GetStockID() const = 0;
	virtual double GetPrice() const = 0;
};

// 台積電
class CTSMCStockSubject : public ISubject
{
	// 其他程式碼省略...

public:
	virtual void Notify() override
	{
		for (auto& ob : m_observers)
		{
			ob->Update(this);
		}
	}

public:
	virtual std::string GetStockID() const override
	{
		return "2330.TW";
	}

	virtual double GetPrice() const override
	{
		return 300;
	}

private:
	std::set<std::shared_ptr<IObserver>> m_observers;
};

// 鴻海
class CFoxconnStockSubject : public ISubject
{
	// 其他程式碼省略...

public:
	virtual std::string GetStockID() const override
	{
		return "2317.TW";
	}

	virtual double GetPrice() const override
	{
		return 70;
	}
};

Update 改為接收 ISubject*,其他參數都可以省略。

class IObserver
{
public:
	virtual ~IObserver() = 0
	{}

public:
	virtual void Update(ISubject* pSubject) = 0;
};

// 技術分析
class CTAViewObserver : public IObserver
{
public:
	virtual void Update(ISubject* pSubject) override
	{
		// 這裡可以先將 pSubject 儲存,稍後有時間再回主題類別拉 (pull) 資料
		m_mapStockID2Subject[pSubject->GetStockID()] = pSubject;
	}

private:
	std::map<std::string, ISubject*> m_mapStockID2Subject;
};

到此為止說明了需要 Subject 抽象基礎類別的原因,下一篇會對設計做更深入的探討。

觀察者模式 (Observer pattern) 2

一個主題可能有多個觀察者,例如技術分析 ( 觀察者1 ),當日走勢 (觀察者 2) 等多個觀察者關注相同的主題。

Observer pattern

因為上一篇的設計沒有 Observer 抽象基礎類別,為了滿足多個觀察者的需求,或許會想到可以新增成員變數 ViewType 來區分不同的觀察者,擁有 $View_TA 的是技術分析,而擁有 $View_Tick 的是當日走勢。

enum class ViewType
{
	$View_TA, // 技術分析
	$View_Tick // 當日走勢
};

class CObserver final // 台積電資料的觀察者
{
public:
	CObserver(ViewType eType) : m_eType(eType)
	{}

public:
	void Update(CTSMCStockSubject* pSubject)
	{}

private:
	ViewType m_eType;
};
int main()
{
	auto ob1 = std::make_shared<CObserver>(ViewType::$View_TA); // 技術分析
	auto ob2 = std::make_shared<CObserver>(ViewType::$View_Tick); // 當日走勢

	CTSMCStockSubject subject;
	subject.Subscribe(ob1);
	subject.Subscribe(ob2);
	subject.Notify();
	subject.Unsubscribe(ob2);
	subject.Unsubscribe(ob1);
	return 0;
}

但這樣勢必 CObserver 內會有許多 if … else … 的判斷敘述去處理 $View_TA 及 $View_Tick 的分支 case,或許還有人進一步想到用狀態模式 (State pattern) 來重構 CObserver。

其實這些都是本末倒置,正確做法就是需要有 Observer 抽象基礎類別的存在,不同的觀察者繼承此抽象基礎類別並實作之,對應的處理邏輯寫在各自的衍生類別內,消除了 CObserver 內可能的 if (ViewType == $View_TA) else … 的判斷處理。

class IObserver // Observer 抽象基礎類別
{
public:
	virtual ~IObserver() = 0
	{}

public:
	virtual void Update(CTSMCStockSubject* pSubject) = 0;
};

// 技術分析
class CTAViewObserver : public IObserver
{
public:
	virtual void Update(CTSMCStockSubject* pSubject) override
	{}
};

// 商品盤勢
class CTickViewObserver : public IObserver
{
public:
	virtual void Update(CTSMCStockSubject* pSubject) override
	{}
};

CTSMCStockSubject 類別內所有的 CObserver 也要修改成 IObserver。

class CTSMCStockSubject final // 台積電資料的主題
{
public:
	void Subscribe(const std::shared_ptr<IObserver>& ob)
	{
		m_observers.emplace(ob);
	}

	void Unsubscribe(const std::shared_ptr<IObserver>& ob)
	{
		m_observers.erase(ob);
	}

	void Notify()
	{
		for (auto& ob : m_observers)
		{
			ob->Update(this);
		}
	}

private:
	std::set<std::shared_ptr<IObserver>> m_observers;
};

使用方式改為直接 new 出對應的 class,不必再使用 ViewType 區分,程式更為清楚明確。

int main()
{
	auto ob1 = std::make_shared<CTAViewObserver>(); // 技術分析
	auto ob2 = std::make_shared<CTickViewObserver>(); // 當日走勢

	CTSMCStockSubject subject;
	subject.Subscribe(ob1);
	subject.Subscribe(ob2);
	subject.Notify();
	subject.Unsubscribe(ob2);
	subject.Unsubscribe(ob1);
}

觀察者模式 (Observer pattern) 1

開發看盤軟體一定會用到的設計模式 (Design Pattern)。

Observer pattern

Wiki 的介紹,本文章從省略 Observer 及 Subject 抽象基礎類別 (Abstract Class) 開始講起,一步步推導出觀察者模式,此模式是一種一對多的依賴關係。

開發看盤軟體時,會需要觀看股票,例如想看台積電 (2330.TW) 的資料,因此有必要使用訂閱 – 等待資料更新通知的設計方式。其中主題類別是被觀察者訂閱的,也就是範例中台積電這個主題 (CTSMCStockSubject ),假設現在陽春版的看盤軟體只有一種頁面,也就是只有一個觀察者,那麼確實可以省略 Observer 及 Subject 抽象基礎類別 (Abstract Base Class) 。

P.S. 這裡需要注意,股價資料在盤中是會不斷的更新並通知,如果只需要一次性通訊,例如可能是等待歷史資料完成,那麼使用 std::future 更為適合。

觀察者類別 (CObserver),有一個等待資料更新的函式 (Update),這是會被主題類別呼叫的函式,參數可能會因為需求傳入某種資料格式或是主題類別的指標,稍後再用此指標去取得資料。

class CObserver final // 台積電資料的觀察者
{
public:
	void Update(CTSMCStockSubject* pSubject)
	{}
};

主題類別 (CTSMCStockSubject) 有三個函式,訂閱 (Subscribe)、取消訂閱 (Unsubscribe) 及通知觀察者們 (Notify)。

觀察者訂閱主題時,將自己傳進主題類別內 void Subscribe(const std::shared_ptr<CObserver>& ob),取消訂閱亦然。

class CTSMCStockSubject final // 台積電資料的主題
{
public:
	void Subscribe(const std::shared_ptr<CObserver>& ob)
	{
		m_observers.emplace(ob);
	}

	void Unsubscribe(const std::shared_ptr<CObserver>& ob)
	{
		m_observers.erase(ob);
	}

	void Notify()
	{
		for (auto& ob : m_observers)
		{
			ob->Update(this);
		}
	}

private:
	std::set<std::shared_ptr<CObserver>> m_observers;
};

我們發現紀錄主題類別的觀察者成員變數是使用 std::set,而且元素是 std::shared_ptr<…>,這是為了避免重複訂閱及提供取消訂閱的簡單實作。

std::set 因為元素即 key,因此可以避免重複訂閱。

使用 std::set (O(log N)) 比起 std::vector (O(N)) 可以更快速的遍歷觀察者們,而使用 std::shared_ptr 是為了讓觀察者可以被比較,找到要取消訂閱的元素。

int main()
{
	auto ob = std::make_shared<CObserver>();

	CTSMCStockSubject subject;
	subject.Subscribe(ob);
	subject.Notify();
	subject.Unsubscribe(ob);
	return 0;
}