設計模式初探

什麼是設計模式?

Q一、設計模式的產生背景?

其實,」設計模式「這個術語最先並不是出如今軟件設計中,而是被應用於建築領域的設計中。java

早在1977 年的時候,美國著名建築大師、加利福尼亞大學伯克利分校環境結構中心主任克里斯托夫·亞歷山大(Christopher Alexander)在他的著做《建築模式語言:城鎮、建築、構造(A Pattern Language: Towns Building Construction)中就提出了253種建築領域設計的基本模式。mysql

直到1987年,肯特·貝克(Kent Beck)和沃德·坎寧安(Ward Cunningham)率先將克里斯托夫·亞歷山大的這些模式思想應用在 Smalltalk 中的圖形用戶接口的生成中,遺憾的是並無引發軟件界的關注。(Smalltalk:被公認爲歷史上第二個面向對象的程序設計語言,有"面向對象編程之母"的稱號。)這裏的對象和接口就比如建築領域的牆壁和門窗。sql

3年後即1990年,軟件界纔開始研討關於設計模式的話題,相關會議也是層出不窮的召開。數據庫

1995 年,艾瑞克·伽馬(ErichGamma)、理査德·海爾姆(Richard Helm)、拉爾夫·約翰森(Ralph Johnson)、約翰·威利斯迪斯(John Vlissides)等 4 位做者合做出版了《設計模式:可複用面向對象軟件的基礎》(Design Patterns: Elements of Reusable Object-Oriented Software)一書,在本教程中收錄了 23 個設計模式,這是設計模式領域裏程碑的事件,致使了軟件設計模式的突破。這 4 位做者在軟件開發領域裏也以他們的「四人組」(Gang of Four,GoF)匿名著稱。編程

GoF(Gang of Four,四人組)所提出的設計模式主要基於如下的面向對象的設計原則:設計模式

對接口編程而不是對實現編程
優先使用對象組合而不是繼承架構

直到今天,狹義的設計模式仍是指GoF(Gang of Four,四人組)的這23種經典設計模式。oracle

Q二、設計模式(Design Pattern)是如何被定義的?

軟件設計模式(Software Design Pattern),又稱設計模式。是一套被反覆使用、多數人知曉的、通過分類編目的、代碼設計經驗的總結。歸納來說,它是解決特定問題的一系列套路,具備程語言無關性。ide

這世上本沒有什麼設計模式,用的人多了,也便成了設計模式。函數

學習設計模式的目的就是提升代碼的可重用性、可讀性、可靠性、靈活性及可維護性。天然也可
以提升程序猿的思惟能力、編程能力及設計能力。同時,學習並掌握設計模式是中級工程師進階高開或者架構師的必經之路。

設計模式的基本要素?

總的來講,設計模式包含如下幾個基本要素:模式名稱、別名、動機、問題、解決方案、效果、結構、模式角色、合做關係、實現方法、適用性、已知應用、例程、模式拓展和相關模式等。

其實,設計模式的基本要素主要有如下四個,羅列以下:

設計模式提出之初就很明確地指定了設計模式的基本要素,可是實際多數軟件開發者會重點關注第1和第3點要素(即重點關注設計模式及其實現)而忽略第2和第4點要素(即忽略設計模式的產生背景及設計目標),最終致使設計出的應用編碼邏輯繁雜或得不到預期的效果。

設計模式的七大原則?

下面,咱們用一張圖清晰地記錄下設計模式中的幾大原則(部分文獻中記錄的爲六大原則,就是沒有合成複用原則):

Q一、開閉原則?

開閉原則是設計模式中的總原則,開閉原則就是說:對拓展開放、對修改關閉。模塊應該在儘可能不修改代碼的前提下進行拓展,這就須要使用接口和抽象類來實現預期效果。

舉個栗子,咱們以書店銷售書籍爲例,類圖以下:

IBook定義了數據(書籍)的三個屬性,分別是:名稱、價格和做者。小說類NovelBook是一個具體的實現類,IBook接口代碼以下:

public interface IBook {
    public String getName();

    public int getPrice();

    public String getAuthor();

}

小說類NovelBook實現了IBook接口,NovelBook類具體代碼以下:

public class NovelBook implements IBook{
    // 書籍名稱
    private String name;
    // 書籍價格
    private int price;
    // 書籍做者
    private String author;

    public NovelBook(String _name, int _price, String _author) {
        this.name = _name;
        this.price = _price;
        this.author = _author;
    }

    @Override
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    @Override
    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

}

而後咱們打印下書籍價格等信息:

public class Main {

    public static void main(String[] args) {
        IBook novel = new NovelBook("笑傲江湖", 45, "金庸");
        System.out.println("書籍名字:" + novel.getName() + "\n書籍做者:" + novel.getAuthor() + "\n書籍價格:" + novel.getPrice());
    }

}

具體輸出以下:

書籍名字:笑傲江湖
書籍做者:金庸
書籍價格:45

那麼問題來了,假若某一天市場環境很差,書籍須要打折銷售的話(好比40元以上的書籍按9折銷售,其它的8折銷售),以上代碼如何應對這種需求變化呢?通常來講,有以下三種方法來解決這個問題:

  • 修改接口,在IBook上新增一個getOffPrice()方法,專門用來獲取折扣書價,全部的實現類包括NovelBook實現該方法。致使的問題,一是實現類要修改,二是main函數中的方法邏輯也要作修改,三就是IBook做爲接口應該是穩定且可靠的,不該該頻繁改動。這違反了開放原則,因此該方案,否認。
  • 修改實現類,即修改NovelBook的getPrice()中的處理邏輯,可是引來的問題是:假如某一天採購書籍的人員要查詢書價的話,那麼他看到的也是折扣後的價錢,顯然是不合理的。所以,該方案也不是最優解決。
  • 經過拓展子類來實現變化,新建一個子類OffNovelBook,覆寫getPrice()方法,經過拓展完成新增長的業務。

OffNovelBook類的代碼示例以下:

public class OffNovelBook extends NovelBook{
    public OffNovelBook(String _name, int _price, String _author) {
        super(_name, _price, _author);
    }

    @Override
    public int getPrice() {
        // 原價
        int selfPrice = super.getPrice();
        // 折扣價
        int offPrice = 0;
        if (selfPrice >= 40) {
            offPrice = selfPrice * 90 / 100;
        } else {
            offPrice = selfPrice * 80 / 100;
        }
        return offPrice;
    }

}

同時,main中調用相應調整以下:

public class Main {

    public static void main(String[] args) {
        IBook novel = new NovelBook("笑傲江湖", 45, "金庸");
        System.out.println("書籍名字:" + novel.getName() + "\n書籍做者:" + novel.getAuthor() + "\n書籍價格:" + novel.getPrice());
        System.out.println("------------------------");
        IBook novel2 = new OffNovelBook("笑傲江湖", 45, "金庸");
        System.out.println("書籍名字:" + novel2.getName() + "\n書籍做者:" + novel2.getAuthor() + "\n書籍折扣後價格:" + novel2.getPrice());
    }

}

輸出的書籍價格信息以下:

書籍名字:笑傲江湖
書籍做者:金庸
書籍價格:45
------------------------
書籍名字:笑傲江湖
書籍做者:金庸
書籍折扣後價格:40

Q二、單一職責原則?

何爲單一指責原則,指的是一個類或者模塊有且只有一個改變的緣由。若是模塊或類承擔的指責過多,就等於這些指責耦合在一塊兒,這樣一個模塊的變快可能會削弱或抑制其它模塊的能力,這樣的耦合是十分脆弱地。因此應該儘可能保持單一職責原則,此原則的核心就是解耦和加強內聚性。

舉個栗子,以下需求涉及到用戶的相關模塊操做,因此定義了IUserInfo接口,以下:

