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

来源: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
  • Timer是什麼 Timer 是一種用於創建定期粒度行為的機制。 與標準的 .NET System.Threading.Timer 類相似,Orleans 的 Timer 允許在一段時間後執行特定的操作,或者在特定的時間間隔內重覆執行操作。 它在分散式系統中具有重要作用,特別是在處理需要周期性執行的 ...
  • 前言 相信很多做WPF開發的小伙伴都遇到過表格類的需求,雖然現有的Grid控制項也能實現,但是使用起來的體驗感並不好,比如要實現一個Excel中的表格效果,估計你能想到的第一個方法就是套Border控制項,用這種方法你需要控制每個Border的邊框,並且在一堆Bordr中找到Grid.Row,Grid. ...
  • .NET C#程式啟動閃退,目錄導致的問題 這是第2次踩這個坑了,很小的編程細節,容易忽略,所以寫個博客,分享給大家。 1.第一次坑:是windows 系統把程式運行成服務,找不到配置文件,原因是以服務運行它的工作目錄是在C:\Windows\System32 2.本次坑:WPF桌面程式通過註冊表設 ...
  • 在分散式系統中,數據的持久化是至關重要的一環。 Orleans 7 引入了強大的持久化功能,使得在分散式環境下管理數據變得更加輕鬆和可靠。 本文將介紹什麼是 Orleans 7 的持久化,如何設置它以及相應的代碼示例。 什麼是 Orleans 7 的持久化? Orleans 7 的持久化是指將 Or ...
  • 前言 .NET Feature Management 是一個用於管理應用程式功能的庫,它可以幫助開發人員在應用程式中輕鬆地添加、移除和管理功能。使用 Feature Management,開發人員可以根據不同用戶、環境或其他條件來動態地控制應用程式中的功能。這使得開發人員可以更靈活地管理應用程式的功 ...
  • 在 WPF 應用程式中,拖放操作是實現用戶交互的重要組成部分。通過拖放操作,用戶可以輕鬆地將數據從一個位置移動到另一個位置,或者將控制項從一個容器移動到另一個容器。然而,WPF 中預設的拖放操作可能並不是那麼好用。為瞭解決這個問題,我們可以自定義一個 Panel 來實現更簡單的拖拽操作。 自定義 Pa ...
  • 在實際使用中,由於涉及到不同編程語言之間互相調用,導致C++ 中的OpenCV與C#中的OpenCvSharp 圖像數據在不同編程語言之間難以有效傳遞。在本文中我們將結合OpenCvSharp源碼實現原理,探究兩種數據之間的通信方式。 ...
  • 一、前言 這是一篇搭建許可權管理系統的系列文章。 隨著網路的發展,信息安全對應任何企業來說都越發的重要,而本系列文章將和大家一起一步一步搭建一個全新的許可權管理系統。 說明:由於搭建一個全新的項目過於繁瑣,所有作者將挑選核心代碼和核心思路進行分享。 二、技術選擇 三、開始設計 1、自主搭建vue前端和. ...
  • Csharper中的表達式樹 這節課來瞭解一下表示式樹是什麼? 在C#中,表達式樹是一種數據結構,它可以表示一些代碼塊,如Lambda表達式或查詢表達式。表達式樹使你能夠查看和操作數據,就像你可以查看和操作代碼一樣。它們通常用於創建動態查詢和解析表達式。 一、認識表達式樹 為什麼要這樣說?它和委托有 ...
  • 在使用Django等框架來操作MySQL時,實際上底層還是通過Python來操作的,首先需要安裝一個驅動程式,在Python3中,驅動程式有多種選擇,比如有pymysql以及mysqlclient等。使用pip命令安裝mysqlclient失敗應如何解決? 安裝的python版本說明 機器同時安裝了 ...