在軟件工程中谈截,設(shè)計(jì)模式(design pattern)是對(duì)軟件設(shè)計(jì)中普遍存在的各種問題,所提出的解決方案嘁酿。設(shè)計(jì)模式并不是固定的一套代碼即碗,而是針對(duì)某一特定問題的具體解決思路與方案「懔疲可以認(rèn)為是一種最佳實(shí)踐嗓蘑,因?yàn)樗菬o數(shù)軟件開發(fā)人員經(jīng)過長(zhǎng)時(shí)間的實(shí)踐總結(jié)出來的。
在了解設(shè)計(jì)模式之前就我們首先要了解一下面向?qū)ο蟮牧笤瓌t匿乃。
單一職權(quán)原則(Single Responsibility Principle, SRP)
定義:就一個(gè)類而言桩皿,應(yīng)該僅有一個(gè)引起它變化的原因
如果一個(gè)類承擔(dān)的職責(zé)過多,就等于把這些職責(zé)耦合在一起扳埂,一個(gè)職責(zé)的變化可能會(huì)削弱或者抑制這個(gè)類完成其他職責(zé)的能力业簿。這種耦合會(huì)導(dǎo)致脆弱的設(shè)計(jì)瘤礁,當(dāng)變化發(fā)生時(shí)阳懂,設(shè)計(jì)會(huì)遭到意想不到的破壞。
軟件設(shè)計(jì)真正要做的許多內(nèi)容柜思,就是發(fā)現(xiàn)職責(zé)并把那些職責(zé)相互分離岩调,其實(shí)要去判斷是否應(yīng)該分離出來,也不難赡盘,那就是如果你能夠想到多余一個(gè)的動(dòng)機(jī)去改變一個(gè)類号枕,那么這個(gè)類就是對(duì)于一個(gè)的職責(zé)
在我們現(xiàn)實(shí)遇到的需求場(chǎng)景中,完全遵守單一職權(quán)原則也不是一件很好的事陨享。比如我們?cè)?2306購票的下單的時(shí)候葱淳,需要對(duì)我們的身份信息做檢查,根據(jù)單一職權(quán)原則我們單獨(dú)編寫了一個(gè)對(duì)身份信息驗(yàn)證抛姑。但是隨著產(chǎn)品體驗(yàn)的優(yōu)化赞厕,需要在添加一個(gè)重復(fù)訂單的驗(yàn)證,如果根據(jù)單一職權(quán)原則我們還要寫一個(gè)檢查重復(fù)訂單的類進(jìn)行重復(fù)訂單的校驗(yàn)定硝。但是此時(shí)我們的代碼結(jié)構(gòu)已經(jīng)定義好了皿桑,重新寫一個(gè)類,然后修改調(diào)用方法就顯得比較復(fù)雜蔬啡,此時(shí)我們就可以對(duì)檢查類進(jìn)行簡(jiǎn)單的修改诲侮,編寫一個(gè)檢查方法,實(shí)現(xiàn)對(duì)身份檢查和重復(fù)訂單檢查的調(diào)用箱蟆。此時(shí)我們的單一職權(quán)原則可以應(yīng)用到我們的方法上沟绪。雖然這樣做對(duì)于類而言有悖于單一職權(quán)原則,但從下單前的校驗(yàn)角度思考它有遵循于單一職權(quán)原則空猜。(這樣做的風(fēng)險(xiǎn)在于職責(zé)擴(kuò)散的不確定性绽慈,可能以后還需要做更多的檢查诺核,所以記住,在職責(zé)擴(kuò)散到我們無法控制的程度之前久信,立刻對(duì)代碼進(jìn)行重構(gòu)窖杀。可根據(jù)不同的檢查類型細(xì)分為不同的檢查類)
遵循單一職責(zé)原的優(yōu)點(diǎn)有:
- 可以降低類的復(fù)雜度裙士,一個(gè)類只負(fù)責(zé)一項(xiàng)職責(zé)入客,其邏輯肯定要比負(fù)責(zé)多項(xiàng)職責(zé)簡(jiǎn)單的多;
- 提高類的可讀性腿椎,提高系統(tǒng)的可維護(hù)性桌硫;
- 變更引起的風(fēng)險(xiǎn)降低,變更是必然的啃炸,如果單一職責(zé)原則遵守的好铆隘,當(dāng)修改一個(gè)功能時(shí),可以顯著降低對(duì)其他功能的影響南用。
需要說明的一點(diǎn)膀钠,單一職權(quán)原則并不是面向?qū)ο缶幊陶Z言特有的原則,只要是模塊化的程序設(shè)計(jì)裹虫,都適用單一職責(zé)原則肿嘲。
里氏替換原則(Liskov Substitution Principle,LSP)
定義:子類型必須能夠替換掉他們的父類型筑公。
對(duì)于里氏替換原則這個(gè)名稱不用太糾結(jié)雳窟,覺得苦澀難懂,其實(shí)是因?yàn)檫@項(xiàng)原則最早是在1988年匣屡,由麻省理工學(xué)院的一位姓里的女士(Barbara Liskov)提出來的封救,就是單純的一個(gè)名字。
如果把里氏替換原則翻譯成大白話就是一個(gè)軟件實(shí)體如果使用的是一個(gè)父類的話捣作,那么一定適用于其子類誉结,而且它察覺不出父類對(duì)象和子類對(duì)象的區(qū)別,也就是說在軟件里面把父類都替換成它的子類虾宇,程序的行為沒有變化
里氏替換原則主要對(duì)于繼承而言搓彻,B繼承A ,在B中添加新的方法的時(shí)候嘱朽,盡量不要重寫A的方法旭贬,也盡量不要重載父類A的方法。
繼承作為面向?qū)ο笕筇匦灾惶掠荆诮o程序設(shè)計(jì)帶來巨大便利的同時(shí)稀轨,也帶來了弊端。比如使用繼承會(huì)給程序帶來侵入性岸军,程序的可移植性降低奋刽,增加了對(duì)象間的耦合性瓦侮,如果一個(gè)類被其他的類所繼承,則當(dāng)這個(gè)類需要修改時(shí)佣谐,必須考慮到所有的子類肚吏,并且父類修改后,所有涉及到子類的功能都有可能會(huì)產(chǎn)生故障狭魂。
舉例說一下集成的風(fēng)險(xiǎn)
class A{
public int func1(int a, int b){
return a-b;
}
}
public class Client{
public static void main(String[] args){
A a = new A();
System.out.println("100-50="+a.func1(100, 50));
System.out.println("100-80="+a.func1(100, 80));
}
}
運(yùn)行結(jié)果:
100-50=50
100-80=20
后來罚攀,我們需要增加一個(gè)新的功能:完成兩數(shù)相加,然后再與100求和雌澄,由類B來負(fù)責(zé)斋泄。即類B需要完成兩個(gè)功能:
- 兩數(shù)相減。
- 兩數(shù)相加镐牺,然后再加100炫掐。
由于類A已經(jīng)實(shí)現(xiàn)了第一個(gè)功能,所以類B繼承類A后睬涧,只需要再完成第二個(gè)功能就可以了募胃,代碼如下:
class B extends A{
public int func1(int a, int b){
return a+b;
}
public int func2(int a, int b){
return func1(a,b)+100;
}
}
public class Client{
public static void main(String[] args){
B b = new B();
System.out.println("100-50="+b.func1(100, 50));
System.out.println("100-80="+b.func1(100, 80));
System.out.println("100+20+100="+b.func2(100, 20));
}
}
類B完成后,運(yùn)行結(jié)果:
100-50=150
100-80=180
100+20+100=220
我們發(fā)現(xiàn)原本運(yùn)行正常的相減功能發(fā)生了錯(cuò)誤宙地。原因就是類B在給方法起名時(shí)無意中重寫了父類的方法摔认,造成所有運(yùn)行相減功能的代碼全部調(diào)用了類B重寫后的方法回论,造成原本運(yùn)行正常的功能出現(xiàn)了錯(cuò)誤派哲。在本例中帮碰,引用基類A完成的功能,換成子類B之后秽梅,發(fā)生了異常。在實(shí)際編程中剿牺,我們常常會(huì)通過重寫父類的方法來完成新的功能企垦,這樣寫起來雖然簡(jiǎn)單,但是整個(gè)繼承體系的可復(fù)用性會(huì)比較差晒来,特別是運(yùn)用多態(tài)比較頻繁時(shí)钞诡,程序運(yùn)行出錯(cuò)的幾率非常大。如果非要重寫父類的方法湃崩,比較通用的做法是:原來的父類和子類都繼承一個(gè)更通俗的基類荧降,原有的繼承關(guān)系去掉,采用依賴攒读、聚合朵诫,組合等關(guān)系代替。
里氏替換原則通俗的來講就是:子類可以擴(kuò)展父類的功能薄扁,但不能改變父類原有的功能剪返。它包含以下4層含義:
- 子類可以實(shí)現(xiàn)父類的抽象方法废累,但不能覆蓋父類的非抽象方法。
- 子類中可以增加自己特有的方法脱盲。
- 當(dāng)子類的方法重載父類的方法時(shí)邑滨,方法的前置條件(即方法的形參)要比父類方法的輸入?yún)?shù)更寬松。
- 當(dāng)子類的方法實(shí)現(xiàn)父類的抽象方法時(shí)钱反,方法的后置條件(即方法的返回值)要比父類更嚴(yán)格驼修。
依賴倒置原則(Dependence Inversion Principle)
定義:
- 高層模塊不應(yīng)該依賴底層模塊。兩個(gè)都應(yīng)該依賴抽象诈铛。
- 抽象不應(yīng)該依賴細(xì)節(jié)乙各。細(xì)節(jié)應(yīng)該依賴抽象。
依賴倒置原則定義比較繞口幢竹,說白了就是針對(duì)接口編程耳峦,不要針對(duì)實(shí)現(xiàn)編程。
依賴倒置原則基于這樣一個(gè)事實(shí):相對(duì)于細(xì)節(jié)的多變性焕毫,抽象的東西要穩(wěn)定的多蹲坷。以抽象為基礎(chǔ)搭建起來的架構(gòu)比以細(xì)節(jié)為基礎(chǔ)搭建起來的架構(gòu)要穩(wěn)定的多。在java中邑飒,抽象指的是接口或者抽象類循签,細(xì)節(jié)就是具體的實(shí)現(xiàn)類,使用接口或者抽象類的目的是制定好規(guī)范和契約疙咸,而不去涉及任何具體的操作县匠,把展現(xiàn)細(xì)節(jié)的任務(wù)交給他們的實(shí)現(xiàn)類去完成。
同樣我們舉個(gè)例子說明撒轮,雙十一即將來臨乞旦,商城搞滿減活動(dòng)。
public class Client {
public static void main(String[] args) {
Activity activity = new Activity();
activity.sale(new Manjian());
}
}
class Activity {
public void sale(Manjian manjian) {
manjian.activityMode();
}
}
class Manjian {
public void activityMode() {
System.out.println("活動(dòng)方式:滿300減100");
}
}
運(yùn)行輸出活動(dòng)方式:滿300減100
過了一天题山,產(chǎn)品又提出一個(gè)打折的需求兰粉,但是如果實(shí)現(xiàn)就必須需要修改我們的活動(dòng)類,以此類推顶瞳,每次不同的活動(dòng)都要去修改玖姑。這顯然不合理,Activity
和Dicount
耦合性太高了慨菱,因此我們抽象一個(gè)優(yōu)惠類
public interface Reduce {
void activityMode();
}
而Discount
和 ManJian
都實(shí)現(xiàn)Reduce
public class Client {
public static void main(String[] args) {
Activity activity = new Activity();
activity.sale(new Manjian());
activity.sale(new Discount());
}
}
class Activity {
public void sale(Reduce reduce) {
reduce.activityMode();
}
}
class Manjian implements Reduce{
@Override
public void activityMode() {
System.out.println("活動(dòng)方式:滿300減100");
}
}
class Discount implements Reduce{
@Override
public void activityMode() {
System.out.println("活動(dòng)方式:打八折");
}
}
輸出活動(dòng)方式:滿300減100
和活動(dòng)方式:打八折
這樣修改后無論怎么修改活動(dòng)方式都不需要修改Activity
類了
傳遞依賴關(guān)系有三種方式焰络,以上的例子中使用的方法是接口傳遞,另外還有兩種傳遞方式:構(gòu)造方法傳遞和setter方法傳遞抡柿,相信用過Spring框架的舔琅,對(duì)依賴的傳遞方式一定不會(huì)陌生。
在實(shí)際編程中洲劣,我們一般需要做到如下3點(diǎn):
- 低層模塊盡量都要有抽象類或接口备蚓,或者兩者都有课蔬。
- 變量的聲明類型盡量是抽象類或接口。
- 使用繼承時(shí)遵循里氏替換原則郊尝。
依賴倒置原則的核心就是要我們面向接口編程二跋,理解了面向接口編程,也就理解了依賴倒置流昏。
接口隔離原則(Interface Segregation Principle)
定義:客戶端不應(yīng)該依賴它不需要的接口扎即;一個(gè)類對(duì)另一個(gè)類的依賴應(yīng)該建立在最小的接口上。
接口隔離原則簡(jiǎn)單來說就是根據(jù)類的職責(zé)將接口進(jìn)行更細(xì)粒度的拆分况凉,使一個(gè)臃腫的接口分散成幾個(gè)接口谚鄙,由實(shí)現(xiàn)者根據(jù)自身需求去分別實(shí)現(xiàn)。
舉個(gè)??:我們?cè)诜庋bJDBC方法的時(shí)候會(huì)有單表查詢
刁绒、添加查詢
闷营、分頁
等等。如果我們封裝到一個(gè)接口里面知市,有些不需要這么多功能的類也要實(shí)現(xiàn)這些邏輯傻盟,就會(huì)造成代碼的臃腫。這里拿通用Mapper
舉例
public interface SelectOneMapper<T> {
/**
* 根據(jù)實(shí)體中的屬性進(jìn)行查詢嫂丙,只能有一個(gè)返回值娘赴,有多個(gè)結(jié)果是拋出異常,查詢條件使用等號(hào)
*
* @param record
* @return
*/
@SelectProvider(type = BaseSelectProvider.class, method = "dynamicSQL")
T selectOne(T record);
}
public interface SelectMapper<T> {
/**
* 根據(jù)實(shí)體中的屬性值進(jìn)行查詢跟啤,查詢條件使用等號(hào)
*
* @param record
* @return
*/
@SelectProvider(type = BaseSelectProvider.class, method = "dynamicSQL")
List<T> select(T record);
}
public interface SelectAllMapper<T> {
/**
* 查詢?nèi)拷Y(jié)果
*
* @return
*/
@SelectProvider(type = BaseSelectProvider.class, method = "dynamicSQL")
List<T> selectAll();
}
將每一種查詢都封裝成一個(gè)方法诽表,然后寫一個(gè)通用的接口
public interface Mapper<T> extends
BaseMapper<T>,
ExampleMapper<T>,
RowBoundsMapper<T>,
Marker {
}
public interface BaseMapper<T> extends
BaseSelectMapper<T>,
BaseInsertMapper<T>,
BaseUpdateMapper<T>,
BaseDeleteMapper<T> {
}
這樣我們就可以根據(jù)不同的需要進(jìn)行選擇性繼承相應(yīng)功能的接口就可以實(shí)現(xiàn)符合我們需要的接口。
接口隔離原則的含義是:建立單一接口腥光,不要建立龐大臃腫的接口关顷,盡量細(xì)化接口,接口中的方法盡量少武福。也就是說,我們要為各個(gè)類建立專用的接口痘番,而不要試圖去建立一個(gè)很龐大的接口供所有依賴它的類去調(diào)用捉片。本文例子中,將一個(gè)龐大的接口變更為3個(gè)專用的接口所采用的就是接口隔離原則汞舱。在程序設(shè)計(jì)中伍纫,依賴幾個(gè)專用的接口要比依賴一個(gè)綜合的接口更靈活。接口是設(shè)計(jì)時(shí)對(duì)外部設(shè)定的“契約”昂芜,通過分散定義多個(gè)接口莹规,可以預(yù)防外來變更的擴(kuò)散,提高系統(tǒng)的靈活性和可維護(hù)性泌神。
說到這里良漱,很多人會(huì)覺的接口隔離原則跟之前的單一職責(zé)原則很相似舞虱,其實(shí)不然。其一母市,單一職責(zé)原則原注重的是職責(zé)矾兜;而接口隔離原則注重對(duì)接口依賴的隔離。其二患久,單一職責(zé)原則主要是約束類椅寺,其次才是接口和方法,它針對(duì)的是程序中的實(shí)現(xiàn)和細(xì)節(jié)蒋失;而接口隔離原則主要約束接口接口返帕,主要針對(duì)抽象,針對(duì)程序整體框架的構(gòu)建篙挽。
采用接口隔離原則對(duì)接口進(jìn)行約束時(shí)溉旋,要注意以下幾點(diǎn):
- 接口盡量小,但是要有限度嫉髓。對(duì)接口進(jìn)行細(xì)化可以提高程序設(shè)計(jì)靈活性是不掙的事實(shí)观腊,但是如果過小,則會(huì)造成接口數(shù)量過多算行,使設(shè)計(jì)復(fù)雜化梧油。所以一定要適度。
- 為依賴接口的類定制服務(wù)州邢,只暴露給調(diào)用的類它需要的方法儡陨,它不需要的方法則隱藏起來。只有專注地為一個(gè)模塊提供定制服務(wù)量淌,才能建立最小的依賴關(guān)系骗村。
- 提高內(nèi)聚,減少對(duì)外交互呀枢。使接口用最少的方法去完成最多的事情胚股。
迪米特法則(Law Of Demeter)
定義:如果兩個(gè)類不必彼此直接通信,那么這兩個(gè)類就不應(yīng)當(dāng)發(fā)生直接的相互作用裙秋,如果其中一個(gè)類需要調(diào)用另一個(gè)類的某一個(gè)方法的話琅拌,可以通過第三者轉(zhuǎn)發(fā)這個(gè)調(diào)用。
迪米特法則也叫最少知識(shí)原則摘刑。強(qiáng)調(diào)的是一個(gè)對(duì)象應(yīng)該對(duì)其他對(duì)象保持做少的了解进宝,在類的結(jié)構(gòu)設(shè)計(jì)上,每一個(gè)類都應(yīng)當(dāng)盡量降低成員的訪問權(quán)限枷恕,也就是說党晋,一個(gè)類包裝好自己的private
狀態(tài),不需要讓別的類知道的字段或行為就不要公開。
迪米特法則其根本思想未玻,是強(qiáng)調(diào)了類之間的松耦合灾而,類之間耦合越弱,越利于復(fù)用深胳,一個(gè)處在弱耦合的類被修改绰疤,不會(huì)對(duì)有關(guān)系的類造成波及。
迪米特法則的初衷是降低類之間的耦合舞终,由于每個(gè)類都減少了不必要的依賴轻庆,因此的確可以降低耦合關(guān)系。但是凡事都有度敛劝,雖然可以避免與非直接的類通信余爆,但是要通信,必然會(huì)通過一個(gè)“中介”來發(fā)生聯(lián)系夸盟。過分的使用迪米特原則蛾方,會(huì)產(chǎn)生大量這樣的中介和傳遞類,導(dǎo)致系統(tǒng)復(fù)雜度變大上陕。所以在采用迪米特法則時(shí)要反復(fù)權(quán)衡桩砰,既做到結(jié)構(gòu)清晰,又要高內(nèi)聚低耦合释簿。
開閉原則
定義:軟件實(shí)體(類亚隅、模塊、函數(shù)等等)應(yīng)該可以擴(kuò)展庶溶,但是不可修改煮纵。
開閉原則有兩個(gè)特征
1.對(duì)擴(kuò)展是開放的(Open for extension)
2.對(duì)更改是封閉的(Closed for modification)
我們?cè)谧鋈魏蜗到y(tǒng)的時(shí)候,都不可能一開始指定需求就不在發(fā)生變化偏螺,但是每次需求的變化都會(huì)引起對(duì)原有代碼的修改行疏,很有可能會(huì)給舊的代碼引入錯(cuò)誤,也可能會(huì)使我們不得不對(duì)整個(gè)功能進(jìn)行重構(gòu)套像,并且還要測(cè)試一遍原有的代碼酿联。
絕對(duì)的對(duì)修改關(guān)閉是不現(xiàn)實(shí)的,這就要求設(shè)計(jì)人員必須對(duì)于他設(shè)計(jì)的代碼應(yīng)該應(yīng)對(duì)那種變化封閉做出選擇凉夯。他必須先猜測(cè)出來最有可能變化的種類货葬,然后構(gòu)造抽象來隔離那些變化。但是我們是很難進(jìn)行預(yù)先的猜測(cè)劲够,這樣要求我們等到變化發(fā)生時(shí)立即采取行動(dòng),當(dāng)發(fā)生變化時(shí)休傍,我們就創(chuàng)建抽象來隔離以后發(fā)生的同類變化
開閉原則是面向?qū)ο笤O(shè)計(jì)的核心所在征绎,遵循這個(gè)原則可以帶來面向?qū)ο蠹夹g(shù)所聲稱的巨大好處,也就是可維護(hù)、可擴(kuò)展人柿、可復(fù)用柴墩、靈活性好。
其實(shí)凫岖,我們遵循設(shè)計(jì)模式前面5大原則江咳,以及使用23種設(shè)計(jì)模式的目的就是遵循開閉原則。也就是說哥放,只要我們對(duì)前面5項(xiàng)原則遵守的好了歼指,設(shè)計(jì)出的軟件自然是符合開閉原則的,這個(gè)開閉原則更像是前面五項(xiàng)原則遵守程度的“平均得分”甥雕,前面5項(xiàng)原則遵守的好踩身,平均分自然就高,說明軟件設(shè)計(jì)開閉原則遵守的好社露;如果前面5項(xiàng)原則遵守的不好挟阻,則說明開閉原則遵守的不好。
再回想一下前面說的5項(xiàng)原則峭弟,恰恰是告訴我們用抽象構(gòu)建框架附鸽,用實(shí)現(xiàn)擴(kuò)展細(xì)節(jié)的注意事項(xiàng)而已:?jiǎn)我宦氊?zé)原則告訴我們實(shí)現(xiàn)類要職責(zé)單一;里氏替換原則告訴我們不要破壞繼承體系瞒瘸;依賴倒置原則告訴我們要面向接口編程坷备;接口隔離原則告訴我們?cè)谠O(shè)計(jì)接口的時(shí)候要精簡(jiǎn)單一;迪米特法則告訴我們要降低耦合挨务。而開閉原則是總綱击你,他告訴我們要對(duì)擴(kuò)展開放,對(duì)修改關(guān)閉谎柄。
最后說明一下如何去遵守這六個(gè)原則丁侄。對(duì)這六個(gè)原則的遵守并不是是和否的問題,而是多和少的問題朝巫,也就是說鸿摇,我們一般不會(huì)說有沒有遵守,而是說遵守程度的多少劈猿。任何事都是過猶不及拙吉,設(shè)計(jì)模式的六個(gè)設(shè)計(jì)原則也是一樣,制定這六個(gè)原則的目的并不是要我們刻板的遵守他們揪荣,而需要根據(jù)實(shí)際情況靈活運(yùn)用筷黔。對(duì)他們的遵守程度只要在一個(gè)合理的范圍內(nèi),就算是良好的設(shè)計(jì)仗颈。我們用一幅圖來說明一下佛舱。
圖中的每一條維度各代表一項(xiàng)原則,我們依據(jù)對(duì)這項(xiàng)原則的遵守程度在維度上畫一個(gè)點(diǎn),則如果對(duì)這項(xiàng)原則遵守的合理的話请祖,這個(gè)點(diǎn)應(yīng)該落在紅色的同心圓內(nèi)部订歪;如果遵守的差,點(diǎn)將會(huì)在小圓內(nèi)部肆捕;如果過度遵守刷晋,點(diǎn)將會(huì)落在大圓外部。一個(gè)良好的設(shè)計(jì)體現(xiàn)在圖中慎陵,應(yīng)該是六個(gè)頂點(diǎn)都在同心圓中的六邊形眼虱。
在上圖中,設(shè)計(jì)1荆姆、設(shè)計(jì)2屬于良好的設(shè)計(jì)蒙幻,他們對(duì)六項(xiàng)原則的遵守程度都在合理的范圍內(nèi);設(shè)計(jì)3胆筒、設(shè)計(jì)4設(shè)計(jì)雖然有些不足邮破,但也基本可以接受;設(shè)計(jì)5則嚴(yán)重不足仆救,對(duì)各項(xiàng)原則都沒有很好的遵守抒和;而設(shè)計(jì)6則遵守過渡了,設(shè)計(jì)5和設(shè)計(jì)6都是迫切需要重構(gòu)的設(shè)計(jì)彤蔽。