Java設計模式之開篇

木木匠發表於2018-10-27

Java設計模式之開篇

一、概述

設計模式筆者之前也學習過一遍,但是慚愧工作中只用到幾種常用的模式,比如單例模式,工廠模式,裝飾者模式等。自己回想起來,發現大部分都差不多忘記了,所以,筆者想把設計模式重新學習一遍,也順便用文字記錄學習的過程,與大家分享。這篇是設計模式的開篇,裡面會講幾個常用的設計原則,也會用程式碼去體現這些設計原則。

二、設計原則

2.1 單一職責

定義:單一職責的英文全稱是Single Responsibility Principle,簡稱SPR。

英文解釋是:There should never be more than one reason for a class to change.

翻譯過來就是,一個類只能有且僅僅有一個原因導致類的變更。

我們用一個例子說明下:

需求場景:設計一個手機,手機包含功能為打電話,掛電話,播放音樂功能。

public interface Imobile {
    //打電話
    public void call(String number);
    //播放音樂
    public void playMusic(Object o);
    //結束通話電話
    public  void hangup();
}
複製程式碼

上面設計了一個Imobile的介面,宣告瞭打電話,結束通話,播放音樂的方法,我們初步看,覺得這麼設計沒什麼問題,但是如果我們考慮單一職責的話,這個設計就有問題了,其實單一職責最難劃分的就是職責,我們針對這個場景可以給這個電話分為兩個職責,打電話和掛電話是屬於協議管理的,播放音樂其實屬於附屬功能管理,所以這裡的職責就劃分了兩個:1.協議管理;2.附屬功能管理。那麼單一職責的定義就是:一個類只能有且僅僅有一個原因導致類的變更。而上面這個介面中劃分了兩個職責,而且,協議的變動,附屬功能的變動,都會導致介面和類改變,所以,這個介面就是不符合單一職責的。那麼如何讓其滿足單一職責原則呢?我們需要拆分介面,因為協議管理和附屬功能管理兩個彼此並不互相影響,所以我們可以直接拆分為兩個介面,如下:

//協議管理介面
public interface IMobileManager {
    //打電話
    public void call(String number);
    //結束通話電話
    public  void hangup();
}
//附屬功能介面
public interface Ifunction {
    public void playMusic(Object o);
}
複製程式碼

這個時候很多人可能不理解,你這麼做的好處是什麼呢?我感覺不到這麼做的好處啊。這裡做一個假設,假設這個時候新增了一部高階手機,它可以保持會話,這個時候協議管理介面需要修改了,需要新增一個保持會話的功能,這個時候實現類也要跟著改變,如果採用第一種設計,那麼所有的電話都要修改。如果有一個玩具手機,它並不會通話,這個時候也要修改這個實現類,這個設計就糟糕了。如果採用了單一職責,玩具手機並不會實現協議管理的介面,只會實現附屬功能介面,所以協議管理的修改並不會導致玩具手機也要修改。

2.1.1 單一職責的好處

  • 類的複雜度降低了,各個職責都有清晰明確的定義
  • 提高了可讀性,知道什麼介面是幹什麼的
  • 提高了可維護性,某個介面的修改不會導致無關類受影響。

2.1.2 單一職責的補充

其實單一職責並不只要求介面,方法也是,我們寫一個方法要能清晰的定義這個方法的職責,比如修改使用者資訊最好就要寫多個方法來實現,不要就只寫一個方法。類似於這樣:

public interface IUserSerivice {
    void updateUserInfo(User user);
}
複製程式碼

這種設計不清晰,我們應該針對每一個修改都有一個方法,類似於這樣:

public interface IUserSerivice {
    void updateUserName(String name,String id);
    void updateUserTelPhone(String phone,String id);
    void updateUserHomeAddr(String adrr,String id);
}
複製程式碼

這樣寫雖然很囉嗦,但是職責很清晰,後續程式碼也好維護,直接就能知道更新了什麼資訊。

2.2 里氏替換

定義:里氏替換原則的英文全稱:Liskov Substitution Principle ,簡稱LSP。

英文解釋:Functions that user pointer or references to base classes must be able to use objects of derived classes without knowing it.

翻譯:所有引用基類的地方都必須能透明的使用其子類物件。

