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

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

如何重構我們以前寫的垃圾代碼——觀察者模式 首先來看下 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;

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

訂閱-發佈模式

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

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

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

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


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

-Advertisement-
Play Games
更多相關文章
  • 每日一句英語學習,每天進步一點點: “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. 在 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...