觀察者模式
這裡面綜合了幾本書的資料.
需求
有這麼個專案:
需求是這樣的:
一個氣象站, 有三個感測器(溫度, 溼度, 氣壓), 有一個WeatherData物件, 它能從氣象站獲得這三個資料. 還有三種裝置, 可以按要求展示氣象站的最新資料.
WeatherData的結構如下:
有3個get方法, 分別獲取最新的氣溫, 溼度和氣壓. 還有一個measurementsChanged()方法, 當任一感測器有變化的時候, 這個方法都會被呼叫.
總結一下專案的需求:
- WeatherData類有三個get方法可以獲取溫度, 溼度和氣壓
- 如果任何一個資料發生變化, 那麼measureChanged()方法就會被呼叫
- 我們需要實現這三種顯示裝置:
- 當前天氣
- 資料統計
- 天氣預測
- 系統必須可以擴充套件, 其他開發者可以建立自定義展示裝置.
初版程式碼
這個地方有個"錯誤", xxxDisplay都是具體的實現, 而程式設計規則要求是應該對介面程式設計而不是對實現程式設計.
那麼什麼是觀察者模式?
舉一個例子:
- 報社發行報紙
- 你訂閱報紙, 一旦有新一期的報紙發行, 新報紙就會送到你家裡, 只要你一直訂閱, 你就一直會收到新報紙
- 你不再訂閱報紙的時候, 就收不到以後的新報紙了
- 報社運營的時候, 一直會有人去訂閱或者取消訂閱報紙.
釋出者 + 訂閱者 = 觀察者模式
Publishers + Subscribers = Observer Pattern
在觀察者模式裡, 我們把報社叫做被觀察物件(Subject), 把訂閱者叫做觀察者(Observers)
觀察者模式是這樣操作的:
觀察者模式的定義就是:
一個目標物件管理所有相依於它的觀察者物件,並且在它本身的狀態改變時主動發出通知。
類圖如下:
談一下鬆耦合
當兩個物件是鬆耦合的時候, 他們可以進行互動, 但是卻幾乎不瞭解對方.
觀察者模式下的被觀察者(Subject)和觀察者(Observers)就是鬆耦合設計的物件. 這是因為:
- 被觀察者(Subject)只知道觀察者實現了某個介面
- 可以隨時新增觀察者
- 新增新型別觀察者的時候不需要修改被觀察者
- 可以複用觀察者或者被觀察者
- 如果被觀察者或觀察者發生變化了, 那麼這些變化不會影響到對方.
一個設計原則:
互動的物件之間應儘量設計成鬆耦合的. Strive for loosely coupled designs between objects that interact.
鬆耦合設計可以讓我們設計出這樣的系統: 因為物件之間的相互依存減小了, 所以系統可以輕鬆處理變化.
重新設計:
程式碼:
OK, 上面是書中的內容, C#7.0裡面對觀察者模式是怎麼實現的呢?
先只談下面這個:
Event
談到Event, 就得把delegate先細說一下
Delegate 委託
一個委託型別定義了某種型別的方法(方法的返回型別和引數型別), 然後這個委託的例項可以呼叫這些方法.
例如:
delegate int Transformer (int x);
這個委託就和返回型別是int, 引數是一個int的方法相容.
例如:
static int Square (int x) { return x * x }; // 或 static int Square (int x) => x * x;
把一個方法賦值給委託變數的時候就建立了一個委託的例項:
Transformer t = Square;
然後就可以像方法一樣進行呼叫:
int answer = t(3); // 9
所以說一個委託的例項就是呼叫者的委託: 呼叫者呼叫委託, 然後委託呼叫目標方法, 這樣就把呼叫者和目標方法解耦了.
其中:
Transformer t = Square; // 是下面的簡寫 Transformer t = new Transformer(Square);
t(3) // 是下面的簡寫 t.Invoke(3)
多播委託
一個委託例項可以引用多個目標方法. 使用+=操作符.
SomeDelegate d = Method1; d += Method2; // 第二行相當於: d = d + Method2;
呼叫d的時候就會呼叫Method1和Method2兩個方法.
委託方法的呼叫順序和它們被新增的順序是一樣的.
使用-=操作符來移除目標方法:
d -= Method1;
這時呼叫d後只會執行Method2了.
注意: 委託是不可變的 +=/-=實際上是建立了新的委託.
多播委託返回型別
如果多播委託有返回值(非void), 那麼呼叫者只會獲得最後一個被呼叫方法的返回值.
委託也可以使用泛型:
public delegate T Transformer<T> (T arg);
Func 和 Action
記住Func有返回值, Action沒有就行.
Event
使用委託的時候, 通常會有兩個角色出現: 廣播者(被觀察者)和訂閱者(觀察者) [觀察者模式]
廣播者包含一個委託field, 廣播者決定何時廣播, 它通過呼叫委託進行廣播.
訂閱者就是方法的目標接收者.訂閱者可以決定何時開始和結束監聽, 是通過在廣播者的委託上使用+=和-=操作符來實現的.
訂閱者之間互相不瞭解, 不干擾.
event就是為上述模型所存在的, 它只把上述模型所必須的功能從委託裡暴露出來. 它的主要目的就是防止訂閱者之間相互干擾.
最簡單宣告event的方法就是在委託成員前面加上event關鍵字:
public delegate void SomeChangedHandler(decimal x); public class Broadcaster { public event SomeChangedHandler handler; }
在Broadcaster類裡面的程式碼, 可以把handler作為委託一樣來用.
在Broadcaster類外邊, 只能對這個event執行+=和-=操作.
Event 模式/ 觀察者模式
這種模式在.net core裡首先需要EventArgs.
EventArgs是一個基類, 它可以為event傳遞資訊.
可以創造它的子類來傳遞自定義引數:
public class FallsIllEventArgs : EventArgs { public readonly string Address; public FallsIllEventArgs(string address) { this.Address = address; } }
然後就需要給這個event定義一個委託了, 這有三條規則:
- 返回型別必須是void
- 需要有兩個引數, 第一個是object, 第二個是EventArgs的子類. 第一個引數代表著廣播者, 第二個引數包含額外的需要傳遞的資訊.
- 名稱必須以EventHandler結束.
.net core定義了System.EventHandler<>, 它滿足這些要求.
public event EventHandler<FallsIllEventArgs> FallsIll;
最後, 需要寫一個 protected virtual 方法可以觸發event. 方法的名稱必須和event匹配: 以On開頭, 接受EventArgs型別的引數:
public void OnFallsIll() { FallsIll?.Invoke(this, new FallsIllEventArgs("China Beijing")); }
注意: 預定義的非泛型的EventHandler委託可以在沒有資料需要傳輸的時候使用, 呼叫的時候可以使用EventArgs.Empty來避免不必要的初始化EventArgs.
用.net core 實現觀察者模式的程式碼:
Person.cs
using System; namespace ObserverPattern { public class Person { public event EventHandler<FallsIllEventArgs> FallsIll; public void OnFallsIll() { FallsIll?.Invoke(this, new FallsIllEventArgs("China Beijing")); } } }
using System; namespace ObserverPattern { public class FallsIllEventArgs : EventArgs { public readonly string Address; public FallsIllEventArgs(string address) { this.Address = address; } } }
using System; namespace ObserverPattern { class Program { static void Main(string[] args) { var person = new Person(); person.FallsIll += OnFallsIll; person.OnFallsIll(); person.FallsIll -= OnFallsIll; } private static void OnFallsIll(object sender, FallsIllEventArgs eventArgs) { Console.WriteLine($"A doctor has been called to {eventArgs.Address}"); } } }