該接口設計有一個很糟糕的地方,就是將用戶信息(Business Object,業務對象BO)和用戶行爲(Business Logic,業務邏輯Biz)的修改混爲一談了,根據單一職責原則,應當將指責解耦,處理過的接口設計以下:

這裏解釋下設計的目的,首先將一個接口拆分紅兩個接口,IUserBO負責用戶屬性信息,IUserBIz負責用戶行爲信息維護,根據面向接口編程的思想,因此產生了UserInfo對象以後,便可以將其看成IUserBO接口使用,又能夠當作IUserBiz接口使用。當咱們須要修改用戶信息時調用IUserBO實現類,須要行爲信息操做時看成IUserBiz實現類便可。

IUserInfo userInfo = new UserInfo();
// 操做BO時
IUserBO userBO = (IUserBO) userInfo;
userBO.setPassword("123456");
// 行爲操做時
IUserBiz userBiz = (IUserBiz) userInfo;
userBiz.deleteUser();

單一職責原則,再舉個栗子就是萬能類:即在一個類(或方法)中徹底能夠實現系統全部功能。可是這樣作每每模塊嚴重耦合,牽一髮而動全身。因此須要職責分離,MVC架構中該原則體現的就比較明顯。

在如今流行的微服務架構體系中,最頭疼的就是服務拆分,拆分的粒度也頗有講究,標準的應該是聽從單一原則,避免服務拆分時發生各類撕逼行爲:」本應該在A服務中的被安排在了B服務中「,因此服務的職責劃分尤其重要。

再有就是,作service層開發時,早期的開發人員會將數據庫操做放在service中,好比getConnection,而後執行prepareStatement,再就是service邏輯處理等等。但是後來發現數據庫要由原來的mysql變動爲oracle,service層代碼豈不是須要重寫一遍,天了嚕...直接崩潰跑路。

」我單純,因此我快樂「用來形容單一指責原則再恰當不過了。

Q三、里氏替換原則?

里氏替換原則的解釋是,全部引用基類的地方必須能透明地使用其子類的對象。通俗來說的話,就是說,只要父類能出現的地方子類就能夠出現,而且使用子類替換掉父類的話,不會產生任何異常或錯誤,使用者可能根本就不須要知道是父類仍是子類。反過來就不行了,有子類的地方不必定能使用父類替換。

好比某個方法接受一個Map型參數,那麼它必定能夠接受HashMap、LinkedHashMap等參數,可是反過來的話,一個接受HashMap的方法不必定能接受全部Map類型參數。

里氏替換原則是開閉原則的實現基礎,它告訴咱們設計程序的時候儘量使用基類進行對象的定義及引用,具體運行時再決定基類對應的具體子類型。

接下來舉個栗子,咱們定義一個抽象類AbstractAnimal對象,該對象聲明內部方法」跳舞「,其中,Rabbit、Dog、Lion分別繼承該對象,另外聲明一個Person類,該類負責餵養各類動物,Client類負責邏輯調用,類圖以下:

其中,Person類代碼以下:

public class Person {
    private AbstractAnimal animal;

    public void feenAnimal(AbstractAnimal _animal) {
        this.animal = _animal;
    }

    public void walkAnimal(){
        System.out.println("人開始溜動物...");
        animal.dance();
    }

}

main函數調用的時候以下:

public class Main {

    public static void main(String[] args) {
        Person person = new Person();
        person.feenAnimal(new Rabbit());
        person.walkAnimal();
    }

}

打印輸出:

人開始溜動物...
小白兔跳舞...

Q四、依賴倒置原則?

依賴倒置原則(Dependency Inversion Principle,DIP)的定義:程序要依賴於抽象接口,不要依賴於具體實現。簡單的說就是要求對抽象進行編程,不要對實現進行編程,這樣就下降了客戶與實現模塊間的耦合。

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

