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