如何重構我們以前寫的垃圾代碼——觀察者模式 首先來看下 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;
為什麼說這是從另一個角度呢,因為這種實現方式其實跟觀察者模式沒什麼關係了,因為我們可以去除掉這些介面也能做到。這種做法我們做到了將觀察者和通知者行為分離。
訂閱-發佈模式
剛開始我以為訂閱-發佈模式就是觀察者模式,只是說法不同而已。
但是實際上呢,他們其實還是一個東西......嗯~~是的,沒錯。
我在網上也查看了一些資料,發現其實有一些人將訂閱-發佈模式與觀察者模式區別開了。說現在我們平時講的訂閱-發佈模式與觀察者模式不一樣,前者能做到完全隔離,零耦合。比如消息中間件等。
其實它們的思想還是一致的,只是訂閱發佈模式增加一個消息調度層用來傳遞信息,使之訂閱者和發佈者不知道彼此的存在。但還是通過一端的改變,其他依賴的對象都會做出相應的更新。