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

来源: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. 在 ...
一周排行
  • 1. 雲停車 (開篇)2. 雲停車 (硬體篇)3. 雲停車 (業務篇)4. 雲停車 (平臺篇)5. 雲停車 (源碼篇) 網址:http://42.194.142.223:8091/Home 賬戶:admin 密碼:[email protected] 在場記錄 出入記錄 月租車繳費記錄 臨時車繳費記錄 車場 ...
  • 1.RabbitMQ簡介 因為RabbitMQ是基於開源的AMQP協議來實現的,所以在瞭解MQ時候,首先我們來瞭解下AMQP協議。AMQP,即Advanced Message Queuing Protocol,一個提供統一消息服務的應用層標準高級消息隊列協議,是應用層協議的一個開放標準,為面向消息的 ...
  • 1.AMQP Messaging中的基本概念 Broker:接收和分發消息的應用,RabbitMQ Server就是Message Broker。Virtual Host:出於多租戶和安全因素設計的,把AMQP的基本組件劃分到一個虛擬的分組中,類似於網路中的Namespace概念。當多個不同的用戶使 ...
  • 在顯示或者隱藏視窗的時候,可以利用Windows API中的AnimateWindow函數實現一些特殊的效果。主要的動畫類型有四種:滾動、幻燈片、摺疊或展開和alpha混合漸變。 ##視窗動畫效果 首先定義動畫工具類,引入AnimateWindow函數。 public class WindowsEf ...
  • 泥水佬大佬的地址已經不能下載,所以分享下。 用法很簡單,輸入地址即可。 鏈接:https://pan.baidu.com/s/1OUeybjqY9uGWmxe_ywgwgQ 提取碼:nls9 ...
  • //前提需要 //需要一個 serialPort 工具 可在vs自帶的工具欄中獲得 //源代碼加串口工具地址: //鏈接:https://pan.baidu.com/s/1YbfvdXEmfsJX87D-Jxljyg 提取碼:d32x //記錄用戶打開的串口號 可改為泛型模式 string seri ...
  • 從業務視角還原問題、業務視角抽象問題、二次抽象發現技術問題,作為思考問題的三個還原點。 提煉單點問題解決能力、思考單點問題解決能力,複製、層層遞進思考問題,從深度和廣度出發, 深度以數據作指標,廣度以領域做方向。 腳踏實地的代碼量成長沒有捷徑。 ...
  • Java電子書分類 Java、Spring、SpringBoot、SpringCloud、mybatis、Tomcat、多線程、Git相關、Redis、設計模式、Nginx、Linux、演算法、資料庫、大數據、架構 電子書大概有30G左右 部分電子書截圖 領取方式 加我的微信(s2001sssss)免 ...
  • 一.node啟動js公鑰加密 //需要導入模塊npm install node-forge var arguments = process.argv.splice(2); // console.log('所傳遞的參數是:', arguments); var e = arguments[1]; var ...
  • LeetCode–最長公共首碼 博客說明 文章所涉及的資料來自互聯網整理和個人總結,意在於個人學習和經驗彙總,如有什麼地方侵權,請聯繫本人刪除,謝謝! 說明 leetcode題,14題 最長公共首碼 題目 編寫一個函數來查找字元串數組中的最長公共首碼。 如果不存在公共首碼,返回空字元串 ""。 示例 ...