依賴倒置原則,高層模塊不該該依賴低層模塊,都應該依賴抽象。抽象不該該依賴細節,細節應該依賴抽象。其核心思想是:要面向接口編程,不要面向實現編程。

舉個栗子,拿顧客商店購物來講,定義顧客類以下,包含一個shopping方法:

public class Customer {
    public void shopping (YanTaShop shop) {
        System.out.println(shop.sell());
    }

}

以上表示顧客在"雁塔店"進行購物,假如再加入一個新的店鋪"高新店",表示修改以下:

public class Customer {
    public void shopping (GaoXinShop shop) {
        System.out.println(shop.sell());
    }

}

這顯然是設計不合理的,違背了開閉原則。同時,顧客類的設計和店鋪類綁定了,違背了依賴倒置原則。解決辦法很簡單,將Shop抽象爲具體接口,shopping入參使用接口形式,顧客類面向接口編程,以下:

public class Customer {
    public void shopping (Shop shop) {
        System.out.println(shop.sell());
    }
}

interface Shop{
    String sell();

}

類圖關係以下:

Q五、接口隔離原則?

接口隔離原則(Interface Segregation Principle,ISP)的定義是客戶端不該該依賴它不須要的接口,類間的依賴關係應該創建在最小的接口上。簡單來講就是創建單一的接口,不要創建臃腫龐大的接口。也就是接口儘可能細化,同時接口中的方法儘可能少,保持接口純潔性。

咱們所講的接口主要分爲兩大類,一是實例接口,好比使用new關鍵字產生一種實例,被new的類就是實例類的接口。從這個角度出發的話,java中的類其實也是一種接口。二是類接口,java中經常使用interface關鍵字定義。

舉個栗子來講,咱們使用接口IPrettyGirl來描述美女,剛開始類圖可能描述以下:

可是發現該接口中包含對美女的外觀描述、內在美描述等,幾乎將美女的全部特性所有歸入,這顯然不是一個很好的設計規範,好比在唐朝,在那個以福爲美的時代對美的理解就不一樣,就會出現單純goodLooking過關就是美女的結果,因此這裏咱們須要將接口隔離拆分。將一個接口拓展爲兩個,增長系統靈活性及可維護性。

這裏咱們將美女接口拆分爲內在美、外在美兩個接口,系統靈活性提升了,另外接口間還能使用繼承實現聚合,系統拓展性也獲得了加強。

Q六、迪米特法則?

迪米特法則(Law of Demeter,LOD),有時候也叫作最少知識原則(Least Knowledge Principle,LKP),它的定義是:一個軟件實體應當儘量少地與其餘實體發生相互做用。迪米特法則的初衷在於下降類之間的耦合。

舉個栗子,拿教師點名來說,體育老師須要清點班上學生人數,教師通常不是本身親自去數,而是委託組長或班長等人去清點,即教師經過下達命令至班長要求清點人數:

public class Girl {

}

public class GroupLeader {

    private final List<Girl> girls;

    public GroupLeader(List<Girl> girls) {
        this.girls = girls;
    }

    public void countGirls() {
        System.out.println("The sum of girls is " + girls.size());
    }
}

public class Teacher {

    public void command(GroupLeader leader){
        leader.countGirls();
    }
}

public class Main {

    public static void main(String[] args) throws Exception {
        Teacher teacher = new Teacher();
        GroupLeader groupLeader = new GroupLeader(Arrays.asList(new Girl(), new Girl()));
        teacher.command(groupLeader);
    }

}


上述例子中,若是去掉GroupLeader這個中間人角色,教師就會直接去清點人數,這樣作會違反迪米特法則。

Q七、合成/聚合複用原則?

合成複用原則是經過將已有的對象歸入新對象中,做爲新對象的成員對象來實現的,新對象能夠調用已有對象的功能,從而達到複用。原則是儘可能首先使用合成/聚合的方式,而不是使用繼承。

相關文章
相關標籤/搜索