本文來自網易雲社群
作者:陸秋炜
引言 :很久之前,在做中間件測試的時候,看到開發人員寫的代碼,有人的代碼,看起來總是特别舒服,但有的開發代碼,雖然邏輯上沒有什麼問題,但總給人感覺特别難受。後來成為了一位專職開發人員,漸漸發現,自己的代碼也是屬于“比較難受”的那種。後來随着代碼的增加,編寫代碼時,總有一些比較乖巧的方式,這就是之前不懂的“設計模式”。之前代碼架構比較少(隻是寫一些測試工具),用不到這些,隻有自己慢慢做了一些架構工作後,才用得到,并去主動了解。
但今天想說的,并不是具體的哪一種設計模式的優劣,而是想記錄一下,設計模式中存在的一些設計思想。有了這些設計思想,某些設計模式就自然而然的出現了。是以說,所謂的“設計模式”并不是被發明出來的,而是被我們自己“發現”的。
一,設計是一個逐漸分解的過程,而不是一個功能合成的過程
之前無論是作為開發還是測試,習慣性的覺得,别人提供了什麼功能,就用什麼樣的功能,這樣做天經地義。然而,在自己的架構設計過程中,如果有了這樣額思維,很容易讓自己的程式設計陷入困境。
打個裝修的比喻,我們一定是有設計師設計相關方案(具體的風格),然後分解成對應的家具,然後再購買材料,打造對應的家具。如果我們将這一過程倒過來,先有什麼材料,然後看這些材料能打造出什麼家具,再把家具組合起來,那麼最後的裝修效果一定會非常差。

圖1 正确的設計方式
圖2 自底向上的設計結果,一定是最後的整合有問題
是以優秀的設計一定是從整體到局部設計出來的。從局部構造整體,不可能得到優秀的設計。
二:對于一個整體的概念性了解,一定是在了解最初的功能(實作目标)為基礎的
了解清楚某個功能子產品(或者整個功能)具體要幹什麼事情,我們才能夠知道具體要如何做設計。而不是找一個設計方案,能夠實作主要功能就行了,其他功能再次基礎上修修補補。
再舉一個簡單的例子:比如說我們要喝水(表面功能/基礎目标),那麼我們就需要找相關盛水的容器(設計實作)。我們找到了以下容器(可能的實作方案):
圖三 各種盛水容器的實作
三種容器都能喝水,但具體要使用哪個呢?如果随便選一個酒杯,但具體實作(或者未來可能的功能)要求能夠帶到戶外去,總不能給酒杯再加個蓋子吧;同理,如果我們要品酒,卻選了個保溫杯的實作,到時候直接設計推倒重來了。是以,要有合适的設計,一定要對産品本身的需求(以及未來可能的需求)做詳細的分析和了解,然後确定設計方案。
三:在設計關聯關系時,優先使用對象組合,而非繼承關系
在學習“面向對象”的語言時,我們首先被教會“封裝、繼承、多态”。從此,感覺有點關系的都要進行繼承,覺得這樣能節省好多代碼。然後我們的代碼中便出現了繼承的亂用
正常情況下,這樣做沒有問題,但問題的起源在于,我們的需求是不斷的修改和添加的,如果使用了繼承,在超類中的方法改動,會影響到子類,并可能引起引起子類之間出現備援代碼。
舉個汽車的例子吧,一輛汽車開行(drive)是一樣的,但車标(logo)是不一樣的,是以用繼承
public abstract class Car { /**
* 駕駛汽車
*/
public void drive(){
System.out.print("drive");
} /**
* 每輛車的車标是不一樣的,是以抽象
*/
public abstract void logo() ;
}class BMW extends Car{ @Override
public void logo() {
System.out.print("寶馬");
}
}class Benz extends Car{ @Override
public void logo() {
System.out.print("奔馳");
}
}class Tesla extends Car{ @Override
public void logo() {
System.out.print("特斯拉");
}
}
一切看起來解決的很完美。突然加了一個需求,要求有個充電(change)需求,這時候,隻有特斯拉(tesla)才有充電方法。但如果使用繼承,在父類添加change方法的同時,就需要在BMW和Benz實作無用的change方法,對于子類的影響非常大。但如果使用組合,使用ChangeBehavior,問題就得到了有效解決,
public interface ChargeBehavior { void charge() ;
}public abstract class Car { protected ChargeBehavior chargeBehavior ; /**
* 駕駛汽車
*/
public void drive(){
System.out.print("drive");
} /**
* 每輛車的車标是不一樣的,是以抽象
*/
public abstract void logo() ; /**
* 充電
*/
public void change(){ /**
* 不用關心具體充電方式,委托ChargeBehavior子類實作
*/
if (chargeBehavior!=null) {
chargeBehavior.charge();
}
}
}class Benz extends Car{ @Override
public void logo() {
System.out.print("奔馳");
}
}class BMW extends Car{ @Override
public void logo() {
System.out.print("寶馬");
}
}class Tesla extends Car{ @Override
public void logo() {
System.out.print("特斯拉");
} public Tesla() { super();
chargeBehavior = new TeslaChargeBehavior() ;
}
}class TeslaChargeBehavior implements ChargeBehavior{ @Override
public void charge() {
System.out.print("charge");
}
}
通過将充電的行為委托給changeBehavior接口,子類如果不需要的話,就可以做到無感覺接入。
這樣的代碼有三個優勢
- 1,代碼不需要子類中重複實作
- 2,子類不想要的東西,可以無感覺實作
- 3,子類運作的行為,可以委托給behavior實作,子類本省本身無需任何改動
四:對于接口和類的再次了解
在剛剛接觸面向對象的時候,封裝,對我們來說就是類,執行個體化後就是對象。最基本功能是對于資料進行隐藏,對于行為進行開放(如JavaBean)。慢慢用多了以後漸漸發現,其實我們可以封裝跟多東西,比如某些實作的細節(私有方法方法),執行個體化規則(構造器)等。
1,對于變化本身進行封裝
由于我們的代碼是分層和分子產品的,但我們的需求又是經常要變化的,我們希望修改新功能,對于除了子產品本身外,調用方是無感覺的。是以,我們的類(或者說是子產品吧)變封裝了變化本身。對于調用方來說,隻需要知道不會變的功能名(方法名)就夠了,而不需要了解可能變化的内容。
圖四 變化本身進行封裝
2,從共性和可變性到抽象類
在一類實作中,我們其實可以分析發現,代碼的實作上是有一些共性的,比如說處理的流程(如何調用一些方法的順序),也有一些完全一緻的操作(比如上文提到的car都可以drive,實作一緻的方法)。但也有一些可變性:如必須存在(共性),但實作不一緻的操作(如上文car裡面的logo方法,必須有,但不一緻)。這時候,我們就可以對這些實作進行一些簡單的抽象,成為抽象類。抽象類就是将共性變為以實作的方法,而将可變性變為抽象方法,讓子類予以實作。
圖五,共性和抽象類
總結:
代碼看多了,寫多了,便會發現,看起來舒服的代碼,在可維護性,可讀性,可擴充性上相對來說都比較高。代碼界也有“顔值即戰鬥力”這一說法,頗有一番玄學的味道。但分析具體的原因,其實可以發現,優秀的編碼設計,在其抽象,封裝,都有其合理之處,其整體的架構設計上,亦有其獨到之處。
網易雲大禮包:https://www.163yun.com/gift
本文來自網易雲社群,經作者陸秋炜授權釋出
相關文章:
【推薦】 流式斷言器AssertJ介紹