其實理解這句話很簡單,無非就是父類執行的方法,替換成子類也可以正確執行並且達到一樣的效果。我們先寫一個沒有按照里氏替換原則的程式碼。

public class Father {
    public void doSomeThing(Map map){
        System.out.println("父類執行啦!");
    }
}
public class Son extends Father{
    public void doSomeThing(HashMap map) {
        System.out.println("子類執行了!");
    }
}
public class Client1 {
    public static void main(String[] args) {
        HashMap map=new HashMap();
        Father father=new Father();
        father.doSomeThing(map);
    }
}

public class Client2 {
    public static void main(String[] args) {
        HashMap map=new HashMap();
        Son son=new Son();
        son.doSomeThing(map);
    }
}
複製程式碼

我們執行客戶端main方法,發現結果輸出為:“父類執行啦!”,我們採用子類替換父類執行doSomeThing() 方法,發現輸出結果是:“子類執行了!”,這和父類執行的結果不一致,不符合里氏替換原則,這裡為什麼沒有執行父類的方法呢?這裡因為是子類過載了父類的方法,客戶端呼叫的引數是HashMap,所以匹配到了子類的方法。那麼我們如何修改就能滿足里氏替換原則呢?其實很簡單,兩種方式。

  • 第一,直接繼承,不要重寫父類的非抽象方法。
  • 第二,我們過載方法的引數範圍必須大於等於父類的範圍。

第一個好理解,那第二個怎麼理解呢?我們還是用上面那個例子改動下,程式碼如下:

public class Father {
    public void doSomeThing(HashMap map){
        System.out.println("父類執行啦!");
    }
}
public class Son extends Father{
    public void doSomeThing(Map map) {
        System.out.println("子類執行了!");
    }
}
複製程式碼

這裡其實就只把子類的引數型別改成了Map,父類的引數型別改成了HashMap, 這樣客戶端宣告的引數型別是HashMap,所以呼叫 son.doSomeThing(map)只會執行父類的方法。

這裡其實可以總結一句:里氏替換原則就是要求,不要重寫父類的非抽象方法,儘量不要過載父類的方法,如果要過載,需要注意方法的前置條件(形參),如果要保持子類的個性化,可以採用新增方法的方式。

2.2.1 里氏替換原則的作用

  • 其實最主要的作用就是降級繼承的複雜度,增強程式碼的可維護性

2.3 依賴倒置

定義:依賴倒置英文全稱為:Dependence Inversion Princiole,簡稱DIP。

英文解釋:High level modules should not depend upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions。

官方翻譯:高層模組不應該依賴低層模組,兩者都應該依賴其抽象;抽象不應該依賴細節,細節應該依賴抽象。

依賴倒置,我們用通俗的解釋就是,平常我們生活中的依賴都是依賴具體細節,比如我要用手機就是具體的某個手機,用電腦就是用具體的某臺電腦,這個依賴倒置就是和我們生活是反的,故稱為倒置,所以依賴倒置就是依賴抽象(介面或者抽象類)。我們同樣用一個例子來說明下:

我們實現一個司機開車的例子,我們可以抽象出2個介面,一個是司機介面,一個是汽車介面。

public interface ICar {
    //開汽車方法
    public void run();
}
public interface IDriver {
    //開車
    public void driver(ICar car);
}
//汽車實現類,寶馬車
public class BmwCar implements ICar {
    @Override
    public void run() {
        System.out.println("寶馬車開動啦");
    }
}
//司機實現類,C1駕照司機
public class COneDriver implements  IDriver {
    @Override
    public void driver(ICar car) {
        System.out.println("我是C1駕照司機");
        car.run();
    }
}
// 客戶端場景類
public class Client {
    public static void main(String[] args) {
        ICar bmw=new BmwCar();
        IDriver cOneDriver=new COneDriver();
        cOneDriver.driver(bmw);
    }
}

複製程式碼

這裡實現了C1駕照司機開寶馬車的場景,這就是依賴倒置原則的寫法,那如果我不採用依賴倒置會發生什麼情況呢?不依賴倒置也就是說要依賴細節,以上場景就會出現C1駕照車司機只能開寶馬車的情況,這顯然是有問題的。

2.3.1 依賴倒置的規則

