19. 設計模式-觀察者模式下

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

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

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

  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這麼奇怪的事了,對觀察者的管理交給總線來完成。this

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

若是以爲複雜,能夠不看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用於消費等。

總結

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

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

源碼連接

相關文章
相關標籤/搜索