一 設計模式
今天在同事工位上看到了一本《大話設計模式》,倍感親切。本科曾經開過這門課程,很清楚記得當時老師推薦的就是這本書。遺憾的是當時並沒有特別深入的學習瞭解,直到後來參加了工作,才越發的重視設計模式。
其實到目前爲止,我依然不建議開發者主動去套用設計模式,因爲以絕大多數開發者的架構能力,尚不足以在動手之前將模式躍然於心,更多的時候生搬硬套反而捨得其反。
另外,我始終認爲設計模式不是對代碼設計的指導,它僅僅是對過往的採用面向對象設計的項目應用的一種優雅經驗總結。我更建議所有的開發者以實現爲主,重構爲輔,在對程序的不斷優化重構中,模式就漸漸的體現出來了。這樣做不僅能節約人力成本、時間成本,也符合常規項目開發的節奏。
但是我依然推薦大家多看設計模式相關的知識,一是能夠擴展視野,二是能夠反哺我們對代碼的重構設計。這裏除了推薦《大話設計模式》,還推薦一本書《重構——改善既有代碼的設計》,兩者相輔相成,而且都是作爲一名開發者必備的技能,而而而且是硬技能,通喫各個領域的。
二 設計原則
今天在看到《大話設計模式》之後便順嘴問了同事一句:如果想要將設計模式應用於項目,必須準守的六大原則是什麼?其實連我自己都記不大清楚了,但是這些年的工作經驗告訴我即使不應用設計模式,項目設計中也應該儘量遵守這些設計原則,這裏我再挖個墳,簡單列一下:
2.1 單一職責
將函數功能細分,每個函數僅處理一種業務邏輯, 目的是對代碼解耦,更好的複用。
講道理,真正在實施項目的時候,很少有人能遵照這個原則進行設計,大多數開發者都是胡亂一氣的編寫函數,我曾在南京的一家公司負責WEB產品設計,其中一個做服務端開發的女孩子,一個函數3000多行……我不得不將其逐步重構,以便未來更好的維護。
還有一個做前端的小男孩,整篇JS將近8000行,各種邏輯參雜在一起,我曾建議過他對功能函數進行拆分,但是他嗤之以鼻——又不給我加工資咯。當然結果很慘,後來一次功能調整需要在所有終端上同步更新用戶頭像,然後他離職了——因爲程序已經沒辦法維護了,處處修改,各種漏洞,牽一髮動全身。
2.2 里氏替換
原則的定義爲“任何基類出現的地方,子類一定可以出現”。
很拗口,也很難理解。我把它當白話說,就是派生類除了自身功能的擴展需求外,不應該改寫父類的實現,而且對父類的方法引用不應該輕易的調整。
爲什麼?
舉個簡單的例子,Java中Object作爲超類其實現了toString()方法,現在一個新類重寫並返回null,後續從該類派生的其他子類並未關注,那麼在通過toString()方法輸出對象信息時,會產生難以理解甚至程序崩潰的情況。
再者,如果不能通過父類聲明的方法來實現派生類多態的邏輯實現,每次做需求調整,都需要對父類方法調用位置進行代碼修改,那麼工作量和項目風險就驟然上升,甚至整個架構體系都會出現問題。
出現這種情況一般是在設計之初沒有真正的理清需求場景,所以沒辦法對模型設計做良好的抽象,最終導致整體設計結構被推翻。
里氏替換原則其實歸根到底就是要告訴我們——事先做好抽象工作,搭建底層函數調用體系,頂層應用如無特殊需求,不允許修改基類的功能實現。
2.3 依賴倒置
高層次模塊設計不允許依賴低層次模塊設計的實現,簡而言之就是依賴接口,而非實現。
說個很好玩的例子,電腦主板設計如果依賴CPU的實現,那麼如果想更換CPU是無法實現的,除非換掉整個主板。而可插拔的主板僅依賴CPU接口,希望升級CPU的時候,拔掉換一個就是了。
2.4 接口隔離
接口定義時,追求簡單。更直白點,如果一個接口中定義了兩個方法,那麼更好的做法是定義兩個接口……(無語,接口定義的多就真的好麼,除了功能組合和接口複用確實便利,維護和閱讀真尼瑪煩,JDK8突然就多了N多個接口定義,我都懶得看)
2.5 知道最少
這個原則也叫迪米特原則,簡而言之我要實現某個功能,需要依賴其他類的方法,那麼我並不關心這個類的這個方法的內部實現細節,我只管函數調用的處理結果及返回。
2.6 開閉
這個原則是我認爲最應該作爲所有項目實施的必須遵守的原則,對功能擴展開放,對修改關閉。
啥意思,新增功能可以,怎麼添加代碼都無所謂,但是!!!不允許修改既有代碼!!!
因爲設計結構的不合理,常常就會出現對現有代碼的調整,每次調整的代價就是承擔修改所帶來的額外Bug風險,給測試增加負擔,因爲之前的驗收都白費了,這也是我爲什麼要寫這篇博客的原因。
綜上,設計模式的六大原則告訴我們:
- 單一職責原則告訴我們實現類要職責單一
- 里氏替換原則告訴我們不要破壞繼承體系
- 依賴倒置原則告訴我們要面向接口編程
- 接口隔離原則告訴我們在設計接口的時候要精簡單一
- 知道最少原則告訴我們要降低耦合
- 開閉原則是總綱,告訴我們要對擴展開放,對修改關閉
三 外發需求
客戶端通過核心將請求轉發給其他外圍系統,此種場景爲請求外發。
因客戶端無法直接與外圍系統建立通訊,只能將請求交予核心,由核心負責與外圍系統通訊,實現請求外發。
初始對接較爲簡單,僅核心以TCP協議與一個外圍通信,並且約定請求報文格式爲XML,此時系統設計爲:
/**
* 外發請求
*
* @param request XML請求
* @return XML應答
*/
public static Document redirectRequest(Document request) {
Document response = null;
// TODO
// 1. 建立Socket通信
// 2. 發送請求request
// 3. 接收應答response
// 4. 返回應答
return response;
}
再後來,業務複雜了,外圍系統越來越多,通訊方式也很多,Kafka、MQ等等,所以…… 改代碼:
/**
* 外發請求
*
* @param request XML請求
* @param ip 外圍IP
* @param port 外圍端口
* @param communicationType 通訊模式
* @return XML應答
*/
public static Document redirectRequest(Document request, String ip, int port, int communicationType) {
Document response = null;
// TODO
// 1. 判斷通訊類型:1-同步短連接,2-KAFKA
// 2. 建立不同類型的通訊
// 3. 發送請求request
// 4. 接收應答response
// 5. 返回應答,如果目標爲KAKFA僅返回回執
return response;
}
沒多久,需求又來了,這次連報文格式都變了,有JSON、有其他格式……我日,繼續改吧:
/**
* 外發請求
*
* @param request XML請求
* @param ip 外圍IP
* @param port 外圍端口
* @param communicationType 通訊模式
* @return XML應答
*/
public static Document redirectXMLRequest(Document request, String ip, int port, int communicationType) {
Document response = null;
// TODO
// 1. 判斷通訊類型:1-同步短連接,2-KAFKA
// 2. 建立不同類型的通訊
// 3. 發送請求request
// 4. 接收應答response
// 5. 返回應答,如果目標爲KAKFA僅返回回執
return response;
}
/**
* 外發請求
*
* @param request JSON請求
* @param ip 外圍IP
* @param port 外圍端口
* @param communicationType 通訊模式
* @return XML應答
*/
public static JsonObject redirectJSONRequest(JsonObject request, String ip, int port, int communicationType) {
JsonObject response = null;
// TODO
// 1. 判斷通訊類型:1-同步短連接,2-KAFKA
// 2. 建立不同類型的通訊
// 3. 發送請求request
// 4. 接收應答response
// 5. 返回應答,如果目標爲KAKFA僅返回回執
return response;
}
最後好不容易上線了,需求又一次不約而至,老子不想幹了……怎麼辦?因爲每次需求變更,都需要調整現有代碼,不僅核心很心痛,應用一樣難受,這時候已經凸顯出程序的難以維護了。
其實問題在於設計之初沒有很好的控制結構,沒能把對外發過程中涉及到的各個因素進行良好的抽象,此時首要考慮的就是如何重構,橋接模式就這樣突然蹦了出來。
四 橋接模式
橋接模式又稱橋樑模式,也有叫柄體(Handle and Body)模式或接口(Interface)模式的,這種設計模式是對象的結構模式。它的用意是“將抽象化(Abstraction)與實現化(Implementation)脫耦,使得二者可以獨立地變化”。
用我自己的理解來說,就是將需求中的不同維度抽象出來,使不同維度的抽象變化獨立開來,因此可以繞開對既有代碼的不斷修改,完美的適應需求擴展。
橋接模式的類結構圖如下:
橋樑模式雖然不是一個使用頻率很高的模式,但是熟悉這個模式對於理解面向對象的設計原則,包括“開-閉”原則以及組合/聚合複用原則都很有幫助。理解好這兩個原則,有助於形成正確的設計思想和培養良好的設計風格。
橋樑模式的用意是“將抽象化(Abstraction)與實現化(Implementation)脫耦,使得二者可以獨立地變化”。這句話很短,但是第一次讀到這句話的人很可能都會思考良久而不解其意。
這句話有三個關鍵詞,也就是抽象化、實現化和脫耦。理解這三個詞所代表的概念是理解橋樑模式用意的關鍵。
抽象化
從衆多的事物中抽取出共同的、本質性的特徵,而捨棄其非本質的特徵,就是抽象化。例如蘋果、香蕉、生梨、 桃子等,它們共同的特性就是水果。得出水果概念的過程,就是一個抽象化的過程。要抽象,就必須進行比較,沒有比較就無法找到在本質上共同的部分。共同特徵是指那些能把一類事物與他類事物區分開來的特徵,這些具有區分作用的特徵又稱本質特徵。因此抽取事物的共同特徵就是抽取事物的本質特徵,捨棄非本質的特徵。 所以抽象化的過程也是一個裁剪的過程。在抽象時,同與不同,決定於從什麼角度上來抽象。抽象的角度取決於分析問題的目的。
通常情況下,一組對象如果具有相同的特徵,那麼它們就可以通過一個共同的類來描述。如果一些類具有相同的特徵,往往可以通過一個共同的抽象類來描述。
實現化
抽象化給出的具體實現,就是實現化。
一個類的實例就是這個類的實例化,一個具體子類是它的抽象超類的實例化。
脫耦
所謂耦合,就是兩個實體的行爲的某種強關聯。而將它們的強關聯去掉,就是耦合的解脫,或稱脫耦。在這裏,脫耦是指將抽象化和實現化之間的耦合解脫開,或者說是將它們之間的強關聯改換成弱關聯。
所謂強關聯,就是在編譯時期已經確定的,無法在運行時期動態改變的關聯;所謂弱關聯,就是可以動態地確定並且可以在運行時期動態地改變的關聯。顯然,在Java語言中,繼承關係是強關聯,而聚合關係是弱關聯。
將兩個角色之間的繼承關係改爲聚合關係,就是將它們之間的強關聯改換成爲弱關聯。因此,橋樑模式中的所謂脫耦,就是指在一個軟件系統的抽象化和實現化之間使用聚合關係而不是繼承關係,從而使兩者可以相對獨立地變化。這就是橋樑模式的用意。
摘自《JAVA設計模式》之橋接模式(Bridge)
五 維度抽象
這裏我簡單的將外發涉及的要素抽象如下:
- 報文類型,可支持XML、JSON或其他格式
- 通訊類型,可支持同步短連接、長連接以及KAFKA、MQ等等各種模式
- 外發參數,將所有外發要素配置於數據表,並且提供DAO對其訪問
那麼可以定義如下接口和基類了:
/**
* 用以定義外發請求的通訊模式的實現接口
*/
public interface RedirectRequestCommunicator {
/**
* 發送請求報文
*
* @param request 請求報文
* @param param 外發參數
* @return 應答報文
*/
String sendRequest(String request, RedirectRequestParam param);
}
/**
* 外發處理器基類,按報文格式可派生出具象類型
*/
public class RedirectRequestProcessor {
private RedirectRequestCommunicator communicator;
/**
* 供處理器訪問
*
* @return 通訊處理器實例
*/
public RedirectRequestCommunicator getCommunicator() {
return this.communicator;
}
/**
* 構造
*
* @param communicator 通訊處理器
*/
public RedirectRequestProcessor(RedirectRequestCommunicator communicator) {
this.communicator = communicator;
}
/**
* 處理外發請求
*
* @param request 外發報文
* @param param 外發參數
* @return 應答報文
*/
public String processRequest(String request, RedirectRequestParam param) {
return this.communicator.sendRequest(request, param);
}
}
注意,Processor關注報文的格式,Communicator關注通訊的類型,而Processor持有一個Communicator類型的成員,也即通過兩者的結合才能實現一次滿足業務需求的外發。
並且兩者均面向接口和麪向對象,在業務場景越來越複雜時,只需要從Processor和Communicator派生出更爲具象的類型即可實現自由裝配。
六 應用裝配
至此,框架搭建完畢,後續只需要根據具體的業務需求來實現不同的Processor和Communicator即可,比如說我們需要一個支持同步短連接的XML格式報文外發,那麼只需要如下實現:
/**
* 實現同步短連接的外發通訊模式
*/
public class SocketRedirectRequestCommunicator implements RedirectRequestCommunicator {
/**
* 實現同步短連接通訊
*/
@Override
public String sendRequest(String request, RedirectRequestParam param) {
// TODO
// 1. 建立通訊(param中包含外圍服務器IP端口等信息)
// 2. 發送請求報文
// 3. 接收應答報文
return null;
}
}
/**
* 實現XML格式報文的外發處理
*/
public class XmlRedirectRequestProcessor extends RedirectRequestProcessor {
public XmlRedirectRequestProcessor(RedirectRequestCommunicator communicator) {
super(communicator);
}
/**
* 處理外發請求
*/
public String processRequest(String request, RedirectRequestParam param) {
// TODO
// 1. 格式化請求報文爲XML格式
// 2. 其他業務處理
return getCommunicator().sendRequest(request, param);
}
}
非常舒服了,應用層只需要根據實際業務需求實例化不同格式的外發報文處理器以及不同通訊模式的實現接口,即可裝配各種外發場景:
public static void main(String[] args) {
// 由應用傳遞
String request = null;
// 通過數據庫查詢獲取
RedirectRequestParam param = null;
// 實現外發
String response = new XmlRedirectRequestProcessor(new SocketRedirectRequestCommunicator())
.processRequest(request, param);
// 處理應答
System.out.println(response);
}
不知道各位看官有沒有直觀感受,反正我是舒服了,以後只要有新的報文類型或者通訊類型,只管派生出新的實現即可,終於不再需要反反覆覆的修改程序了。
七 加深優化
這一部分再簡單說下上述實現思路中可能存在的優化點:
- 參數RedirectRequestParam可作爲靜態參數存於Redis,提高訪問效率;
- 應用不必知道核心有那些具體的通訊實現類型以及報文實現類型,應用應該只面對業務,核心可提供兩者的工廠(工廠設計模式讀者可自行了解)對實現進一步封裝,如果param.getCommunicator==“SOCKET”,那麼工廠返回SocketRedirectRequestCommunicator對象即可,也即是說應用只需要瞭解核心的RedirectRequestCommunicator和RedirectRequestProcessor;
- 進一步提供工具函數,對RedirectRequestCommunicator和RedirectRequestProcessor對象的獲取動作進行封裝,應用僅面向工具函數doRedirectRequest(RedirectRequestParam param, String request);
- 應用甚至不應該瞭解RedirectRequestParam,應用應該只傳遞參數訪問的唯一鍵即可,所以函數定義爲doRedirectRequest(String paramId, String request);
- 進一步封裝請求類型,顯然請求報文用String類型描述已經不合適了,可提供Request基類,並由此派生出各類外發報文的類型,所以函數調整爲doRedirectRequest(RedirectRequestParam param, Request request);
當然,本文只是藉着外發業務場景來介紹橋接模式,目前我參與的大型分部署項目,其業務場景遠遠比之複雜,但歸根結底瞭解模式不是最關鍵的,如何通過巧妙的設計來重構代碼,使之變得更加健壯、更好維護纔是每一個開發者的終極目的。
八 結語
如果想關注更多硬技能的分享,可以參考積少成多系列傳送門,未來每一篇關於硬技能的分享都會在傳送門中更新鏈接。