觀察者模式(Observer)
觀察者模式(Observer)又叫做「發佈-訂閱(Publish/Subscribe)模式」。
觀察者模式(Observer)定義了一種一對多的依賴關係,讓多個觀察者物件同時監聽某一個主題物件。這個主題物件在狀態發生變化時,會通知所有觀察者物件,使它們能夠自動更新自己。
觀察者的特點與動機
將一個系統分割成一系統相互協作的類別有一個很不好的副作用,那就是需要維護相關物件間的一致性。我們不希望為了維持一致性而使各類別緊密耦合,這樣會給維護、擴展和重用都帶來不便。
而觀察者模式的關鍵物件是「主題Subject」和「觀察者Object」,一個「主題Subject」可以有很多的「觀察者Object」,一旦「主題Subject」的狀態發生了改變,所有的「觀察者Object」都可以得到通知。
「主題Subject」發出通知時,並不需要知道誰是它的觀察者,也就是說,具體觀察者是誰,它根本不需要知道,而任何一個具體觀察者不知道也不需要知道其他觀察者的存在。
什麼時候考慮用「觀察者模式(Observer)」?
當一個物件的改變需要同時改變其他物件時。而且它不知道到底有多少物件有待改變時,應該考慮使用「觀察者模式(Observer)」。
總結來說
總結來說,「觀察者模式」所做的工作其實就是在解除耦合。讓耦合的雙方都依賴於抽象,而不是依賴具體。從而使得各自的變化都不會影響另一邊的變化。
「抽象觀察者
Object」可以用「介面」來定義,不一定要用「抽象類別」。因為具體的觀察者完全有可能是風牛馬不相及的類別。
觀察者模式結構
Subject類別,可翻譯為主題或抽象通知者,一般用一個抽象類別或者一個介面實現。它把所有對觀察者物件的參考保存在一個聚集裡,每個主題都可以有任何數量的觀察者。抽象主題提供一個介面,可以增加和除觀察者物件。Observer類別,抽象觀察者,為所有的具體觀察者定義一個介面,在得到主題的通知時更新自己。ConcreteSubject類別,具體主題,將有關狀態存入具體觀察者物件;在具體主題的內部狀態改變時,發出通知給所有登記過的觀察者。ConcreteObserver類別,具體觀察者,實現抽象觀察者角色所要求的更新介面,以便使本身的狀態與主題相協調。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Subject 主題或抽象通知者,它把所有對觀察者物件的參考保存在一個聚集裡,每個主題都可以有任何數量的觀察者。抽象主題提供一個介面,可以增加和除觀察者物件。
- observer
+ Attach(in : Obsever)
+ Detach(in : Obsever)
+ Notify()
ConcreteSubject 具體主題,具體主題,將有關狀態存入具體觀察者物件;在具體主題的內部狀態改變時,發出通知給所有登記過的觀察者。
+ SubjectState
Observer 抽象觀察者,為所有的具體觀察者定義一個介面,在得到主題的通知時更新自己。
+ Update()
ConcreteObserver 具體觀察者,具體觀察者,實現抽象觀察者角色所要求的更新介面,以便使本身的狀態與主題相協調。
- subject
- observerState
+ Update()
觀察者模式程式碼
Subject類別(主題或抽象通知者)
Subject類別,可翻譯為主題或抽象通知者,一般用一個抽象類別或者一個介面實現。它把所有對觀察者物件的參考保存在一個聚集裡,每個主題都可以有任何數量的觀察者。抽象主題提供一個介面,可以增加和除觀察者物件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//Subject類別,可翻譯為主題或抽象通知者,一般用一個抽象類別或者一個介面實現。
//它把所有對觀察者物件的參考保存在一個聚集裡,每個主題都可以有任何數量的觀察者。
//抽象主題提供一個介面,可以增加和除觀察者物件。
abstract class Subject {
IList<Observer> observers = new List<Observer>();
//增加觀察者
public void Attach(Observer observer) {
observers.Add();
}
//移除觀察者
public void Detach(Observer observer) {
observers.Remove();
}
//通知
public void Notify() {
foreach(Observer o in observers) {
o.Update();
}
}
}
Observer類別(抽象觀察者)
Observer類別,抽象觀察者,為所有的具體觀察者定義一個介面,在得到主題的通知時更新自己。
這個介面叫做更新介面。抽象觀察者一般用一個抽象類別或者一個介面實現。更新介面通常包含一個Update()方法,這個方法叫做更新方法。
1
2
3
4
5
6
//Observer類別,抽象觀察者,為所有的具體觀察者定義一個介面,在得到主題的通知時更新自己。
//這個介面叫做更新介面。抽象觀察者一般用一個抽象類別或者一個介面實現。
//更新介面通常包含一個Update()方法,這個方法叫做更新方法。
interface Observer {
void Update();
}
具體的觀察者完全有可能是風馬牛不相及的類別,用介面
interface比較好。
ConcreteSubject類別(具體主題)
ConcreteSubject類別,具體主題,將有關狀態存入具體觀察者物件;在具體主題的內部狀態改變時,發出通知給所有登記過的觀察者。
具體主題角色通常用一個具體子類別實現。
1
2
3
4
class ConcreteSubject: Subject {
//具體被觀察者狀態
public string SubjectState {get; set;}
}
ConcreteObserver類別(具體觀察者)
ConcreteObserver類別,具體觀察者,實現抽象觀察者角色所要求的更新介面,以便使本身的狀態與主題相協調。
具體觀察者角色可以保存一個指向具體主題物件的參考。
具體觀察者角色通常用一個具體子類別實現。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ConcreteObserver: Observer {
ConcreteSubject subject; //具體主題/通知者
string observerState;
string name;
public ConcreteSubject Subject {get; set;}
public ConcreteObserver(ConcreteSubject subject, string name) {
this.subject = subject;
this.name = name;
}
public override void Update() {
observerState = subject.SubjectState;
Console.WriteLine($"觀察者{name}的新狀態是{observerState}");
}
}
用戶端程式碼
1
2
3
4
5
6
7
8
9
10
11
12
ConcreteSubject s = new ConcreteSubject();
s.Attach(new ConcreteObserver(s, "X"));
s.Attach(new ConcreteObserver(s, "Y"));
s.Attach(new ConcreteObserver(s, "Z"));
s.SubjectState = "ABC";
s.Notify();
/* 顯示的結果:
觀察者X的新狀態是ABC
觀察者Y的新狀態是ABC
觀察者Z的新狀態是ABC
*/
老闆回來了,我不知道
情境:在公司,老闆不在時,很多人都會偷偷做自己的事,這時,總需要有個眼線,只要老闆回來了,就立即通知大家。
v1.0 雙向耦合的程式碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
//櫃台秘書類別(通知者)
class Secretary {
//櫃台狀態:櫃台透過電話,所說的話或所做的事
public string Action {get; set;}
//同事列表
IList<StockObserver> observers = new List<StockObserver>();
//增加
public void Attach(StockObserver observer) {
//有幾個同事請櫃台秘書幫忙,就給集合增加幾個物件
observers.Add(observer);
}
//通知
public void Notify() {
//老闆來了,就給所有登記的同事們發通知「老闆來了」
foreach(StockObserver o in observers) {
o.Update(); //同事得到櫃台的通知,趕快採取行動
}
}
}
//看股票同事類別(觀察者)
class StockObserver {
string name;
Secretary sercretary;
public StockObserver(string name, Secretary sercretary) {
this.name = name;
this.sercretary = sercretary;
}
//得到櫃台的通知,趕快採取行動
public void Update() {
Console.WriteLine($"{sercretary.Action} {name} 關閉股票行情,繼續工作");
}
}
//用戶端程式碼
public static void Main()
{
//櫃台秘書
Secretary secretary = new Secretary();
//看股票的同事們
StockObserver o1 = new StockObserver("張三", secretary);
StockObserver o2 = new StockObserver("李四", secretary);
//櫃台秘書記下兩位同事
secretary.Attach(o1);
secretary.Attach(o2);
//發現老闆回來了
secretary.Action = "老闆回來了!";
//櫃台秘書通知兩個同事
secretary.Notify();
/* 執行結果
老闆回來了! 張三 關閉股票行情,繼續工作
老闆回來了! 李四 關閉股票行情,繼續工作
*/
}
分析問題
分析:
- 「櫃台」類別和「看股票者」類別之間怎麼樣?互相耦合。因為「櫃台類別」要增加「觀察者(看股票者)」,「看股票類別」需要「櫃台的狀態」。
- 如果「觀察者」中還有人是「看NBA」,「櫃台類別」就得修改了。
問題:
v2.0 解耦實踐1
- 增加「抽象的觀察類別」
- 增加兩個「具體觀察者」
- 櫃台秘書類別,把所有與具體觀察者耦合的地方都改成「抽象觀察者」
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
//抽象的觀察類別
abstract class Observer {
protected string name;
protected Secretary sercretary;
public Observer(string name, Secretary sercretary) {
this.name = name;
this.sercretary = sercretary;
}
public abstract void Update();
}
//具體觀察者 -看股票同事類別
class StockObserver: Observer {
//繼承父類別的建構函式
public StockObserver(string name, Secretary sercretary): base(name, sercretary) {
}
//重寫父類別的Update(),得到櫃台的通知,趕快採取行動
public override void Update() {
Console.WriteLine($"{sercretary.Action} {name} 關閉股票行情,繼續工作");
}
}
//具體觀察者 -看NBA同事類別
class NBAObserver: Observer {
//繼承父類別的建構函式
public NBAObserver(string name, Secretary sercretary): base(name, sercretary) {
}
//重寫父類別的Update(),得到櫃台的通知,趕快採取行動
public override void Update() {
Console.WriteLine($"{sercretary.Action} {name} 關閉NBA線上直播,繼續工作");
}
}
//櫃台秘書類別(通知者)
class Secretary {
//櫃台狀態:櫃台透過電話,所說的話或所做的事
public string Action { get; set;}
//同事列表
IList<Observer> observers = new List<Observer>(); //宣告為「抽象觀察者」類型的集合
//增加觀察者
public void Attach(Observer observer) { //針對抽象程式設計,減少了與具體類別的耦合
observers.Add(observer);
}
//移除觀察者
public void Detach(Observer observer) { //針對抽象程式設計,減少了與具體類別的耦合
observers.Remove(observer);
}
//通知
public void Notify() {
//老闆來了,就給所有登記的同事們發通知「老闆來了」
foreach(Observer o in observers) {
o.Update(); //同事得到櫃台的通知,趕快採取行動
}
}
}
//用戶端程式碼
public static void Main()
{
//櫃台秘書
Secretary secretary = new Secretary();
//觀察者:以父類別(抽象類別)的類型 宣告兩個同事的物件
Observer o1 = new StockObserver("張三", secretary); //看股票的同事
Observer o2 = new NBAObserver("李四", secretary); //看NBA的同事
//櫃台秘書記下兩位同事
secretary.Attach(o1);
secretary.Attach(o2);
//發現老闆回來了
secretary.Action = "老闆回來了!";
//櫃台秘書通知兩個同事
secretary.Notify();
/* 執行結果
老闆回來了! 張三 關閉股票行情,繼續工作
老闆回來了! 李四 關閉NBA線上直播,繼續工作
*/
}
分析問題
用戶端程式跟前面v1.0一樣。
- 具體觀察者(看股票、NBA的同事)跟具體的類型(櫃台秘書)耦合,「櫃台秘書」是一個具體的類型,也應該抽象出來。
- 如果老闆回來,「櫃台秘書」來不及通知大家,那麼通知的任務變成老闆來做。老闆、櫃台秘書都是具體的通知者。
- 觀察者不應該依賴具體的實現,而是一個抽象的通知者。
v3.0 解耦實踐2 (觀察者模式)
- 增加「抽象通知者介面」
- 具體的通知者類別,可能是櫃台秘書,也有可能是老闆,他們也許有各自的一些方法,但對於通知者來說,他們都是一樣的,所以他們都去實現這個介面。
- 對於具體的觀察者,需要更改的地方就是把與「櫃台」耦合的地方都改成針對抽象通知者。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
//通知者介面
interface Subject {
string Action { get; set; } //通知者狀態:櫃台透過電話,所說的話或所做的事
void Attach(Observer observer); //增加觀察者
void Detach(Observer observer); //移除觀察者
void Notify(); //通知
}
//具體通知者 -老闆
class Boss: Subject {
//老闆狀態:所說的話或所做的事
public string Action { get; set;}
//同事列表
IList<Observer> observers = new List<Observer>(); //宣告為「抽象觀察者」類型的集合
//增加觀察者
public void Attach(Observer observer) { //針對抽象程式設計,減少了與具體類別的耦合
observers.Add(observer);
}
//移除觀察者
public void Detach(Observer observer) { //針對抽象程式設計,減少了與具體類別的耦合
observers.Remove(observer);
}
//通知
public void Notify() {
foreach(Observer o in observers) {
o.Update(); //同事採取行動
}
}
}
//具體通知者 -櫃台秘書
class Secretary: Subject {
//與老闆類別類似,略
}
//抽象觀察者
abstract class Observer {
protected string name;
protected Subject subject; //通知者
//原來是「櫃台秘書」現在改成「抽象通知者」
public Observer(string name, Subject subject) {
this.name = name;
this.subject = subject;
}
public abstract void Update();
}
//具體觀察者 -看股票同事類別
class StockObserver: Observer {
//繼承父類別的建構函式。
//原來是「櫃台秘書」現在改成「抽象通知者」
public StockObserver(string name, Subject subject): base(name, subject) {
}
//重寫父類別的Update(),得到通知者的通知,趕快採取行動
public override void Update() {
Console.WriteLine($"{subject.Action} {name} 關閉股票行情,繼續工作");
}
}
//具體觀察者 -看NBA同事類別
class NBAObserver: Observer {
//繼承父類別的建構函式
//原來是「櫃台秘書」現在改成「抽象通知者」
public NBAObserver(string name, Subject subject): base(name, subject) {
}
//重寫父類別的Update(),得到通知者的通知,趕快採取行動
public override void Update() {
Console.WriteLine($"{subject.Action} {name} 關閉NBA線上直播,繼續工作");
}
}
//用戶端程式
public static void Main() {
//老闆
Boss boss = new Boss();
//觀察者:以父類別(抽象類別)的類型 宣告兩個同事的物件
Observer o1 = new StockObserver("張三", boss); //看股票的同事
Observer o2 = new NBAObserver("李四", boss); //看NBA的同事
//通知者加入觀察者
boss.Attach(o1);
boss.Attach(o2);
//老闆走到李四位置
boss.Detach(o1); //張三沒有被老闆通知到,所以移除
//老闆回來了
boss.Action = "我是老闆,我回來了!";
//老闆發出通知
boss.Notify();
}
/* 執行結果:
我是老闆,我回來了! 李四 關閉NBA線上直播,繼續工作
*/
抽象觀察者用介面來定義
實務上的程式設計中,具體的觀察者完全有可能是風馬牛不相及的類別,但它們都需要根據通知者來做出Update()的操作,所以讓它們都實現下面這樣的介面就可以實現這個想法了。
1
2
3
4
//觀察者介面
interface Observer {
void Update();
}
修改後:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//觀察者介面
interface Observer {
void Update();
}
//具體觀察者 -看股票同事類別
class StockObserver: Observer {
string name;
Subject subject;
//原來是「櫃台秘書」現在改成「抽象通知者」
public StockObserver(string name, Subject subject) {
this.name = name;
this.subject = subject;
}
//實作Update(),得到通知者的通知,趕快採取行動
public void Update() {
Console.WriteLine($"{subject.Action} {name} 關閉股票行情,繼續工作");
}
}
//具體觀察者 -看NBA同事類別
class NBAObserver: Observer {
string name;
Subject subject;
//原來是「櫃台秘書」現在改成「抽象通知者」
public NBAObserver(string name, Subject subject) {
this.name = name;
this.subject = subject;
}
//實作父類別的Update(),得到通知者的通知,趕快採取行動
public void Update() {
Console.WriteLine($"{subject.Action} {name} 關閉NBA線上直播,繼續工作");
}
}
結構圖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
抽象通知者 Subject
+ Action:string 通知狀態
+ Attach(in 觀察者: 抽象觀察者) 增加
+ Detach(in 觀察者: 抽象觀察者) 移除
+ Notify() 通知
具體通知者-老闆 Boss
+ Action:string 通知狀態
+ Attach(in 觀察者: 抽象觀察者) 增加
+ Detach(in 觀察者: 抽象觀察者) 移除
+ Notify() 通知
具體通知者-櫃台秘書 Secretary
+ Action:string 通知狀態
+ Attach(in 觀察者: 抽象觀察者) 增加
+ Detach(in 觀察者: 抽象觀察者) 移除
+ Notify() 通知
抽象觀察者 Observer
+ Update() 更新自己
具體觀察者-看股票同事 StockObserver
+ Update() 更新自己
具體觀察者-看NBA同事 NBAObserver
+ Update() 更新自己
觀察者模式的不足
儘管已經用了「依賴倒轉原則」,但是「抽象通知者」還是依賴「抽象觀察者」,也就是說,萬一沒有了抽象觀察者這樣的介面,這通知的功能也沒辦法做了。
另外就是每個具體觀察者,它不一定是「更新」的方法要調用呀。
如果通知者和觀察者之間根本互相不知道,由用戶端來決定通知誰,那就好了。(怎麼辦?事件委託實現)
事件委託實現
去除「抽象觀察類別」: 「看股票觀察者」和「看NBA觀察者」先去掉父類別的「抽象觀察類別」,並將各自的「更新方法」名稱改為各自適合的方法名稱。
「抽象通知者」去除依賴「抽象觀察者」: 「抽象通知者」由於不希望依賴「抽象觀察者」,所以「增加」、「移除」的方法也就沒有必要了(抽象觀察者已經不存在了)。
使用「委託」來處理「觀察者」: 接著就是如何處理「老闆」類別和「櫃台秘書」類別的問題,它們當中「通知」方法有了「觀察者」的走遍,我們可以使用「委託」來處理這個問題。
1. 去除「抽象觀察類別」
「看股票觀察者」和「看NBA觀察者」先去掉父類別的「抽象觀察類別」,並將各自的「更新方法」名稱改為各自適合的方法名稱。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//具體觀察者 -看股票同事類別
class StockObserver {
string name;
Subject subject;
public StockObserver(string name, Subject subject) {
this.name = name;
this.subject = subject;
}
//關閉股票行情
public void CloseStockMarket() { //更新方法Update()改為「關閉股票行情」
Console.WriteLine($"{subject.Action} {name} 關閉股票行情,繼續工作");
}
}
//具體觀察者 -看NBA同事類別
class NBAObserver {
string name;
Subject subject;
public NBAObserver(string name, Subject subject) {
this.name = name;
this.subject = subject;
}
//關閉NBA直播
public void CloseNBALiveStreaming() { //更新方法Update()改為「關閉NBA直播」
Console.WriteLine($"{subject.Action} {name} 關閉NBA線上直播,繼續工作");
}
}
2.「抽象通知者」拿掉相關「抽象觀察者」依賴
「抽象通知者」由於不希望依賴「抽象觀察者」,所以「增加」、「移除」的方法也就沒有必要了(抽象觀察者已經不存在了)。
1
2
3
4
5
//通知者介面
interface Subject {
string SubjectState { get; set; } //通知者狀態:所說的話或所做的事
void Notify(); //通知
}
3. 使用「委託」來處理「觀察者」
下面就是如何處理「老闆」類別和「櫃台秘書」類別的問題,它們當中「通知」方法有了「觀察者」的走遍,我們可以使用「委託」來處理這個問題。
實作「委託」
- 宣告一個委託,名稱叫「EventHandler(事件處理程式)」,無參數,無返回值。
1
delegate void EventHandler();
老闆類別和櫃台秘書類別
- 宣告事件 Update,類型為委託 EventHandler
- 在通知方法中,調用事件 Update()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//具體通知者-老闆
delegate void EventHandler();
class Boss: Subject {
//宣告事件Update,類型為EventHandler的委託
public event EventHandler Update;
//通知者狀態:櫃台透過電話,所說的話或所做的事
public string Action { get; set; }
//通知
public void Notify() {
//在呼叫通知方法時,調用「更新」事件
Update();
}
}
//具體通知者-櫃台秘書
class Secretary: Subject {
//與老闆類別類似,略
}
用戶端程式碼
將看股票同事的「關閉股票方法」和看NBA同事的關閉「NBA直播方法」掛到老闆的「更新方法」上,也就是將兩個不同類別的不同方法委託給「老闆」類別的「更新」方法了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//老闆
Boss boss = new Boss();
//看股票的同事
StockObserver staff1 = new StockObserver("張三", boss);
//看NBA的同事
NBAObserver staff2 = new NBAObserver("李四", boss);
//將看股票同事的「關閉股票方法」和看NBA同事的關閉「NBA直播方法」掛到老闆的「更新方法」上
//也就是將兩個不同類別的不同方法委託給「老闆」類別的「更新」方法
boss.Update += new EventHandler(staff1.CloseStockMarket);
boss.Update += new EventHandler(staff2.CloseNBALiveStreaming);
//老闆回來了
boss.Action = "我是老闆,我回來了!";
//老闆發出通知
boss.Notify();
//執行結果:
//我是老闆,我回來了! 張三 關閉股票行情,繼續工作
//我是老闆,我回來了! 李四 關閉NBA線上直播,繼續工作
事件委託的說明
什麼是委託?委託就是一種「參考方法」的類型。一旦為委託分配了方法,委託將與該方法具有完全相同的行為。委託方法的使用可以像其他任何方法一樣,具有參數和返回值。委託可以看作是對函數的抽象,是函數的類別,委託的實體將代表一個具體的函數。
1
2
3
4
5
//可以理解為宣告了一個特殊的類別
delegate void EventHandler();
//可以理解為宣告了一個事件委託的變數叫「更新」
public event EventHandler Update;
委託的實體將代表一個具體的函數,意思是說:
1
2
3
//new EventHandler(staff1.CloseStockMarket)其實就是委託的實體
//而它就等於將staff1.CloseStockMarket()的這個方法給boss.Update這個方法。
boss.Update += new EventHandler(staff1.CloseStockMarket);
一旦為委託分配了方法,委託將與該方法具有完全相同的行為。而且,一個委託可以搭載多個方法,所有方法被依序喚起。更重要的是,它可以使得委託物件所搭載的方法並不需要屬於同一個類別。
這樣就使得,本來是在「老闆類別」中增加和減少的抽象觀察者集合以及通知者走遍的抽象觀察者都不必要了。轉到用戶端來讓委託搭載多個方法,這就解決了本來與博象觀察者的耦合問題。
但委託也是有前提的,那就是,委託物件所搭載的所有方法必須具有相同的原形和形式,也就是擁有相同的參數列表和返回值類型。