Java設計模式學習筆記(一) 設計模式概述

前言

大約在一年前學習過一段時間的設計模式,可是當時本身的學習方式比較低效,也沒有深入的去理解、運用所學的知識.html

因此如今準備系統的再從新學習一遍,寫一個關於設計模式的系列博客.編程

廢話很少說,正文開始.設計模式

1. 設計模式是什麼

設計模式是一套被反覆使用、多數人知曉的、通過分類編目的、代碼設計經驗的總結,使用設計模式是爲了可重用代碼、讓代碼更容易被他人理解而且保證代碼可靠性.框架

2. 學習設計模式的好處

  1. 提升開發效率,使用設計模式能夠避免咱們作一些重複工做
  2. 減小開發人員的溝通成本.
  3. 閱讀源碼,更深刻的理解使用的框架和類庫
  4. 本身寫出靈活性高、易維護、易擴展和易複用的代碼

3. 設計模式的分類

根據用途設計模式可分爲建立型、結構型和行爲型三種編程語言

3.1 建立型設計模式

建立型模式是處理對象建立的設計模式,試圖根據實際狀況使用合適的方式建立對象。ide

5種建立型設計模式:函數

  • 簡單工廠模式
  • 工廠方法模式
  • 單例模式
  • 原型模式
  • 建造者模式

3.2 結構型設計模式

結構型設計模式是藉由一以貫之的方式來了解元件間的關係,以簡化設計.學習

一以貫之: 指作人作事,按照一個道理,從一而終,出自《論語·里仁》。優化

注: 以上是百科的解釋,一以貫之是我本身百度貼上的,又能學到技術又能學到成語,看這篇博客賺翻了有沒有.this

七種結構型設計模式:

  • 適配器模式
  • 橋接模式
  • 組合模式
  • 裝飾者模式
  • 外觀模式
  • 享元模式
  • 代理模式

3.3 行爲型設計模式

行爲型設計模式是用來識別對象之間的經常使用交流模式並加以實現。如此,可在進行這些交流活動時加強彈性.

十一種行爲型設計模式:

  • 職責鏈模式
  • 命令模式
  • 解釋器模式
  • 迭代器模式
  • 中介者模式
  • 備忘錄模式
  • 觀察者模式
  • 狀態模式
  • 策略模式
  • 模板方法模式
  • 訪問者模式

4. 學習設計模式的一些其餘準備工做

學習設計模式還須要一些其餘的知識儲備,例如:

  • UML類圖相關知識(部分示例使用UML類圖演示,如沒有相關知識,請移步個人上一篇博客 UML類圖簡介)
  • 瞭解面向對象設計原則

5. 面向對象設計原則

面向對象設計原則是從設計模式中總結出來的指導性原則,也就是說設計模式遵循了面向對象設計原則.咱們平時在開發軟件的時刻也要儘可能遵循面向對象設計原則進行開發.

面向對象設計原則爲支持可維護性複用而誕生.

最多見的七種面向對象設計原則:

  • 單一職責
  • 開閉原則
  • 里氏代換原則
  • 依賴倒轉原則
  • 接口隔離原則
  • 合成複用原則
  • 迪米特法則

5.1 單一職責

定義: 一個類只負責一個功能領域中的相應職責,或者能夠定義爲: 就一個類而言,應該只有一個引發變化的緣由.

使用單一職責的緣由: 若是一個類承擔的職責太多,它被複用的可能性就越小,並且一個類承擔的職責過多,就至關於將這些職責耦合在一塊兒,當其中一個職責變化時,可能影響其餘職責的運做,所以要將這些職責分離.將不一樣的職責封裝在不一樣的類中.(若是多個職責老是同時發生改變則能夠將他們封裝在同一個類中)

單一職責原則是實現高內聚、低耦合的指導方針.

內聚: 內聚是從功能角度來度量模塊內的聯繫,一個好的內聚模塊應當剛好作一件事。它描述的是模塊內的功能聯繫.

耦合: 耦合是軟件結構中各模塊之間相互鏈接的一種度量,耦合強弱取決於模塊間接口的複雜程度、進入或訪問一個模塊的點以及經過接口的數據。

示例: 有一個汽車的類,有幾個方法分別是開門、關門、前進、後退、修車、維護、洗車的功能

按照單一職責的定義一個類只負責一個功能領域中的相應職責,咱們能夠對汽車這個類進行優化,將修車、維護、洗車的工做抽離到修車廠的類中.

優化後的類:

5.2 開閉原則

