如何重構我們以前寫的垃圾代碼——觀察者模式

来源:https://www.cnblogs.com/ms27946/archive/2020/02/12/Improve-Your-Code-In-Observer-Pattern.html

如何重構我們以前寫的垃圾代碼——觀察者模式 首先來看下 GoF 對觀察者模式的定義: 多個對象間存在一對多關係,當一個對象發生改變時,把這種改變通知給其他多個對象,從而影響其他對象的行為 就是說當一個對象要發生變化時,要通知其他多個對象同時要發生相應的變化的行為。 從這句定義上來看,重點在於兩個“對 ...


如何重構我們以前寫的垃圾代碼——觀察者模式

首先來看下 GoF 對觀察者模式的定義:

多個對象間存在一對多關係,當一個對象發生改變時,把這種改變通知給其他多個對象,從而影響其他對象的行為

就是說當一個對象要發生變化時,要通知其他多個對象同時要發生相應的變化的行為。

從這句定義上來看,重點在於兩個“對象”——觀察者(後者多個對象),被觀察者(前者一個對象)。也就是我們經常說的訂閱者和發佈者。

首先我們先用最直觀的代碼來實現上面的這句話。

以我們高三高考前夕為時間背景,為了緩建我們的學習壓力,我們班的搞事同學們總是會和當天的值日生串通一氣,在最後一節自習課里,值日生在教室外放哨,我們在教室里看電影《肖生克的救贖》(哈哈哈哈哈,應該不止我們這麼乾過哈~

// 值日生
public class StudentOnDuty {
    private List<StudentObserver> observers = new List<StudentObserver>();
    public void Notify() {
        foreach (var observer in observers) {
            observer.Update();
        }
    }

    public void Attach(StudentObserver observer) {
        observers.Add(observer);
    }

    public string State => "班主任來了!!!";
}

// 搞事的同學們
public class StudentObserver {
    private readonly StudentOnDuty _studentOnDuty;
    public StudentObserver(string studentTypeName, StudentOnDuty studentOnDuty) {
        _studentOnDuty = studentOnDuty;
        StudentTypeName = studentTypeName;
    }
    public void Update() {
        Console.WriteLine(StudentTypeName + "接受到了來自值日生的通知:" + _studentOnDuty.State+ " 關閉講臺的電腦並假裝翻開書本看書,寫作業等");
    }

    public string StudentTypeName { get; }
}
// Program
StudentOnDuty studentOnDuty = new StudentOnDuty();
studentOnDuty.Attach(new StudentObserver("marson shine", studentOnDuty));
studentOnDuty.Attach(new StudentObserver("summer zhu", studentOnDuty));
studentOnDuty.Notify();

這段代碼非常簡單,也是最能體現出對觀察者概念定義的。

那麼上面這段代碼有什麼問題呢?其實很明顯,我們看到值日生和搞事的同學們這兩個類是直接耦合的(值日生要一個個通知他 Attach 的同學,而同學還要記得值日生傳過來的狀態),那麼這就意味著只要改動其中一個類,受影響的就是整體的邏輯。比如我們有些童鞋是不喜歡看我們這次播放的電影,那麼他們必定會幹其它事,比如玩手機,看小說等。所以針對 StudentObserver.Update() 就要更改。

如何重構?

一提到重構,我們腦子裡就要閃出一個念頭,封裝公共部分,提取抽象部分來應對變化的部分。

開閉原則告訴我們對修改關閉,對新增開放。

依賴倒置原則告訴我們要依賴於抽象而不是具體實現。

所以第一件事我們就是要抽象,提取公共部分。顯然,值日生作為通知者,它的通知行為是穩定的。所以把它抽象成一個介面

public interface ISubject {
    object State { get;}
        void Notify();
}

那麼值日生就得繼承它來稱為一個通知者,有人可能會說這裡的場景沒必要讓值日生抽象,因為它是穩定的,它只負責同志我們嘛。其實不然,值日生不可能無時無刻的站在教室外幫我們放哨,不然那也太明顯了吧~。~

值日生有時候也會跟我們一起看電影中的高潮部分啊哈哈。那麼正當這時候,我們的班主任進教室了,除了突然變得死靜以及空氣都凝固的教室之外,我們拿著通知者身份牌的是不是由值日生換成班主任了啊!所以觀察者肯定也是要抽象出來的。而觀察者抽象出來的職責只有一個——接受通知者的通知,來作出相應的更新。

public interface IObserver {
        void Update();
}

那麼如何給值日生添加訂閱者,以及通知呢。因為添加的對象會變,是不穩定的,所以我們要做出相應措施來應對這種情況。

就從原來直接的添加觀察對象轉變成了"面向介面"編程——首先把代表被通知的群體並做出相應改變的行為抽象出來成一個介面,它就只有一個職責,就是做出變換,比如關閉電腦,打開書本假裝自習等。

public class StudentOnDuty: ISubject {
        private readonly List<IObserver> _observers = new List<IObserver>();
  
    public void AddObserver(IObserver observer)
    {
        _observers.Add(observer);// 簡單起見,不驗重等判斷
    }
        public void Notify()
        {
                ....
        }
    public object State => "關電腦,老師來了!!!";
}

public class ClassTeacher: ISubject {
    ...
    public object State => "就怕突如其來的安靜!!!";
    ...
}

那麼我們的觀察者們同樣接受到通知者的訊息,也不能具體的,也要面向抽象編程。因為我們不知道我們即將面對的是沒事的值日生通知還是暴雷的班主任的通知。

// 看電影的同學
public class MovieStudent: IObserver {
    private readonly ISubject _subject;
    public MovieStudent(ISubject subject) {
        _subject = subject;
    }
    public void Update()
    {
        Console.WriteLine($"看電影的同學接受到了subject的訊號:{_subject.State},立馬打開書本假裝自習。")
    }
}
// 負責關電腦的同學
public class ClosePlayerStudent: IObserver {
    //...
}

這樣我們的客戶端的代碼跟之前的調用無異,當要添加乾其他事的同學的通知者時,我們客戶端代碼也無需做過多改變,只需增加具體的實現類即可。

從另一個角度優化

看到這裡千萬不要滿意這個時候的重構程度,其實繼續看我們就會發現,我們在面向介面編程的時候,那我們就必須得按照介面的契約來實現,特別是對代碼有潔癖的人來說,這些觀察者的實現類的方法“Update”一詞實在是不妥,概念太大,太泛了。特別是在這個微服務盛行的時代,每個方法代表的職責要求非常明確,並且我們要從名字上一眼就能看出其行為是最好的。比如負責關電腦的同學他的“Update”所做的就是關電腦並打開書本看書,那麼方法名就應該改為ClosePlayerThenOpenTheBook(),看電影的同學就只需要直接打開書本看書即可,即OpenTheBook()

那麼在這個時候,我們如何通知他們並做出相應的行為呢?

這個時候,我們的委托就要出場了,我們可以給通知者暴露一個委托(也可以是事件),讓這個委托來執行具體觀察者的更新行為。

public class ClassTeacher
{
    ...
    public Action UpdateEvent { get; set;}
    public void Update()
    {
        UpdateEvent?.Invoke();
    }
    ...
}

那麼具體的行為操作就轉到了客戶端了

classTeacher.UpdateEvent += closePlayerStudent.ClosedPLayerThenOpenTheBook;
classTeacher.UpdateEvent += moiveStudent.OpenTheBook;

為什麼說這是從另一個角度呢,因為這種實現方式其實跟觀察者模式沒什麼關係了,因為我們可以去除掉這些介面也能做到。這種做法我們做到了將觀察者和通知者行為分離。

訂閱-發佈模式

剛開始我以為訂閱-發佈模式就是觀察者模式,只是說法不同而已。

但是實際上呢,他們其實還是一個東西......嗯~~是的,沒錯。

我在網上也查看了一些資料,發現其實有一些人將訂閱-發佈模式與觀察者模式區別開了。說現在我們平時講的訂閱-發佈模式與觀察者模式不一樣,前者能做到完全隔離,零耦合。比如消息中間件等。

其實它們的思想還是一致的,只是訂閱發佈模式增加一個消息調度層用來傳遞信息,使之訂閱者和發佈者不知道彼此的存在。但還是通過一端的改變,其他依賴的對象都會做出相應的更新。


您的分享是我們最大的動力!

更多相關文章
  • 每日一句英語學習,每天進步一點點: “Action may not always bring happiness; but there is no happiness without action.” 「行動不見得一定帶來快樂,但沒有行動就沒有快樂。」 前言 我在閱讀 《Effective C++ ...
  • 在前面幾篇文章的例子中也可以看到mybatis中輸入映射和輸出映射的身影,但是沒有系統的總結一下,這篇博客主要對這兩個東東做一個總結。我們知道mybatis中輸入映射和輸出映射可以是基本數據類型、hashmap或者pojo的包裝類型,這裡主要來總結一下pojo包裝類型的使用,因為這個在開發中比較常用 ...
  • 4.1 數組的相關概念和名詞(瞭解) 1、數組(array): 一組具有相同數據類型的數據的按照一定順序排列的集合。 把有限的幾個相同類型的變數使用一個名稱來進行統一管理。 2、數組名: (1)這個數組名,代表的是一組數 (2)這個數組名中存儲的整個數組的“首地址” 3、下標(index): 我們使 ...
  • 由於Api的介面需要返回多語言,因此參考了網上很多篇文章,,有些文章寫的太過於理論,看起來比較費勁,今天下午搞了一個下午,總結了一下經驗,, 做這個功能時,主要參考了兩篇文章: https://blog.johnwu.cc/article/ironman-day21-asp-net-core-loc ...
  • 在 寫xaml的使用遇到了一些特殊字元,這裡記錄一下特殊字元轉義。 這些特殊字元遵循用於編碼的萬維網聯合會(W3C) XML 標準。 下表顯示這組特殊字元的編碼語法: 字元語法描述 < &lt; 小於符號。 > &gt; 大於符號。 & &amp; & 符號。 " &quot; 雙引號。 參見: h ...
  • 樣式提供了重用一組屬性設置的實用方法。它們為幫助構建一致的、組織良好的界面邁出了重要的第一步——但是它們也是有許多限制。 問題是在典型的應用程式中,屬性設置僅是用戶界面基礎結構的一小部分。甚至最基本的程式通常也需要大量的用戶界面代碼,這些代碼與應用程式的功能無關。在許多程式中,用於用戶界面任務的代碼 ...
  • static void Main(string[] args) { InsertionSortDemo(); Console.ReadLine(); } static void InsertionSortDemo() { Random rnd = new Random(); int[] arr = ...
  • 長鏈接發送request/response時, 絕大部分包都是小包, 而每個小包都要消耗一個IP包, 成本大約是20-30us, 普通千兆網卡的pps大約是60Wpps, 所以想要提高長鏈接密集IO的應用性能, 需要做包的合併, 也稱為了scatter/gather io或者vector io. 在 ...
一周排行
  • 前幾天發佈了 "抄抄《CSS 故障藝術》的動畫" 這篇文章,在這篇文章里介紹瞭如何使用Win2D繪製文字然後配合BlendEffect製作故障藝術的動畫。本來打算就這樣收手不玩這個動畫了,但後來又發現性能不符合理想。明明只是做做Resize動畫和用BlendEffect混合,為什麼性能會這麼差呢? ...
  • 控制條控制項: progressBar 不能按照你程式的進程自動變化,需認為計算,調整變化量 private void progressBar1_Click(object sender, EventArgs e) { this.progressBar1.Maximum = 100;//設置進度條最大長 ...
  • 首先創建一個asp.net core web應用程式 第二步 目前官方預置了7種模板項目供我們選擇。從中我們可以看出,既有我們熟悉的MVC、WebAPI,又新添加了Razor Page,以及結合比較流行的Angular、React前端框架的模板項目。 空項目模板 Program.cs using S ...
  • 對閉包的理解 1.對於成員變數和局部變數:成員變數就是方法外部,類的內部定義的變數;局部變數就是方法或語句塊內部定義的變數。局部變數必須初始化。 形式參數是局部變數,局部變數的數據存在於棧記憶體中。棧記憶體中的局部變數隨著方法的消失而消失。成員變數存儲在堆中的對象裡面,由垃圾回收器負責回收。 成員變數它 ...
  • Xamarin.Forms讀取並展示Android和iOS通訊錄 TerminalMACS客戶端 本文同步更新地址: https://dotnet9.com/11520.html https://terminalmacs.com/861.html 閱讀導航: 一、功能說明 二、代碼實現 三、源碼獲取 ...
  • 做下對文件複製操作相關的筆記: /// <summary> /// 文件幫助類 /// </summary> public class FileHelper { /// <summary> /// 複製一個目錄下所有文件到一個新目錄下 /// </summary> /// <param name=" ...
  • 前言 有一個東西叫做鴨子類型,所謂鴨子類型就是,只要一個東西表現得像鴨子那麼就能推出這玩意就是鴨子。 C 裡面其實也暗藏了很多類似鴨子類型的東西,但是很多開發者並不知道,因此也就沒法好好利用這些東西,那麼今天我細數一下這些藏在編譯器中的細節。 不是只有 和 才能 在 C 中編寫非同步代碼的時候,我們經 ...
  • [toc] 1.應用背景 底端設備有大量網路報文(位元組數組):心跳報文,數據採集報文,告警報文上報。需要有對應的報文結構去解析這些位元組流數據。 2.結構體解析 由此,我第一點就想到了用結構體去解析。原因有以下兩點: 2.1.結構體存在棧中 類屬於引用類型,存在堆中;結構體屬於值類型,存在棧中,在一個 ...
  • 《深入淺出 C#》 (第3版) [作者] (美) Andrew Stellman (美) Jennifer Greene[譯者] (中) 徐陽 丁小峰 等譯[出版] 中國電力出版社[版次] 2016年08月 第1版[印次] 2018年04月 第4次 印刷[定價] 148.00元 【引子】 要學習編程 ...
  • 記錄使用對象初始值設定項初始化對象。 using System; using System.Collections.Generic; namespace ConsoleApp2 { class Program { static void Main(string[] args) { // 使用構造函數 ...
x