1. 程式人生 > >Java設計模式學習筆記(一) 設計模式概述

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

前言

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

所以現在準備系統的再重新學習一遍,寫一個關於設計模式的系列部落格.

廢話不多說,正文開始.

1. 設計模式是什麼

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

2. 學習設計模式的好處

  1. 提高開發效率,使用設計模式可以避免我們做一些重複工作
  2. 減少開發人員的溝通成本.
  3. 閱讀原始碼,更深入的理解使用的框架和類庫
  4. 自己寫出靈活性高、易維護、易擴充套件和易複用的程式碼

3. 設計模式的分類

根據用途設計模式可分為建立型、結構型和行為型三種

3.1 建立型設計模式

建立型模式是處理物件建立的設計模式,試圖根據實際情況使用合適的方式建立物件。

5種建立型設計模式:

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

3.2 結構型設計模式

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

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

注: 以上是百科的解釋,一以貫之是我自己百度貼上的,又能學到技術又能學到成語,看這篇部落格賺翻了有沒有.

七種結構型設計模式:

  • 介面卡模式
  • 橋接模式
  • 組合模式
  • 裝飾者模式
  • 外觀模式
  • 享元模式
  • 代理模式

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設計模