開閉原則是面向對象的可複用設計的第一基石,它是最重要的面向對象設計原則.

定義: 一個軟件實體應當對擴展開放,對修改關閉.即軟件實體應該儘可能在不修改原有代碼的狀況下進行擴展.

爲了知足開閉原則,須要對系統進行抽象化設計,抽象化是開閉原則的關鍵.使用接口、抽象類定義系統的抽相層,再經過具體類來進行擴展.

若是須要修改系統的行爲,無須對抽象層進行任何改動,只須要增長新的具體類來實現新的業務功能便可,實如今不修改已有代碼的基礎上擴展系統的功能,達到開閉原則的要求.

示例: 超市舉辦促銷活動,打折策略是滿200打八折.咱們來看看打折策略的設計

代碼:

/**
 * @author liuboren
 * @Title: 打折策略類
 * @Description: 具體的打折實現
 * @date 2019/7/11 14:39
 */
public class DiscountStrategy {

    /*
    * 消費超過200,打八折
    * */
    public Double strategy(Double money){
        if(money > 200){
            money = money * 0.8;
        }
        return money;
    }
}



/**
 * @author liuboren
 * @Title:結帳功能
 * @Description: 使用打折策略結帳
 * @date 2019/7/11 14:37
 */
public class SettleAccounts {

    public Double Buy(Double money,DiscountStrategy strategy){
        //返回打折後的金額
        return strategy.strategy(money);
    }
}

一切都看上去很完美,可是過了幾個月,超市決定換一種打折策略,消費滿500立減200.

這時候若是直接去改打折策略的類,就違反了開閉原則,並且若是過幾天打折策略又要還回去,或者同時增長新的打折策略,也沒有辦法很好的擴展.

實現開閉原則的關鍵在於面向接口編程,咱們來更改一下代碼.

新增打折策略接口,結帳類使用接口進行結算:

/**
 * @author liuboren
 * @Title: 打折接口
 * @Description: 聲明打折方法,具體有實現類去實現
 * @date 2019/7/11 14:48
 */
public interface DiscountStrategyInterface {

    //打折策略
    Double strategy(Double money);
}



/**
 * @author liuboren
 * @Title: 滿200打八折實現類
 * @Description: 具體的打折實現
 * @date 2019/7/11 14:39
 */
public class TwentyPercentStrategy implements DiscountStrategyInterface{

    /*
    * 消費超過200,打八折
    * */
    @Override
    public Double strategy(Double money){
        if(money > 200){
            money = money * 0.8;
        }
        return money;
    }
}



/**
 * @author liuboren
 * @Title:結帳功能
 * @Description: 使用打折策略結帳
 * @date 2019/7/11 14:37
 */
public class SettleAccounts {

    
    public Double Buy(Double money,DiscountStrategyInterface strategy){
        //返回打折後的金額
        return strategy.strategy(money);
    }


  public static void main(String[] args) {
        SettleAccounts settleAccounts = new SettleAccounts();
        /*
        * 這樣很靈活,有新的打折策略的時候,只須要添加新的實現類,
        * 並傳入購買方法,開閉原則獲得了很好的實現
        * */
        settleAccounts.Buy(300d,new TwentyPercentStrategy());
    }

}

修改後咱們的代碼在增長新的打折策略的時候變得很容易擴展,並且還不須要修改原來的類了.

5.3 里氏代換原則

定義: 全部引用基類(父類)的地方必須能透明地使用其子類的對象.

里氏代換原則告訴咱們,在軟件中將一個基類對象替換成它的子類對象時,程序將不會產生任何錯誤和異常,反過來則不成立.

里氏代換原則是實現開閉原則的重要方式之一,因爲使用基類對象的地方均可以使用子類對象,所以在程序中儘可能使用基類類型來對對象定義,而在運行時再肯定其子類類型,用子類對象來替換基類對象.

使用里氏代換原則須要注意的問題:

  1. 子類的全部方法必須在父類中聲明,或子類必須實現父類中聲明的全部方法.根據里氏代換原則,爲了保證系統的擴展性,在程序中一般使用父類來進行定義,若是一個方法只存在子類中,在父類中不提供相應的聲明,則沒法在父類定義的對象中使用該方法.

  2. 儘可能把父類設計爲抽象類或接口,讓子類繼承父類或實現父接口,並實如今父類中聲明的方法,運行時,子類實例替換父類實例,咱們能夠很方便地擴展系統的功能,同時無須修改原有子類的代碼,增長新的功能能夠經過增長一個新的子類來實現.里氏代換原則是開閉原則的具體實現之一

