設計模式-觀察者模式下


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

  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用於消費等。

總結

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

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

源碼鏈接


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM