設計模式實戰 - 中介者模式
0 進銷存管理簡介
各個公司都有相同的三個環節:採購、銷售和庫存
- 比如一個軟體公司,要開發軟體,就需要購買開發環境,如Windows作業系統、資料庫產品等,這就是採購
- 開發完產品還要把產品推銷出
-
有產品就必然有庫存,軟體產品也有庫存,雖然不需要佔用庫房空間,但也要佔用光碟或硬碟,這也是庫存
就來講講它的原理和設計,一般的做法都是通過資料庫來完成相關產品的管理
進銷存示意圖
以終端銷售商(以服務最終客戶為目標的企業,如超市)為例,採購部門要採購IBM的電腦,它根據以下兩個要素來決定採購數量。
● 銷售情況
銷售部門要反饋銷售情況,暢銷就多采購,滯銷就不採購
● 庫存情況
即使是暢銷產品,庫存都有1000臺了,每天才賣出去10臺,也不需再採購
銷售模組是企業的贏利核心,對其他兩個模組也有影響:
● 庫存情況
庫房有貨,才能銷售
● 督促採購
在特殊情況下,比如一個企業客戶要一次性購買100臺電腦,庫存只有80臺,這時需要催促採購部門趕快採購
同樣庫存管理也對其他兩個模組有影響
- 庫房是有容積限制的,不可能無限大,所以就有了清倉處理,那就要求採購部門停止採購,同時銷售部門進行打折銷售
分析來看,這三個模組都有自己的行為,並且與其他模組之間的行為產生關聯,類似於我們辦公室的同事,大家各幹各的活,但是彼此之間還是有交叉的,於是彼此之間就產生緊耦合,也就是一個團隊
我們先來實現這個進銷存
1 實現進銷系統

簡單的進銷存類圖

image
Purchase 採購管理

image
Purchase
定義了採購電腦的標準
- 如果銷售情況比較好,大於80分,你讓我採購多少我就採購多少
- 銷售情況不好,你讓我採購100臺,我就採購50臺,對摺採購
電腦採購完畢,需要放到庫房中,因此要呼叫庫存的方法,增加庫存電腦數量。我們繼續來看庫房Stock類
Stock 庫存管理

image
- 庫房中的貨物數量肯定有增減,同時庫房還有一個容量顯示,達到一定的容量後就要求對一些商品進行折價處理,以騰出更多的空間容納新產品。於是就有了clearStock方法
- 既然是清倉處理肯定就要折價銷售了。於是在Sale類中就有了offSale方法
我們來看Sale原始碼
Sale

image
Sale類中的getSaleStatus是獲得銷售情況
記住要把恰當的方法放到恰當的類中,銷售情況只有銷售人員才能反饋出來,通過百分制的機制衡量銷售情況
我們再來看場景類是怎麼執行的
Client

image
我們在場景類中模擬了三種人員的活動:採購人員採購電腦,銷售人員銷售電腦,庫管員管理庫存
執行結果

image
執行結果也是我們期望的,三個不同型別的參與者完成了各自的活動
你有沒有發現這三個類是彼此關聯的?每個類都與其他兩個類產生了關聯關係
迪米特法則
認為“每個類只和友類交流”,這個友類並非越多越好,友類越多,耦合性越大,要想修改一個就得修改一片,這不是OOP所期望的,況且這還是僅三個模組的情況,屬於比較簡單的一個小專案
我們把進銷存擴充套件一下

擴充套件後的進銷存示意圖
這是一個網的結構,別說是編寫程式了,就是給人看估計也能讓一大批人昏倒!
每個物件都需要和其他幾個物件交流,物件越多,每個物件要交流的成本也就越大,只是維護這些物件的交流就能讓一大批程式設計師望而卻步!從這方面來說,我們已經發現設計的缺陷了,作為一個SE,發現缺陷就要想辦法修改。
網路拓撲有三種類型:匯流排型、環型、星型
-
星型網路拓撲
image
每個計算機通過交換機和其他計算機進行資料交換,各個計算機之間並沒有直接出現互動的情況。這種結構簡單,而且穩定,只要中間那個交換機不癱瘓,整個網路就不會發生大的故障。公司和網咖一般都採用星型網路
我們是不是可以把這種星型結構引入到我們的設計中呢?我們先畫一個示意圖
修改後的進銷存示意圖
加入了一個
中介者
作為三個模組的交流核心,每個模組之間不再相互交流,要交流就通過中介者進行每個模組只負責自己的業務邏輯,不屬於自己的則丟給中介者來處理,簡化了各模組之間的耦合關係
修改後的進銷存類圖
建立兩個抽象類 AbstractMediator
和 AbstractColeague
,每個物件只是與中介者 Mediator
產生依賴,與其他物件之間沒有直接關係
AbstractMediator
的作用是實現中介者的抽象定義,定義了一個抽象方法 execute
抽象中介者

