C# 上 Observer pattern 的實作差異 - observer pattern on C#

前陣子有注意到,大多數的書籍或網站資料在介紹 設計模式(Design Pattern) 時,往往所採用的實作方式都是以 C++ 或 JAVA 為實作基礎,甚至在 Unity 為主題的書籍上,也沒有針對 C# 做進一步說明。

事實上作為一個不斷擴充版本及功能的語言,.NET 的 C# 的使用上提供了更多樣化的類別或介面,可以用不太一樣的方式來實作設計模式,妥善的應用 C# 所帶來的便利。

最明顯的例子,就是利用 C# 的 delegate,可以更加方便的實作 觀察者模式(Observer pattern),甚至讓應用上更加靈活。

本文記錄了三種作法可讓大家比較參考:

  • 純 class
  • Delegate (Action)
  • Event & EventHandler

純 class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public abstract class Subject {
List<Observer> m_Observers = new List<Observer> ();
public void Attach(Observer observer) {
m_Observers.Add (observer);
}
public void Detach (Observer observer) {
m_Observers.Remove (observer);
}
public void Notify (System.Object data) {
for (int i = m_Observers.Count - 1; i >= 0; i--) {
m_Observers\[i\].Update (data);
}
}
}
public abstract class Observer {
public abstract void Update (System.Object data);
}

這是一般很常見的範例內容,其中我加上了 System.Object 作為發出通知時可以額外傳送資料的欄位。

如果想避免使用 System.Object 造成的 Boxing 消耗,可以改用泛型來設計類別:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public abstract class Subject<T> {
private List<Observer<T>> m_Observers = new List<Observer<T>> ();
public void Attach (Observer<T> observer) {
m_Observers.Add (observer);
}
public void Detach (Observer<T> observer) {
m_Observers.Remove (observer);
}
public void Notify (T data) {
for (int i = m_Observers.Count - 1; i >= 0; i--) {
m_Observers\[i\].Update (data);
}
}
}
public abstract class Observer<T> {
public abstract void Update (T data);
}

加上泛型,可以避免掉 Boxing 的消耗,但同時也會犧牲掉 Subject 類別的使用彈性,不再能使用 List 的方式來管理大量的主題。

Delegate (Action)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SubjectWithDelegate<T> {
private System.Action<T> m_Observers = null;
public void Attach (System.Action<T> observer) {
m_Observers += observer;
}
public void Detach (System.Action<T> observer) {
m_Observers -= observer;
}
public void Notify (T data) {
if (m_Observers != null) {
m_Observers (data);
}
}
}

Delegate 是在 C# 相當泛用且強大的功能,先接觸 C++ 的我在一開始並無法靈活使用 Delegate,但是熟悉了之後就不想回去寫要自行管理函式指標的 C++ 了。

利用 Delegate 來實作 觀察者模式(Observer pattern),可以硬生生的省去一個 Observer 類別。不只減少開發上要不斷繼承的需求,也可以更加靈活的在各處使用觀察者 (畢竟只是再多寫個函式),在開發 Unity 時可以使用 C# 真是太好了。

Event & EventHandler

1
2
3
4
5
6
7
8
9
10
public class SubjectWithEvent<T> where T : System.EventArgs {
public event System.EventHandler<T> Observers;
// use += on Observers to Attach
// use -= on Observers to Detach
public void Notify (T data) {
if (Observers != null) {
Observers (this, data);
}
}
}

再更進一步使用 event 的關鍵字來封裝 Delegate,可以再省去兩個方法的實作,同時因為 C# 的 event 關鍵字對 public delegate member 做了安全上的限制,也不怕會被外部類別誤用。關於 event 的說明可以在我的一篇文章中看到:

同時這段程式碼使用了 EventHandler 來代替 Action,如此一來可以使用 EventArgs 這個 .NET 內建的類別作為事件資訊的基礎類別,使類別的架構更加符合設計原則。

不過使用 EventHandler 有個缺點,他是一個 Action<Object, EventArgs> 形式的 Delegate,所以運作上會有 Boxing 消耗,是否使用需依照情況進行斟酌。

其他

介紹了三個方法,每個方法都有各自的優點跟缺點,目前我最常使用的是混和了第二跟第三種方法的設計,即使用 event 關鍵字但不使用 EventHandler。

同為設計模式,有很多不同的實作方式可以因使用的語言而產生差異,如何同時善用語言特性跟設計模式,則需要不斷嘗試跟研究,希望我的一些心得可以帶給大家幫助。

.NET 甚至還有提供以觀察者為名的介面可以直接利用,不過因為我自己沒有使用過,便不多做討論,僅提供官方的文件連結做為參考: