設計模式 - 概述

在軟件工程中,設計模式(design pattern)是對軟件設計中廣泛存在的各類問題,所提出的解決方案。設計模式並非固定的一套代碼,而是針對某一特定問題的具體解決思路與方案。能夠認爲是一種最佳實踐,由於他是無數軟件開發人員通過長時間的實踐總結出來的。 在瞭解設計模式以前就咱們首先要了解一下面向對象的六大原則。java

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

定義:就一個類而言,應該僅有一個引發它變化的緣由編程

若是一個類承擔的職責過多,就等於把這些職責耦合在一塊兒,一個職責的變化可能會削弱或者抑制這個類完成其餘職責的能力。這種耦合會致使脆弱的設計,當變化發生時,設計會遭到意想不到的破壞。設計模式

軟件設計真正要作的許多內容,就是發現職責並把那些職責相互分離,其實要去判斷是否應該分離出來,也不難,那就是若是你可以想到多餘一個的動機去改變一個類,那麼這個類就是對於一個的職責bash

在咱們現實遇到的需求場景中,徹底遵照單一職權原則也不是一件很好的事。好比咱們在12306購票的下單的時候,須要對咱們的身份信息作檢查,根據單一職權原則咱們單獨編寫了一個對身份信息驗證。可是隨着產品體驗的優化,須要在添加一個重複訂單的驗證,若是根據單一職權原則咱們還要寫一個檢查重複訂單的類進行重複訂單的校驗。可是此時咱們的代碼結構已經定義好了,從新寫一個類,而後修改調用方法就顯得比較複雜,此時咱們就能夠對檢查類進行簡單的修改,編寫一個檢查方法,實現對身份檢查和重複訂單檢查的調用。此時咱們的單一職權原則能夠應用到咱們的方法上。雖然這樣作對於類而言有悖於單一職權原則,但從下單前的校驗角度思考它有遵循於單一職權原則。(這樣作的風險在於職責擴散的不肯定性,可能之後還須要作更多的檢查,因此記住,在職責擴散到咱們沒法控制的程度以前,馬上對代碼進行重構。可根據不一樣的檢查類型細分爲不一樣的檢查類網絡

遵循單一職責原的優勢有:架構

  • 能夠下降類的複雜度,一個類只負責一項職責,其邏輯確定要比負責多項職責簡單的多;
  • 提升類的可讀性,提升系統的可維護性;
  • 變動引發的風險下降,變動是必然的,若是單一職責原則遵照的好,當修改一個功能時,能夠顯著下降對其餘功能的影響。

須要說明的一點,單一職權原則並非面向對象編程語言特有的原則,只要是模塊化的程序設計,都適用單一職責原則。app

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

定義:子類型必須可以替換掉他們的父類型。框架

對於里氏替換原則這個名稱不用太糾結,以爲苦澀難懂,實際上是由於這項原則最先是在1988年,由麻省理工學院的一位姓裏的女士(Barbara Liskov)提出來的,就是單純的一個名字。編程語言

若是把里氏替換原則翻譯成大白話就是一個軟件實體若是使用的是一個父類的話,那麼必定適用於其子類,並且它察覺不出父類對象和子類對象的區別,也就是說在軟件裏面把父類都替換成它的子類,程序的行爲沒有變化ide

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

繼承做爲面向對象三大特性之一,在給程序設計帶來巨大便利的同時,也帶來了弊端。好比使用繼承會給程序帶來侵入性,程序的可移植性下降,增長了對象間的耦合性,若是一個類被其餘的類所繼承,則當這個類須要修改時,必須考慮到全部的子類,而且父類修改後,全部涉及到子類的功能都有可能會產生故障。 舉例說一下集成的風險

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

後來,咱們須要增長一個新的功能:完成兩數相加,而後再與100求和,由類B來負責。即類B須要完成兩個功能:

  • 兩數相減。
  • 兩數相加,而後再加100。 因爲類A已經實現了第一個功能,因此類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

咱們發現本來運行正常的相減功能發生了錯誤。緣由就是類B在給方法起名時無心中重寫了父類的方法,形成全部運行相減功能的代碼所有調用了類B重寫後的方法,形成本來運行正常的功能出現了錯誤。在本例中,引用基類A完成的功能,換成子類B以後,發生了異常。在實際編程中,咱們經常會經過重寫父類的方法來完成新的功能,這樣寫起來雖然簡單,可是整個繼承體系的可複用性會比較差,特別是運用多態比較頻繁時,程序運行出錯的概率很是大。若是非要重寫父類的方法,比較通用的作法是:原來的父類和子類都繼承一個更通俗的基類,原有的繼承關係去掉,採用依賴、聚合,組合等關係代替。

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

  • 子類能夠實現父類的抽象方法,但不能覆蓋父類的非抽象方法。
  • 子類中能夠增長本身特有的方法。
  • 當子類的方法重載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬鬆。
  • 當子類的方法實現父類的抽象方法時,方法的後置條件(即方法的返回值)要比父類更嚴格。

依賴倒置原則(Dependence Inversion Principle)

定義:

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

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

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

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

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 過了一天,產品又提出一個打折的需求,可是若是實現就必須須要修改咱們的活動類,以此類推,每次不一樣的活動都要去修改。這顯然不合理,ActivityDicount耦合性過高了,所以咱們抽象一個優惠類

public interface Reduce {
    void activityMode();
}
複製代碼

DiscountManJian都實現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)

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

舉個🌰:咱們在封裝JDBC方法的時候會有單表查詢添加查詢分頁等等。若是咱們封裝到一個接口裏面,有些不須要這麼多功能的類也要實現這些邏輯,就會形成代碼的臃腫。這裏拿通用Mapper舉例

public interface SelectOneMapper<T> {

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

}

public interface SelectMapper<T> {

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

}

public interface SelectAllMapper<T> {

    /**
     * 查詢所有結果
     *
     * @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> {

}
複製代碼

這樣咱們就能夠根據不一樣的須要進行選擇性繼承相應功能的接口就能夠實現符合咱們須要的接口。

接口隔離原則的含義是:創建單一接口,不要創建龐大臃腫的接口,儘可能細化接口,接口中的方法儘可能少。也就是說,咱們要爲各個類創建專用的接口,而不要試圖去創建一個很龐大的接口供全部依賴它的類去調用。本文例子中,將一個龐大的接口變動爲3個專用的接口所採用的就是接口隔離原則。在程序設計中,依賴幾個專用的接口要比依賴一個綜合的接口更靈活。接口是設計時對外部設定的「契約」,經過分散定義多個接口,能夠預防外來變動的擴散,提升系統的靈活性和可維護性。

說到這裏,不少人會覺的接口隔離原則跟以前的單一職責原則很類似,其實否則。其一,單一職責原則原注重的是職責;而接口隔離原則注重對接口依賴的隔離。其二,單一職責原則主要是約束類,其次纔是接口和方法,它針對的是程序中的實現和細節;而接口隔離原則主要約束接口接口,主要針對抽象,針對程序總體框架的構建。

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

  • 接口儘可能小,可是要有限度。對接口進行細化能夠提升程序設計靈活性是不掙的事實,可是若是太小,則會形成接口數量過多,使設計複雜化。因此必定要適度。
  • 爲依賴接口的類定製服務,只暴露給調用的類它須要的方法,它不須要的方法則隱藏起來。只有專一地爲一個模塊提供定製服務,才能創建最小的依賴關係。
  • 提升內聚,減小對外交互。使接口用最少的方法去完成最多的事情。

迪米特法則(Law Of Demeter)

定義:若是兩個類沒必要彼此直接通訊,那麼這兩個類就不該當發生直接的相互做用,若是其中一個類須要調用另外一個類的某一個方法的話,能夠經過第三者轉發這個調用。

迪米特法則也叫最少知識原則。強調的是一個對象應該對其餘對象保持作少的瞭解,在類的結構設計上,每個類都應當儘可能下降成員的訪問權限,也就是說,一個類包裝好本身的private狀態,不須要讓別的類知道的字段或行爲就不要公開。

迪米特法則其根本思想,是強調了類之間的鬆耦合,類之間耦合越弱,越利於複用,一個處在弱耦合的類被修改,不會對有關係的類形成波及。

迪米特法則的初衷是下降類之間的耦合,因爲每一個類都減小了沒必要要的依賴,所以的確能夠下降耦合關係。可是凡事都有度,雖然能夠避免與非直接的類通訊,可是要通訊,必然會經過一個「中介」來發生聯繫。過度的使用迪米特原則,會產生大量這樣的中介和傳遞類,致使系統複雜度變大。因此在採用迪米特法則時要反覆權衡,既作到結構清晰,又要高內聚低耦合。

開閉原則

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

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

咱們在作任何系統的時候,都不可能一開始指定需求就不在發生變化,可是每次需求的變化都會引發對原有代碼的修改,頗有可能會給舊的代碼引入錯誤,也可能會使咱們不得不對整個功能進行重構,而且還要測試一遍原有的代碼。

絕對的對修改關閉是不現實的,這就要求設計人員必須對於他設計的代碼應該應對那種變化封閉作出選擇。他必須先猜想出來最有可能變化的種類,而後構造抽象來隔離那些變化。可是咱們是很難進行預先的猜想,這樣要求咱們等到變化發生時當即採起行動,當發生變化時,咱們就建立抽象來隔離之後發生的同類變化

開閉原則是面向對象設計的核心所在,遵循這個原則能夠帶來面向對象技術所聲稱的巨大好處,也就是可維護、可擴展、可複用、靈活性好。

其實,咱們遵循設計模式前面5大原則,以及使用23種設計模式的目的就是遵循開閉原則。也就是說,只要咱們對前面5項原則遵照的好了,設計出的軟件天然是符合開閉原則的,這個開閉原則更像是前面五項原則遵照程度的「平均得分」,前面5項原則遵照的好,平均分天然就高,說明軟件設計開閉原則遵照的好;若是前面5項原則遵照的很差,則說明開閉原則遵照的很差。

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

最後說明一下如何去遵照這六個原則。對這六個原則的遵照並非是和否的問題,而是多和少的問題,也就是說,咱們通常不會說有沒有遵照,而是說遵照程度的多少。任何事都是過猶不及,設計模式的六個設計原則也是同樣,制定這六個原則的目的並非要咱們刻板的遵照他們,而須要根據實際狀況靈活運用。對他們的遵照程度只要在一個合理的範圍內,就算是良好的設計。咱們用一幅圖來講明一下。

圖片來源網絡

圖中的每一條維度各表明一項原則,咱們依據對這項原則的遵照程度在維度上畫一個點,則若是對這項原則遵照的合理的話,這個點應該落在紅色的同心圓內部;若是遵照的差,點將會在小圓內部;若是過分遵照,點將會落在大圓外部。一個良好的設計體如今圖中,應該是六個頂點都在同心圓中的六邊形。

圖片來源網絡

在上圖中,設計一、設計2屬於良好的設計,他們對六項原則的遵照程度都在合理的範圍內;設計三、設計4設計雖然有些不足,但也基本能夠接受;設計5則嚴重不足,對各項原則都沒有很好的遵照;而設計6則遵照過渡了,設計5和設計6都是迫切須要重構的設計。

相關文章
相關標籤/搜索