設計模式-觀察者模式下

煮詩君發表於2021-05-16

上一篇說到了觀察者模式較為傳統的用法,這篇準備分享點流行的,不過在開始新內容之前,我們不妨先思考一下兩種場景,一個是報社訂閱報紙,另一個是在黑板上發公告,都是典型觀察者模式應用場景,二者有何不同?

  1. 報社訂閱報紙,訂閱者需要到報社登記交錢,然後報社才會每次有新報紙時通知到訂閱者。
  2. 而在黑板上發公告,釋出的人不知道誰會看到,看到的人也不知道是誰發出的,而事實上,看到公告的人也可能只是偶然的機會瞟了一眼黑板而已。

可以看到,二者有明顯的區別。前者,觀察者必須要註冊到被觀察者上才能接收通知;而後者,觀察者和被觀察者之間是相互完全陌生的。回顧一下我們在上一篇中舉的例子,不難發現它其實類似第二種場景,狗叫並不知道誰會聽見,而聽的人也不是為了聽狗叫,他僅僅是在關注外界的動靜,恰好聽到了狗叫而已。但我們採用的是類似第一種場景的處理方式,顯然並不合適。因此,也就自然而然的留下了兩個問題:

  1. dog.AddObserver(...)真的合適嗎?實際生活中,狗真的有這種能力嗎?
  2. 我們知道C#中不支援多繼承,如果Dog本身繼承自Animal的基類,如果同時作為被觀察者,除了用上述演進一的實現,還能如何實現?

針對這兩個問題,該怎麼解決了?不妨再回顧一下之前學過的設計原則,看看哪裡可以尋找突破口。

一番思索不難發現,主題類違背了合成複用原則,也就是我們常說的,HAS AIS A更好。既然知道HAS A更好,我們為什麼非得通過繼承來實現功能的複用呢?更何況我們繼承的還是個普通類。

演進四-事件匯流排

基於這種思路,我們可以試著把繼承改成組合,不過在這之前,我們不妨一步到位,乾脆再為Subject類定義一個抽象的介面,免得看著不舒服,畢竟面向抽象程式設計嘛:

public interface ISubject
{
    void AddObserver(IObserver observer);

    void RemoveObserver(IObserver observer);

    void Publish(EventData eventData);
}

public class Subject: ISubject
{
    private readonly IList<IObserver> _observers = new List<IObserver>();

    public void AddObserver(IObserver observer)
    {
        _observers.Add(observer);
    }

    public void RemoveObserver(IObserver observer)
    {
        _observers.Remove(observer);
    }

    public void Publish(EventData eventData)
    {
        foreach (var observer in _observers)
        {
            observer.Update(eventData);
        }
    }
}

邏輯並沒有任何改動,僅僅是實現了一個介面而已,這一步不做其實也沒有關係。接下來該做什麼應該也很清楚了,沒錯,就是組合到被觀察者中去,也就是DogSon,下面是具體的實現:

public class Dog
{
    private readonly ISubject _subject;

    public Dog(ISubject subject)
    {
        this._subject = subject;
    }

    public void Bark()
    {
        Console.WriteLine("遙聞深巷中犬吠");

        _subject.Publish(new EventData { Source = this, EventType = "DogBark" });
    }
}

public class Son : IObserver
{
    private readonly ISubject _subject;
    public Son(ISubject subject)
    {
        this._subject = subject;
    }
    public void Update(EventData eventData)
    {
        if (eventData.EventType == "DogBark")
        {
            Wakeup();
        }
    }

    public void Wakeup()
    {
        Console.WriteLine("既而兒醒,大啼");
        _subject.Publish(new EventData { Source = this, EventType = "SonCry" });
    }
}

修改的僅僅是被觀察者,觀察者不需要做任何改變。看到上面的呼叫,不知道大家有沒有一種熟悉的感覺呢?沒錯,這裡的使用方式像極了微服務中常用的事件匯流排EventBus,事實上,事件匯流排就是這麼實現的,基本原理僅僅是觀察者模式繼承組合而已。

再看看呼叫的地方:

static void Main(string[] args)
{
    ISubject subject = new Subject();
    Dog dog = new Dog(subject);
    Wife wife = new Wife();
    Husband husband = new Husband();
    Son son = new Son(subject);

    subject.AddObserver(wife);
    subject.AddObserver(husband);
    subject.AddObserver(son);

    dog.Bark();
}

DogSubject之間的關係改為HAS A之後,實際的事件發出者和事件接收者之間多了一層,使得二者之間完全解耦了。這時,Dog可以繼承自己的Animal基類了,並且也不用再做類似在Dog類中管理WifeHusbandSon這麼奇怪的事了,對觀察者的管理交給匯流排來完成。

再來看看這時的類圖長什麼樣子:

如果覺得複雜,可以不看DogSun這兩個節點,只看實線框中的部分,有沒有發現就是前面簡易版的觀察者模式呢?被觀察者還是Subject,只不過和DogSun已經沒什麼關係了,這是多一層必然會導致的結果。到這裡,其實已經完美實現需求了,Subject是原來的被觀察者,但現在相當於事件匯流排,在程式啟動的時候,將觀察者全部註冊到匯流排上就可以接收到匯流排上的事件訊息了。

演進五-MQ

你以為這樣就完了嗎?其實並沒有。再回到軟體開發領域,我們知道,事件的觸發可以發生在系統內部,也可以發生在系統之間。而前面無論哪種方式的實現,其實解決的都是內部問題,那如果需要跨系統該怎麼辦呢?直接呼叫的話,會像上篇當中的第一個實現一樣,出現強耦合,只不過這時呼叫的不再是普通的方法,而是跨網路的API,而強耦合的也不再是類與類之間,而是系統與系統之間。並且隨著事件數量的增多,也會使得呼叫鏈變得混亂不堪,難以管理。

為了解決這個問題,就需要在所有系統之外,加入一箇中間代理的角色,所有釋出者將事件訊息按不同主題傳送給代理,然後代理再根據觀察者關注主題的不同,將訊息分發給相應的觀察者,當然,前提是釋出者和觀察者都提前在代理這裡完成註冊登記。

我們先模擬實現一個代理,當然,我這裡只是通過單例模式實現一個簡單的示例,真實情況會比這個複雜的多:

public class Broker
{
    private static readonly Lazy<Broker> _instance
        = new Lazy<Broker>(() => new Broker());

    private readonly Queue<EventData> _eventDatas = new Queue<EventData>();

    private readonly IList<IObserver> _observers = new List<IObserver>();

    private readonly Thread _thread;
    private Broker()
    {
        _thread = new Thread(Notify);
        _thread.Start();
    }

    public static Broker Instance
    {
        get
        {
            return _instance.Value;
        }
    }

    public void AddObserver(IObserver observer)
    {
        _observers.Add(observer);
    }

    public void RemoveObserver(IObserver observer)
    {
        _observers.Remove(observer);
    }

    private void Notify(object? state)
    {
        while (true)
        {
            if (_eventDatas.Count > 0)
            {
               var eventData = _eventDatas.Dequeue();
                foreach (var observer in _observers)
                {
                    observer.Update(eventData);
                }
            }

            Thread.Sleep(1000);
        }
    }

    public void Enqueue(EventData eventData)
    {
        _eventDatas.Enqueue(eventData);
    }
}

這裡通過單例模式定義了一個Broker代理類,實際情況下,這部分是由一個永不停機的MQ服務承擔,主要包括四個部分組成:

  1. 一個Queue<EventData>型別的佇列,用於存放事件訊息;
  2. 一組註冊和登出觀察者的方法;
  3. 一個接收來自事件釋出者的事件訊息的方法;
  4. 最後就是事件訊息的通知機制,這裡用的是定時輪詢的方式,實際應用中肯定不會這麼簡單。

事實上,上述四個部分都應該針對不同的主題實現,也就是我們常常會提到的Topic,幾乎所有的MQ都會有Topic的概念,為了簡單,我們這裡就不考慮了。

再來看看Subject的實現:

public interface ISubject
{
    void Publish(EventData eventData);
}

public class Subject: ISubject
{
    public void Publish(EventData eventData)
    {
        Broker.Instance.Enqueue(eventData);
    }
}

由於對IObserver的管理交給了Broker代理,因此這裡就不需要再關注具體的觀察者是誰,也不需要管理觀察者了,只需要負責釋出事件就行了。需要注意的是,事件訊息釋出給了Broker,後續的一切工作交給Broker全權處理,觀察者依然不需要做任何程式碼上的修改。

呼叫的地方涉及到的改變主要體現在觀察者的註冊上,畢竟管理者不再是Subject,而是交由Broker代理接管了:

static void Main(string[] args)
{
    ISubject subject = new Subject();
    Dog dog = new Dog(subject);
    Wife wife = new Wife();
    Husband husband = new Husband();
    Son son = new Son(subject);
    Broker.Instance.AddObserver(wife);
    Broker.Instance.AddObserver(husband);
    Broker.Instance.AddObserver(son);

    dog.Bark();
}

乍一看,事情變得越來越複雜了,這裡為了解決跨系統的問題,又套了一層,類圖有點複雜,為避免混亂,我就不畫了。不過好在思路的演進是清晰的,達到現在的結果,應該也不會覺得突兀,這個其實就是當前盛行的MQ的基本實現思路了。

演進過程

通過前面一系列的改造,我們解決了不同場景下的事件處理問題。接下來,我們再次梳理一下觀察者模式的整個演進過程,先看一張圖:

這張圖顯示了觀察者模式演進的不同階段,主題與觀察者之間的呼叫關係:

  1. 第一階段降低了主題與觀察者之間的耦合度,但並沒有完全解耦,這種情況主要應用在類似報紙訂閱的場景;
  2. 第二階段在主題與觀察者之間加了一條匯流排,使得主題與觀察者完全解耦,這種情況主要運用在類似黑板釋出公告的場景,但該實現難以應對跨系統的事件處理;
  3. 第三階段在匯流排與觀察者之間又加了一個代理,使得存在於不同系統之間的主題與觀察者也能夠解耦並且正常通訊了。

可以看出,他們都有各自的應用場景,並不能簡單的說誰更先進,誰能替代誰。可以預見,觀察者模式未來可能還會繼續演進,去應對更多新的更復雜的場景。

.Net中的應用

既然觀察者模式這麼好用,那.Net框架中自然也會內建一些處理機制了。

  1. 在.Net專案中,委託(delegate)和事件(event)就是觀察者模式的很好的一種實踐,不過需要注意的是,委託和事件,嚴格意義上講,已經不能稱之為設計模式了,因為它們針對的都是方法,跟物件導向設計無關,不過倒是可以稱之為慣用法。不過不管怎麼樣,它們要解決的問題跟觀察者模式是一致的。
  2. .Net中提供了一組泛型介面IObserver<T>IObservable<T>可用於實現事件通知機制,顧名思義,前者相當於觀察者,後者相當於主題。

這裡就不列程式碼,以免喧賓奪主了,因為這不是本文的重點。而且前者太常用了,應該沒什麼人不會。而後者呢,不知道大家用的多不多,但其實我自己沒怎麼用,我更願意根據不同的場景來定義語義更明確的介面,如ISender用於傳送,IProducer用於生產,IListener用於監聽,IConsumer用於消費等。

總結

事件無處不在,毫不誇張的說,整個世界的運轉都是由事件驅動的。因此觀察者模式也是無處不在的。我們知道,設計模式經過這麼多年的發展,已經有了很大的變化,有的下沉變成了某些語言的慣用法,例如後面會講到的迭代器模式,有些上升更偏向於架構模式,例如前面講過的外觀模式。甚至有的被淘汰,例如備忘錄模式。但是觀察者模式卻是唯一一個向上可用於架構設計,向下被實現為慣用法,中間還能重構程式碼,簡直無處不在,無所不能。並且可以預見,未來也必然是經久不衰。

說的有點誇張了,不過也確實說明觀察者模式再怎麼重視也不為過了!

原始碼連結

相關文章