根據上面的例子以及我們的分析,我們可以總結出依賴倒置的幾個規則:

  • 每個類儘量都有介面或者抽象類,或者抽象類和介面兩者都具備。
  • 變數的表面型別儘量是介面或者是抽象類。
  • 任何類都不應該從具體的實現類中派生
  • 儘量不要重寫基類的方法

2.4 迪米特法則

定義:迪米特法則(Law of Demeter,LoD)也稱為最少知識原則(Least Knowledge Principle,LKP)

迪米特法則通俗的解釋就是,一個類要對其所耦合的類瞭解的儘量少,不管耦合的類內部多麼複雜,都只管其暴露的public方法。迪米特法則另外一種說法是,只和朋友類交流。朋友類的定義:出現在成員變數、方法的輸入輸出引數中的類稱為成員朋友類,而出現在方法體內部的類不屬於朋友類。我們先看一個違法迪米特法則的例子。

場景:我們吃飯要經過客戶點菜,服務員下單,廚師做菜這三個流程,我們來用程式碼設計這個場景。

//廚師介面
public interface ICooker {
    //根據訂單做菜
    public void cooke(List<Order> orders);
}
//服務員介面
public interface IWaiter {
    //下單
    public void doOrder(List<String> dishNames);
}
// 訂單實體類
public class Order {
    private List<String> dishNames;

    public Order(List<String> dishNames) {
        this.dishNames = dishNames;
    }

    public List<String> getDishNames() {
        return dishNames;
    }

    public void setDishNames(List<String> dishNames) {
        this.dishNames = dishNames;
    }
}

// 服務員實現類
public class ChineseWaiter implements IWaiter {
    private ICooker cooker;

    public ChineseWaiter(ICooker cooker) {
        this.cooker = cooker;
    }

    @Override
    public void doOrder(List<String> dishNames) {
        List<Order> cookOrders=new ArrayList<>();
        cookOrders.add(new Order(dishNames));
        cooker.cooke(cookOrders);
    }
}

//廚師實現類
public class ChineseCooker implements ICooker {
    @Override
    public void cooke(List<Order> orders) {
        for (int i = 0; i < orders.size(); i++) {
           Order order=orders.get(i);
           List<String> dishNames=order.getDishNames();
            for (int j = 0; j < dishNames.size(); j++) {
                System.out.println("我是中餐廚師,我做:"+dishNames.get(j));
            }
        }

    }
}
//場景類
public class Client {
    public static void main(String[] args) {
        IWaiter waiter=new ChineseWaiter(new ChineseCooker());
        List<String> dishNames=new ArrayList<>();
        dishNames.add("紅燒魚塊");
        dishNames.add("宮保雞丁");
        waiter.doOrder(dishNames);
    }
}
複製程式碼

我們自己思考下,其實上述程式碼中,違法迪米特法則地方就是服務員的實現類,我們發現,服務員實現類ChineseWaiter在實現類中,和非朋友類產生了依賴,這個依賴就是Order類,我們再回顧下朋友類的定義:出現在成員變數、方法的輸入輸出引數中的類稱為成員朋友類,Order類並不滿足這個定義,所以它違反了迪米特法則。那麼我們如何修改滿足迪米特法則呢?我們只要修改服務員實現類和場景類即可,修改後的程式碼如下:

public interface IWaiter {
    //下單
    public void doOrder(List<Order> orders);
}


public class ChineseWaiter implements IWaiter {
    private ICooker cooker;

    public ChineseWaiter(ICooker cooker) {
        this.cooker = cooker;
    }


    @Override
    public void doOrder(List<Order> orders) {
        cooker.cooke(orders);
    }
}
public class Client {
    public static void main(String[] args) {
        IWaiter waiter=new ChineseWaiter(new ChineseCooker());
        List<String> dishNames=new ArrayList<>();
        dishNames.add("紅燒魚塊");
        dishNames.add("宮保雞丁");
        List<Order> orders =new ArrayList<>();
        orders.add(new Order(dishNames));
        waiter.doOrder(orders);
    }
}
複製程式碼

這裡把訂單的封裝丟給了場景類中,服務員只依賴他的朋友類廚師類就可以了。那麼這個迪米特法則有什麼作用呢?其實迪米特法則最主要的作用就是降低耦合,從而使得類的複用率得以提高。但是採用迪米特法則後就會導致產生了過多的中間類和跳轉類,導致系統的複雜性提高,所以我們在使用該法則的時候要權衡利弊,還是那句話,沒有最完美的設計,只有最合適的設計。

