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); //結束通話電話 publicvoid hangup(); } 複製程式碼
上面設計了一個Imobile
的介面,聲明瞭打電話,結束通話,播放音樂的方法,我們初步看,覺得這麼設計沒什麼問題,但是如果我們考慮單一職責的話,這個設計就有問題了,其實單一職責最難劃分的就是職責,我們針對這個場景可以給這個電話分為兩個職責,打電話和掛電話是屬於協議管理的,播放音樂其實屬於附屬功能管理,所以這裡的職責就劃分了兩個:1.協議管理;2.附屬功能管理。那麼單一職責的定義就是:一個類只能有且僅僅有一個原因導致類的變更。而上面這個介面中劃分了兩個職責,而且,協議的變動,附屬功能的變動,都會導致介面和類改變,所以,這個介面就是不符合單一職責的。那麼如何讓其滿足單一職責原則呢?我們需要拆分介面,因為協議管理和附屬功能管理兩個彼此並不互相影響,所以我們可以直接拆分為兩個介面,如下:
//協議管理介面 public interface IMobileManager { //打電話 public void call(String number); //結束通話電話 publicvoid 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 implementsIDriver { @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
官方翻譯:一個軟體實體如類、模組和函式應該對擴充套件開放,對修改關閉
開閉原則,其實是一個總的原則,前面五種原則其實都是開閉原則的具體實現,它並沒有一個具體的設計思路,只是要求我們對設計的類,方法等對擴充套件開放,對修改關閉。掌握了前面五種設計原則,其實也就掌握了開閉原則了,這裡就不舉例說明了。