設計模式 - 概述

在軟件工程中,設計模式(design pattern)是對軟件設計中普遍存在的各種問題,所提出的解決方案。設計模式并不是固定的一套代碼,而是針對某一特定問題的具體解決思路與方案。可以認為是一種最佳實踐,因為他是無數(shù)軟件開發(fā)人員經(jīng)過長時間的實踐總結出來的。
在了解設計模式之前就我們首先要了解一下面向?qū)ο蟮牧笤瓌t。

單一職權原則(Single Responsibility Principle, SRP)

定義:就一個類而言,應該僅有一個引起它變化的原因

如果一個類承擔的職責過多,就等于把這些職責耦合在一起,一個職責的變化可能會削弱或者抑制這個類完成其他職責的能力。這種耦合會導致脆弱的設計,當變化發(fā)生時,設計會遭到意想不到的破壞。

軟件設計真正要做的許多內(nèi)容,就是發(fā)現(xiàn)職責并把那些職責相互分離,其實要去判斷是否應該分離出來,也不難,那就是如果你能夠想到多余一個的動機去改變一個類,那么這個類就是對于一個的職責

在我們現(xiàn)實遇到的需求場景中,完全遵守單一職權原則也不是一件很好的事。比如我們在12306購票的下單的時候,需要對我們的身份信息做檢查,根據(jù)單一職權原則我們單獨編寫了一個對身份信息驗證。但是隨著產(chǎn)品體驗的優(yōu)化,需要在添加一個重復訂單的驗證,如果根據(jù)單一職權原則我們還要寫一個檢查重復訂單的類進行重復訂單的校驗。但是此時我們的代碼結構已經(jīng)定義好了,重新寫一個類,然后修改調(diào)用方法就顯得比較復雜,此時我們就可以對檢查類進行簡單的修改,編寫一個檢查方法,實現(xiàn)對身份檢查和重復訂單檢查的調(diào)用。此時我們的單一職權原則可以應用到我們的方法上。雖然這樣做對于類而言有悖于單一職權原則,但從下單前的校驗角度思考它有遵循于單一職權原則。(這樣做的風險在于職責擴散的不確定性,可能以后還需要做更多的檢查,所以記住,在職責擴散到我們無法控制的程度之前,立刻對代碼進行重構。可根據(jù)不同的檢查類型細分為不同的檢查類

遵循單一職責原的優(yōu)點有:

  • 可以降低類的復雜度,一個類只負責一項職責,其邏輯肯定要比負責多項職責簡單的多;
  • 提高類的可讀性,提高系統(tǒng)的可維護性;
  • 變更引起的風險降低,變更是必然的,如果單一職責原則遵守的好,當修改一個功能時,可以顯著降低對其他功能的影響。

需要說明的一點,單一職權原則并不是面向?qū)ο缶幊陶Z言特有的原則,只要是模塊化的程序設計,都適用單一職責原則。

里氏替換原則(Liskov Substitution Principle,LSP)

定義:子類型必須能夠替換掉他們的父類型。

對于里氏替換原則這個名稱不用太糾結,覺得苦澀難懂,其實是因為這項原則最早是在1988年,由麻省理工學院的一位姓里的女士(Barbara Liskov)提出來的,就是單純的一個名字。

如果把里氏替換原則翻譯成大白話就是一個軟件實體如果使用的是一個父類的話,那么一定適用于其子類,而且它察覺不出父類對象和子類對象的區(qū)別,也就是說在軟件里面把父類都替換成它的子類,程序的行為沒有變化

里氏替換原則主要對于繼承而言,B繼承A ,在B中添加新的方法的時候,盡量不要重寫A的方法,也盡量不要重載父類A的方法。

繼承作為面向?qū)ο笕筇匦灾唬诮o程序設計帶來巨大便利的同時,也帶來了弊端。比如使用繼承會給程序帶來侵入性,程序的可移植性降低,增加了對象間的耦合性,如果一個類被其他的類所繼承,則當這個類需要修改時,必須考慮到所有的子類,并且父類修改后,所有涉及到子類的功能都有可能會產(chǎ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));
    }
} 

運行結果:

100-50=50

100-80=20

后來,我們需要增加一個新的功能:完成兩數(shù)相加,然后再與100求和,由類B來負責。即類B需要完成兩個功能:

  • 兩數(shù)相減。
  • 兩數(shù)相加,然后再加100。
    由于類A已經(jīng)實現(xiàn)了第一個功能,所以類B繼承類A后,只需要再完成第二個功能就可以了,代碼如下:
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完成后,運行結果:

100-50=150

100-80=180

100+20+100=220

我們發(fā)現(xiàn)原本運行正常的相減功能發(fā)生了錯誤。原因就是類B在給方法起名時無意中重寫了父類的方法,造成所有運行相減功能的代碼全部調(diào)用了類B重寫后的方法,造成原本運行正常的功能出現(xiàn)了錯誤。在本例中,引用基類A完成的功能,換成子類B之后,發(fā)生了異常。在實際編程中,我們常常會通過重寫父類的方法來完成新的功能,這樣寫起來雖然簡單,但是整個繼承體系的可復用性會比較差,特別是運用多態(tài)比較頻繁時,程序運行出錯的幾率非常大。如果非要重寫父類的方法,比較通用的做法是:原來的父類和子類都繼承一個更通俗的基類,原有的繼承關系去掉,采用依賴、聚合,組合等關系代替。

里氏替換原則通俗的來講就是:子類可以擴展父類的功能,但不能改變父類原有的功能。它包含以下4層含義:

  • 子類可以實現(xiàn)父類的抽象方法,但不能覆蓋父類的非抽象方法。
  • 子類中可以增加自己特有的方法。
  • 當子類的方法重載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入?yún)?shù)更寬松。
  • 當子類的方法實現(xiàn)父類的抽象方法時,方法的后置條件(即方法的返回值)要比父類更嚴格。

依賴倒置原則(Dependence Inversion Principle)

定義:

  • 高層模塊不應該依賴底層模塊。兩個都應該依賴抽象。
  • 抽象不應該依賴細節(jié)。細節(jié)應該依賴抽象。

依賴倒置原則定義比較繞口,說白了就是針對接口編程,不要針對實現(xiàn)編程。

依賴倒置原則基于這樣一個事實:相對于細節(jié)的多變性,抽象的東西要穩(wěn)定的多。以抽象為基礎搭建起來的架構比以細節(jié)為基礎搭建起來的架構要穩(wěn)定的多。在java中,抽象指的是接口或者抽象類,細節(jié)就是具體的實現(xiàn)類,使用接口或者抽象類的目的是制定好規(guī)范和契約,而不去涉及任何具體的操作,把展現(xiàn)細節(jié)的任務交給他們的實現(xiàn)類去完成。

同樣我們舉個例子說明,雙十一即將來臨,商城搞滿減活動。

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("活動方式:滿300減100");
    }
}

運行輸出活動方式:滿300減100
過了一天,產(chǎn)品又提出一個打折的需求,但是如果實現(xiàn)就必須需要修改我們的活動類,以此類推,每次不同的活動都要去修改。這顯然不合理,ActivityDicount耦合性太高了,因此我們抽象一個優(yōu)惠類

public interface Reduce {
    void activityMode();
}

DiscountManJian都實現(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("活動方式:滿300減100");
    }
}

class Discount implements Reduce{

    @Override
    public void activityMode() {
        System.out.println("活動方式:打八折");
    }
}

輸出活動方式:滿300減100活動方式:打八折
這樣修改后無論怎么修改活動方式都不需要修改Activity類了

傳遞依賴關系有三種方式,以上的例子中使用的方法是接口傳遞,另外還有兩種傳遞方式:構造方法傳遞和setter方法傳遞,相信用過Spring框架的,對依賴的傳遞方式一定不會陌生。

在實際編程中,我們一般需要做到如下3點:

  • 低層模塊盡量都要有抽象類或接口,或者兩者都有。
  • 變量的聲明類型盡量是抽象類或接口。
  • 使用繼承時遵循里氏替換原則。

依賴倒置原則的核心就是要我們面向接口編程,理解了面向接口編程,也就理解了依賴倒置。

接口隔離原則(Interface Segregation Principle)

定義:客戶端不應該依賴它不需要的接口;一個類對另一個類的依賴應該建立在最小的接口上。
接口隔離原則簡單來說就是根據(jù)類的職責將接口進行更細粒度的拆分,使一個臃腫的接口分散成幾個接口,由實現(xiàn)者根據(jù)自身需求去分別實現(xiàn)。

舉個??:我們在封裝JDBC方法的時候會有單表查詢添加查詢分頁等等。如果我們封裝到一個接口里面,有些不需要這么多功能的類也要實現(xiàn)這些邏輯,就會造成代碼的臃腫。這里拿通用Mapper舉例

public interface SelectOneMapper<T> {

    /**
     * 根據(jù)實體中的屬性進行查詢,只能有一個返回值,有多個結果是拋出異常,查詢條件使用等號
     *
     * @param record
     * @return
     */
    @SelectProvider(type = BaseSelectProvider.class, method = "dynamicSQL")
    T selectOne(T record);

}

public interface SelectMapper<T> {

    /**
     * 根據(jù)實體中的屬性值進行查詢,查詢條件使用等號
     *
     * @param record
     * @return
     */
    @SelectProvider(type = BaseSelectProvider.class, method = "dynamicSQL")
    List<T> select(T record);

}

public interface SelectAllMapper<T> {

    /**
     * 查詢?nèi)拷Y果
     *
     * @return
     */
    @SelectProvider(type = BaseSelectProvider.class, method = "dynamicSQL")
    List<T> selectAll();
}

將每一種查詢都封裝成一個方法,然后寫一個通用的接口

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ù)不同的需要進行選擇性繼承相應功能的接口就可以實現(xiàn)符合我們需要的接口。

接口隔離原則的含義是:建立單一接口,不要建立龐大臃腫的接口,盡量細化接口,接口中的方法盡量少。也就是說,我們要為各個類建立專用的接口,而不要試圖去建立一個很龐大的接口供所有依賴它的類去調(diào)用。本文例子中,將一個龐大的接口變更為3個專用的接口所采用的就是接口隔離原則。在程序設計中,依賴幾個專用的接口要比依賴一個綜合的接口更靈活。接口是設計時對外部設定的“契約”,通過分散定義多個接口,可以預防外來變更的擴散,提高系統(tǒng)的靈活性和可維護性。

說到這里,很多人會覺的接口隔離原則跟之前的單一職責原則很相似,其實不然。其一,單一職責原則原注重的是職責;而接口隔離原則注重對接口依賴的隔離。其二,單一職責原則主要是約束類,其次才是接口和方法,它針對的是程序中的實現(xiàn)和細節(jié);而接口隔離原則主要約束接口接口,主要針對抽象,針對程序整體框架的構建。

采用接口隔離原則對接口進行約束時,要注意以下幾點:

  • 接口盡量小,但是要有限度。對接口進行細化可以提高程序設計靈活性是不掙的事實,但是如果過小,則會造成接口數(shù)量過多,使設計復雜化。所以一定要適度。
  • 為依賴接口的類定制服務,只暴露給調(diào)用的類它需要的方法,它不需要的方法則隱藏起來。只有專注地為一個模塊提供定制服務,才能建立最小的依賴關系。
  • 提高內(nèi)聚,減少對外交互。使接口用最少的方法去完成最多的事情。

迪米特法則(Law Of Demeter)

定義:如果兩個類不必彼此直接通信,那么這兩個類就不應當發(fā)生直接的相互作用,如果其中一個類需要調(diào)用另一個類的某一個方法的話,可以通過第三者轉(zhuǎn)發(fā)這個調(diào)用。

迪米特法則也叫最少知識原則。強調(diào)的是一個對象應該對其他對象保持做少的了解,在類的結構設計上,每一個類都應當盡量降低成員的訪問權限,也就是說,一個類包裝好自己的private狀態(tài),不需要讓別的類知道的字段或行為就不要公開。

迪米特法則其根本思想,是強調(diào)了類之間的松耦合,類之間耦合越弱,越利于復用,一個處在弱耦合的類被修改,不會對有關系的類造成波及。

迪米特法則的初衷是降低類之間的耦合,由于每個類都減少了不必要的依賴,因此的確可以降低耦合關系。但是凡事都有度,雖然可以避免與非直接的類通信,但是要通信,必然會通過一個“中介”來發(fā)生聯(lián)系。過分的使用迪米特原則,會產(chǎn)生大量這樣的中介和傳遞類,導致系統(tǒng)復雜度變大。所以在采用迪米特法則時要反復權衡,既做到結構清晰,又要高內(nèi)聚低耦合。

開閉原則

定義:軟件實體(類、模塊、函數(shù)等等)應該可以擴展,但是不可修改。

開閉原則有兩個特征
1.對擴展是開放的(Open for extension)
2.對更改是封閉的(Closed for modification)