image
具體的中介者

image
我們可以根據業務的要求產生多箇中介者,並劃分各中介者的職責
中介者Mediator定義了多個private方法,其目的是處理各個物件之間的依賴關係,就是說把原有一個物件要依賴多個物件的情況移到中介者的private方法中實現。在實際專案中,一般的做法是中介者按照職責進行劃分,每個中介者處理一個或多個類似的關聯請求。
由於要使用中介者,我們增加了一個抽象同事類,三個具體的實現類分別繼承該抽象類
抽象同事類

image
採購Purchase類

image
Purchase
簡化了很多,也清晰很多,處理自己的職責,與外界有關係的事件處理則交給了中介者來完成
再來看Stock類
Stock

image
修改後的銷售管理

image
增加了中介者,場景類也需要小小的改動
修改後的場景類

image
在場景類中增加了一箇中介者,然後分別傳遞到三個同事類中,三個類都具有相同的特性:只負責處理自己的活動,與自己無關的活動就丟給中介者處理,程式執行的結果是相同的
從專案設計上來看,加入了中介者,設計結構清晰了很多,而且類間的耦合性大大減少,程式碼質量也有了很大的提升。
在多個物件依賴的情況下,通過加入中介者角色,取消了多個物件的關聯或依賴關係,減少了物件的耦合性
定義
-
Define an object that encapsulates how a set of objects interact.Mediator promotes loose coupling by keeping objects from referring to each other explicitly,and it lets you vary their interaction independently
用一箇中介物件封裝一系列的物件互動,中介者使各物件不需要顯示地相互作用,從而使其耦合鬆散,而且可以獨立地改變它們之間的互動
中介者模式通用類圖
● Mediator 抽象中介者角色
定義統一的介面,用於各同事角色之間的通訊
● Concrete Mediator 具體中介者角色
通過協調各同事角色實現協作行為,因此它必須依賴於各個同事角色
● Colleague 同事角色
每一個同事角色都知道中介者角色,而且與其他的同事角色通訊的時候,一定要通過中介者角色協作
每個同事類的行為分為兩種:
- 同事本身的行為
比如改變物件本身的狀態,處理自己的行為等,這種行為叫做自發行為(Self-Method),與其他的同事類或中介者沒有任何的依賴 - 依賴方法(Dep-Method)
必須依賴中介者才能完成的行為
中介者模式比較簡單,其通用原始碼也比較簡單,先看抽象中介者
Mediator類

image
只定義了同事類的注入,為什麼使用同事實現類注入而不使用抽象類注入呢?
那是因為同事類雖然有抽象,但是沒有每個同事類必須要完成的業務方法,當然如果每個同事類都有相同的方法,比如execute、handler等,那當然注入抽象類,做到依賴倒置
具體的中介者一般只有一個,即通用中介者
通用中介者

image
doSomething1
和
doSomething2
都是比較複雜的業務邏輯,為同事類服務,其實現是依賴各個同事類來完成的。
抽象同事類

image
這個基類也非常簡單。一般來說,中介者模式中的抽象都比較簡單,是為了建立這個中介而服務的
具體同事類

image

image
為什麼同事類要使用建構函式注入中介者,而中介者使用getter/setter方式注入同事類呢?
這是因為同事類必須有中介者,而中介者卻可以只有部分同事類
應用
3.1 優點
減少類間的依賴,把原有的一對多的依賴變成了一對一的依賴,同事類只依賴中介者,減少了依賴,當然同時也降低了類間的耦合。
3.2 缺點
中介者會膨脹得很大,而且邏輯複雜,原本N個物件直接的相互依賴關係轉換為中介者和同事類的依賴關係,同事類越多,中介者的邏輯就越複雜。
3.3 中介者模式的使用場景
中介者模式簡單,但是簡單不代表容易使用,很容易被誤用。在面向物件的程式設計中,物件和物件之間必然會有依賴關係,如果某個類和其他類沒有任何相互依賴的關係,那這個類就是一個“孤島”,在專案中就沒有存在的必要了!就像是某個人如果永遠獨立生活,與任何人都沒有關係,那這個人基本上就算是野人了——排除在人類這個定義之外。
類之間的依賴關係是必然存在的,一個類依賴多個類的情況也是存在的,存在即合理,那是否可以說只要有多個依賴關係就考慮使用中介者模式呢?答案是否定的
中介者模式未必能幫你把原本凌亂的邏輯整理得清清楚楚,而且中介者模式也是有缺點的,這個缺點在使用不當時會被放大,比如原本就簡單的幾個物件依賴關係,如果為了使用模式而加入了中介者,必然導致中介者的邏輯複雜化,因此中介者模式的使用需要“量力而行”!中介者模式適用於多個物件之間緊密耦合的情況,緊密耦合的標準是:在類圖中出現了蜘蛛網狀結構。在這種情況下一定要考慮使用中介者模式,這有利於把蜘蛛網梳理為星型結構,使原本複雜混亂的關係變得清晰簡單
4 實際應用
中介者模式也叫做調停者模式,是什麼意思呢?一個物件要和N多個物件交流,就像物件間的戰爭,很混亂。這時,需要加入一箇中心,所有的類都和中心交流,中心說怎麼處理就怎麼處理,我們舉一些在開發和生活中經常會碰到的例子。
● 機場排程中心
大家在每個機場都會看到有一個“××機場排程中心”,它就是具體的中介者,用來排程每一架要降落和起飛的飛機
比如,某架飛機(同事類)飛到機場上空了,就詢問排程中心(中介者)“我是否可以降落”以及“降落到哪個跑道”,排程中心(中介者)檢視其他飛機(同事類)情況,然後通知飛機降落。如果沒有機場排程中心,飛機飛到機場了,飛行員要先看看有沒有飛機和自己一起降落的,有沒有空跑道,停機位是否具備等情況,這種局面是難以想象的!
● MVC框架
大家都應該使用過SpringMVC,MVC框架,其中的C(Controller)就是一箇中介者,叫做前端控制器(Front Controller),它的作用就是把M(Model,業務邏輯)和V(View,檢視)隔離開,協調M和V協同工作,把M執行的結果和V代表的檢視融合成一個前端可以展示的頁面,減少M和V的依賴關係。MVC框架已經成為一個非常流行、成熟的開發框架,這也是中介者模式的優點的一個體現。
● 媒體閘道器
媒體閘道器也是一個典型的中介者模式,比如使用MSN時,張三發訊息給李四,其過程應該是這樣的:張三傳送訊息,MSN伺服器(中介者)接收到訊息,查詢李四,把訊息傳送到李四,同時通知張三,訊息已經發送。在這裡,MSN伺服器就是一箇中轉站,負責協調兩個客戶端的資訊交流,與此相反的就是IPMsg(也叫飛鴿),它沒有使用中介者,而直接使用了UDP廣播的方式,每個客戶端既是客戶端也是伺服器端。
● 中介服務
現在中介服務非常多,比如租房中介、出國中介,這些也都是中介模式的具體體現,比如你去租房子,如果沒有房屋中介,你就必須一個一個小區去找,看看有沒有空房子,有沒有適合自己的房子,找到房子後還要和房東籤合約,自己檢查房屋的傢俱、水電煤等;有了中介後,你就省心多了,找中介,然後安排看房子,看中了,籤合約,中介幫你檢查房屋傢俱、水電煤等等。這也是中介模式的實際應用。
5 最佳實踐
中介者模式很少用到介面或者抽象類,這與依賴倒置原則是衝突的,這是什麼原因呢?
- 首先,既然是同事類而不是兄弟類(有相同的血緣),那就說明這些類之間是協作關係,完成不同的任務,處理不同的業務,所以不能在抽象類或介面中嚴格定義同事類必須具有的方法(從這點也可以看出繼承是高侵入性的)。這是不合適的,就像你我是同事,雖然我們大家都是朝九晚五地上班,但是你跟我乾的活肯定不同,不可能抽象出一個父類統一定義同事所必須有的方法。當然,每個同事都要吃飯、上廁所,可以把這些最基本的資訊封裝到抽象中,但這些最基本的行為或屬性是中介者模式要關心的嗎?如果兩個物件不能提煉出共性,那就不要刻意去追求兩者的抽象,抽象只要定義出模式需要的角色即可。當然如果嚴格遵守面向介面程式設計的話,則是需要抽象的,這就需要讀者在實際開發中靈活掌握
- 其次,在一個專案中,中介者模式可能被多個模組採用,每個中介者所圍繞的同事類各不相同,你能抽象出一個具有共性的中介者嗎?不可能,一箇中介者抽象類一般只有一個實現者,除非中介者邏輯非常複雜,程式碼量非常大,這時才會出現多箇中介者的情況。所以,對於中介者來說,抽象已經沒有太多的必要。
中介者模式是一個非常好的封裝模式,也是一個很容易被濫用的模式,一個物件依賴幾個物件是再正常不過的事情,但是純理論家就會要求使用中介者模式來封裝這種依賴關係,這是非常危險的!使用中介模式就必然會帶來中介者的膨脹問題,這在一個專案中是很不恰當的。大家可以在如下的情況下嘗試使用中介者模式:
● N個物件之間產生了相互的依賴關係(N>2)。
● 多個物件有依賴關係,但是依賴的行為尚不確定或者有發生改變的可能,在這種情況下一般建議採用中介者模式,降低變更引起的風險擴散。
● 產品開發。一個明顯的例子就是MVC框架,把中介者模式應用到產品中,可以提升產品的效能和擴充套件性,但是對於專案開發就未必,因為專案是以交付投產為目標,而產品則是以穩定、高效、擴充套件為宗旨。