實例: 還看超市的例子

/**
 * @author liuboren
 * @Title:結帳功能
 * @Description: 使用打折策略結帳
 * @date 2019/7/11 14:37
 */
public class SettleAccounts {

    public Double Buy(Double money,DiscountStrategyInterface strategy){
        //返回打折後的金額
        return strategy.strategy(money);
    }

    public static void main(String[] args) {
        SettleAccounts settleAccounts = new SettleAccounts();
        /*
        * 這樣很靈活,有新的打折策略的時候,只須要添加新的實現類,
        * 並傳入購買方法,開閉原則獲得了很好的實現
        * */
        settleAccounts.Buy(300d,new TwentyPercentStrategy());
    }


}

Buy方法使用的參數是DiscountStrategyInterface 接口,可是在main方法使用的是其子類,這就是父類出現的地方均可以被子類替換的里氏代換原則.

5.4 依賴倒轉原則

若是說開閉原則是面向對象設計的目標的話,那麼依賴倒轉原則就是面向對象設計的主要實現機制之一,它是系統抽象化的具體實現.

定義: 抽象不該該依賴於細節,細節應該依賴於抽象.換言之,要針對接口編程,而不是針對實現編程.

依賴倒轉原則要求咱們在程序代碼中傳遞參數時或在關聯關係中,儘可能引用層次高的抽象層類,即便用接口和抽象類進行變量類型聲明、參數類型聲明、方法返回類型聲明,以及數據類型的轉換等,而不要用具體類來作這些事情.

爲了確保該原則的應用,一個具體類應當只實現接口或者抽象類中聲明過的方法,而不要給出多餘的方法,不然將沒法調用到在子類中增長的新方法.

示例: 同上面..一句話面對接口編程.

5.5 接口隔離原則

定義: 使用多個專門的接口,而不使用單一的總接口,即客戶端不該該依賴那些它不須要的接口.

根據接口隔離原則,當一個接口太大時咱們須要將它分割成一些更細小的接口,使用該接口的客戶端僅需知道與之相關的方法便可. 每一個接口都應該承擔一種相對獨立的角色,不應乾的事不幹,該乾的事都要幹.

接口有兩種含義,一種是指一個類型所具備的方法特徵的集合,僅僅是一種邏輯上的抽象例如上面的Animal接口;另外一種是值某種語言具體的"接口"定義,有嚴格的定義和機構,好比Java語言中的interface;對這兩種不一樣的含義,接口隔離原則的表達方式以及含義都有所不一樣:

  1. 把"接口"理解成一個類型所提供的的全部方法特徵的集合的時候,這就是一種邏輯上的概念,接口的劃分將直接帶來類型的劃分.能夠把接口理解成角色,一個接口只能表明一個角色,每一個角色都有它特定的一個接口,此時,這個原則能夠叫作"角色隔離原則".例如動物能夠抽象成一個接口,接口封裝動物的一些特性和行爲.

  2. 若是把"接口"理解成狹義的特定語言的接口,那麼接口隔離原則的意思是指接口僅僅提供客戶端須要的行爲,客戶端不須要的行爲則隱藏起來.

應當爲客戶提供儘量小的單獨的接口,而不要提供大的總接口.

在面向對象編程語言中,實現一個接口就須要實現該接口中定義的全部方法,所以大的總接口使用起來不必定很方便,爲了使接口的職責單一,須要將大接口中的方法根據其職責不一樣分別放在不一樣的小接口中,以確保每一個接口使用起來都較爲方便,並都承擔某一單一角色.

接口應該儘可能細化,同時接口中的方法應該儘可能少,每一個接口中只包含一個客戶端所需的方法便可.和單一職責有殊途同歸之妙.

5.6 合成複用原則/聚合複用原則

定義: 儘可能使用對象組合,而不是繼承來達到複用的目的.

合成複用原則就是在一個新的對象裏經過關聯關係(包括組合關係和聚合關係)來使用一些已有的對象,使之成爲新對象的一部分;新對象經過委派調用已有對象的方法達到複用功能的目的.

簡言之: 複用時要儘可能使用組合/聚合關係(關聯關係),少用繼承.

組合/聚合和繼承均可以複用已有的設計和實現,可是應該優先考慮使用組合/聚合.由於組合/聚合可使系統更加靈活,下降類與類之間的耦合度,一個類的變化對其餘類形成的影響相對較少.其次再考慮繼承,在使用繼承時,須要嚴格遵循里氏代換原則,有效使用繼承有助於對問題的理解,下降複雜度,而濫用繼承反而會增長系統構建和維護的難度以及系統的複雜度,所以須要慎重使用繼承複用.