2.5 介面隔離

英文解釋:Clients should not be forced to depend upon interfaces that they don't use.The dependency of one class to another one should depend on the smallest possible interface.

官方翻譯:客戶端不應該依賴它不需要的介面。類間的依賴關係應該建立在最小的介面上。

介面隔離原則,其實可以理解為介面設計的粒度要儘量小,介面中的方法要儘量少。這裡其實和單一職責很相識,但是有區別,單一職責是職責的劃分要求,每個介面只要表述對應的職責就可以了。但是介面隔離一般是對應於某個模組呼叫,可能只用到某個介面的部分方法,可以更細分。舉例說明:

還是以單一職責的例子,設計手機。之前的程式碼是分為了一個功能介面,一個協議管理介面。程式碼見單一職責部分。我們看看如果是用介面隔離還可以怎麼設計。我們其實還可以對功能介面可以劃分更細的粒度,比如最新的iPhone手機有faceId功能,三星手機有虹膜功能。那這個時候,我還是用一個功能介面,就會導致介面非常冗餘,一個介面有faceid,虹膜,但是實際上有些手機並沒有這些功能,那麼我們就可以對功能介面進行拆分。拆分成這樣:

public interface ISamFunction {
    //虹膜功能
    public void iris();
}

public interface IAppleFnction {
    //faceId 功能
    public void faceId();
}
複製程式碼

然後如果有手機既有虹膜又有faceId功能,直接實現兩個介面就可以了。這樣就滿足了介面隔離原則。

2.6 開閉原則

英文解釋:Software entities like classes,modules and functions should be open for extension but closed for modifications

官方翻譯:一個軟體實體如類、模組和函式應該對擴充套件開放,對修改關閉

開閉原則,其實是一個總的原則,前面五種原則其實都是開閉原則的具體實現,它並沒有一個具體的設計思路,只是要求我們對設計的類,方法等對擴充套件開放,對修改關閉。掌握了前面五種設計原則,其實也就掌握了開閉原則了,這裡就不舉例說明了。

三、總結

1.單一職責

  • 介面,類,方法的劃分要職責單一,不要寫出一個萬能的介面,類和方法,要按照職責,寫出明確職責的介面,類和方法,這樣可讀性好,可維護性高。

2.里氏替換原則

  • 父類出現的地方,子類就能出現。它要求,儘量不要重寫父類的非抽象方法,儘量不要過載父類的方法。

3.依賴倒置原則

  • 類之間的依賴要依賴抽象(介面或者抽象類),不要依賴具體的實現類,這樣方便後續的擴充套件。

4.迪米特原則

  • 不要關注類的內部實現,只關注其public方法,只和自己的朋友類交流。這個原則要求,類的耦合只能是朋友類,朋友類指的是,出現在成員變數、方法的輸入輸出引數中的類。

5.介面隔離

  • 要保持介面儘可能的細粒度,不要依賴不相關的介面和方法。這樣才能提高介面的複用率。

6.開閉原則

  • 是一個總的原則,是對前面五種原則一個總結的抽象。要求我們設計對擴充套件開放,對修改關閉。這個並不是要求我們不能去修改,我們要根據實際情況,竟可能的進行少的修改,儘可能的保證修改影響的範圍儘可能的小。

最後,上面六個設計原則,都是一種原則,並不是要求我們生搬硬套這幾種原則去寫程式碼,這幾種思想我們要理解消化,根據專案實際情況去設計,去寫程式碼,沒有最好的設計,沒有萬能的設計,沒有一成不變的設計,只有最合適的設計。這裡,分享無印良品的著名設計師原研哉的一個設計理念:

這樣就好

四、參考

《設計模式之蟬》

五、推薦閱讀

帶你走進java集合之ConcurrentHashMap

帶你走進java集合之ArrayList

帶你走進java集合之HashMap

Java鎖之ReentrantLock(一)

Java鎖之ReentrantLock(二)

Java鎖之ReentrantReadWriteLock

JAVA NIO程式設計入門(一)

JAVA NIO 程式設計入門(二)

JAVA NIO 程式設計入門(三)

相關文章