我們在做任何系統(tǒng)的時候,都不可能一開始指定需求就不在發(fā)生變化,但是每次需求的變化都會引起對原有代碼的修改,很有可能會給舊的代碼引入錯誤,也可能會使我們不得不對整個功能進行重構,并且還要測試一遍原有的代碼。

絕對的對修改關閉是不現(xiàn)實的,這就要求設計人員必須對于他設計的代碼應該應對那種變化封閉做出選擇。他必須先猜測出來最有可能變化的種類,然后構造抽象來隔離那些變化。但是我們是很難進行預先的猜測,這樣要求我們等到變化發(fā)生時立即采取行動,當發(fā)生變化時,我們就創(chuàng)建抽象來隔離以后發(fā)生的同類變化

開閉原則是面向?qū)ο笤O計的核心所在,遵循這個原則可以帶來面向?qū)ο蠹夹g所聲稱的巨大好處,也就是可維護、可擴展、可復用、靈活性好。

其實,我們遵循設計模式前面5大原則,以及使用23種設計模式的目的就是遵循開閉原則。也就是說,只要我們對前面5項原則遵守的好了,設計出的軟件自然是符合開閉原則的,這個開閉原則更像是前面五項原則遵守程度的“平均得分”,前面5項原則遵守的好,平均分自然就高,說明軟件設計開閉原則遵守的好;如果前面5項原則遵守的不好,則說明開閉原則遵守的不好。

再回想一下前面說的5項原則,恰恰是告訴我們用抽象構建框架,用實現(xiàn)擴展細節(jié)的注意事項而已:單一職責原則告訴我們實現(xiàn)類要職責單一;里氏替換原則告訴我們不要破壞繼承體系;依賴倒置原則告訴我們要面向接口編程;接口隔離原則告訴我們在設計接口的時候要精簡單一;迪米特法則告訴我們要降低耦合。而開閉原則是總綱,他告訴我們要對擴展開放,對修改關閉。

最后說明一下如何去遵守這六個原則。對這六個原則的遵守并不是是和否的問題,而是多和少的問題,也就是說,我們一般不會說有沒有遵守,而是說遵守程度的多少。任何事都是過猶不及,設計模式的六個設計原則也是一樣,制定這六個原則的目的并不是要我們刻板的遵守他們,而需要根據(jù)實際情況靈活運用。對他們的遵守程度只要在一個合理的范圍內(nèi),就算是良好的設計。我們用一幅圖來說明一下。


圖片來源網(wǎng)絡

圖中的每一條維度各代表一項原則,我們依據(jù)對這項原則的遵守程度在維度上畫一個點,則如果對這項原則遵守的合理的話,這個點應該落在紅色的同心圓內(nèi)部;如果遵守的差,點將會在小圓內(nèi)部;如果過度遵守,點將會落在大圓外部。一個良好的設計體現(xiàn)在圖中,應該是六個頂點都在同心圓中的六邊形。

圖片來源網(wǎng)絡

在上圖中,設計1、設計2屬于良好的設計,他們對六項原則的遵守程度都在合理的范圍內(nèi);設計3、設計4設計雖然有些不足,但也基本可以接受;設計5則嚴重不足,對各項原則都沒有很好的遵守;而設計6則遵守過渡了,設計5和設計6都是迫切需要重構的設計。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內(nèi)容

  • 設計模式概述 在學習面向?qū)ο笃叽笤O計原則時需要注意以下幾點:a) 高內(nèi)聚、低耦合和單一職能的“沖突”實際上,這兩者...
    彥幀閱讀 3,771評論 0 14
  • 轉(zhuǎn)載自 設計模式六大原則[http://www.uml.org.cn/sjms/201211023.asp#3] ...
    廚子閱讀 1,108評論 2 5
  • 目錄: 設計模式六大原則(1):單一職責原則 設計模式六大原則(2):里氏替換原則 設計模式六大原則(3):依賴倒...
    加油小杜閱讀 736評論 0 1
  • 當看到離上班遲到只有半個小時,你是否還躺在床上起不來? 當臟衣服都堆滿房間角落,你是否還在猶豫著等有時間了再洗? ...
    李白馬閱讀 183評論 1 2
  • 下午會議結束會,老王默默地遞給我兩張月總結,“來,看看這個”我拿過來仔細端詳一番,也只能搖搖頭。“看吧,這兩個人就...
    冬眠小熊閱讀 352評論 3 2