繼承的壞處:

  1. 經過繼承來複用的主要問題在於繼承複用會破壞系統的封裝性.由於繼承會將基類的實現細節暴露給子類,因爲基類的內部細節對子類來講是可見的,因此這種複用又稱"白箱"複用,若是基類發生改變,那麼子類的實現也不得不發生改變.

  2. 從基類繼承而來的實現是靜態的,不可能在運行時發生改變,沒有足夠的靈活性.

  3. 類沒有聲明final才能被繼承,使用條件有限.

組合/聚合的好處:

  1. 組合/聚合將已有對象歸入新對象中,使之成爲新對象的一部分,所以新對象能夠調用已有對象的功能.這樣作可使得成員對象的內部實現細節對於新對象不可見.因此這種複用又稱爲"黑箱"複用,相對於繼承而言,其耦合度相對較低,成員對象的變化對新對象的影響不大,能夠再新對象中根據實際須要有選擇性的調用成員對象的方法.

  2. 合成複用能夠在運行時動態進行,新對象能夠動態地引用與成員對象類型相同的其餘對象.

繼承和組合/聚合的選擇: 像以前超市打折的例子中,能夠提升程序的靈活性才使用繼承/實現,不然優先使用組合.

5.7 迪米特法則

定義: 一個軟件實體應當儘量少地與其餘實體發生相互做用.

若是一個系統符合迪米特法則,那麼當其中某一個模塊發生修改時,就會盡可能少的影響其餘模塊,擴展會相對容易,這是對軟件實體之間通訊的限制,迪米特法則要求限制軟件實體時間通訊的寬度和深度.迪米特法則能夠下降系統的耦合度,使類與類之間保持鬆散的耦合關係.

迪米特法則要求對象只與朋友通訊,"不要和陌生人說話",朋友包括如下幾類:

  1. 當前對象自身(this);

  2. 以參數形式傳入到當前對象方法中的對象;

  3. 當前對象的成員對象;

  4. 若是當前對象的成員對象是一個集合,那麼集合中的元素也都是朋友;

  5. 當前對象所建立的對象.

在應用迪米特法則時,一個對象只能與直接朋友發生交互,不要與"陌生人"發生直接交互,這樣作能夠下降系統的耦合度,一個對象的改變不會給太多其餘對象帶來影響.

迪米特法則要求咱們在設計系統時,應當儘可能減小對象之間的交互,若是兩個對象之間沒必要彼此直接通訊,那麼這兩個對象就不該該發生任何直接的相互做用,若是其中的一個對象須要調用另外一個對象的某一個方法,能夠經過第三者轉發這個調用.

簡言之,就是經過引入一個合理的第三者來下降現有對象之間的耦合度.

在將迪米特法則運用到系統設計中時,要注意下面的幾點:

  1. 在類的劃分上,應當儘可能建立鬆耦合的類,類之間的耦合度越低,就越有利於複用,一個處在鬆耦合中的類一旦被修改,不會對關聯的類形成太大波及.

  2. 在類的設計結構上,每個類都應當儘可能下降其成員變量和成員函數的訪問權限.

  3. 在類的設計上,只要有可能,一個類型應當設計成不變類.

  4. 在對其餘類的引用上,一個對象對其餘對象的引用應當降到最低.

示例: 有一個客戶關係管理系統包含不少業務操做窗口,某些界面控件之間存在複雜的交互關係,一個控件事件的觸發將致使不少其餘界面產生響應.

例如,當一個按鈕(button)被單擊時,對應的列表框(List)、組合框(ComboBox)、文本框(TextBox)、文本標籤(Label)等都將發生改變.

因爲界面空間之間的交互關係複雜,致使在該窗口增長新的界面控件時須要修改與之交互的其餘控件的源代碼,系統擴展性較差,也不便於增長和刪除新控件.

改良方法:

引入一個專門用於控制控件交互的中間類(Mediator)來下降界面控件的耦合度.

引入中間類後,界面控件之間再也不發生直接引用,而是將請求先轉發給中間類,再有中間類來完成對其餘控件的調用.

當須要增長或刪除新的控件時,只需修改中間類便可,無須修改新控件或已有控件的代碼.

6. 主要參考文獻

大話設計模式

Java設計模式

相關文章
相關標籤/搜索