使用C# (.NET Core) 實現觀察者模式 (Observer Pattern) 並介紹 delegate 和 event

solenovex發表於2018-04-01

觀察者模式

這裡面綜合了幾本書的資料.

需求

有這麼個專案: 

需求是這樣的:

一個氣象站, 有三個感測器(溫度, 溼度, 氣壓), 有一個WeatherData物件, 它能從氣象站獲得這三個資料. 還有三種裝置, 可以按要求展示氣象站的最新資料.

WeatherData的結構如下:

有3個get方法, 分別獲取最新的氣溫, 溼度和氣壓. 還有一個measurementsChanged()方法, 當任一感測器有變化的時候, 這個方法都會被呼叫.

總結一下專案的需求:

  • WeatherData類有三個get方法可以獲取溫度, 溼度和氣壓
  • 如果任何一個資料發生變化, 那麼measureChanged()方法就會被呼叫
  • 我們需要實現這三種顯示裝置:
    •   當前天氣
    •   資料統計
    •   天氣預測
  • 系統必須可以擴充套件, 其他開發者可以建立自定義展示裝置.

初版程式碼

這個地方有個"錯誤", xxxDisplay都是具體的實現, 而程式設計規則要求是應該對介面程式設計而不是對實現程式設計.

那麼什麼是觀察者模式?

舉一個例子:

  1. 報社發行報紙
  2. 你訂閱報紙, 一旦有新一期的報紙發行, 新報紙就會送到你家裡, 只要你一直訂閱, 你就一直會收到新報紙
  3. 你不再訂閱報紙的時候, 就收不到以後的新報紙了
  4. 報社運營的時候, 一直會有人去訂閱或者取消訂閱報紙.

釋出者 + 訂閱者 = 觀察者模式
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"));
        }

    }
}

 

 
FallsIllEventArgs.cs:
using System;

namespace ObserverPattern
{
    public class FallsIllEventArgs : EventArgs
    {
        public readonly string Address;

        public FallsIllEventArgs(string address)
        {
            this.Address = address;
        }
    }
}

 

Program.cs:
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}");
        }
    }
}

 

 

相關文章