設計模式筆者以前也學習過一遍,可是慚愧工做中只用到幾種經常使用的模式,好比單例模式,工廠模式,裝飾者模式等。本身回想起來,發現大部分都差很少忘記了,因此,筆者想把設計模式從新學習一遍,也順便用文字記錄學習的過程,與你們分享。這篇是設計模式的開篇,裏面會講幾個經常使用的設計原則,也會用代碼去體現這些設計原則。java
2.1 單一職責編程
定義:單一職責的英文全稱是Single Responsibility Principle,簡稱SPR。設計模式
英文解釋是:There should never be more than one reason for a class to change.bash
翻譯過來就是,一個類只能有且僅僅有一個緣由致使類的變動。ide
咱們用一個例子說明下:函數
需求場景:設計一個手機,手機包含功能爲打電話,掛電話,播放音樂功能。post
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);
}
複製代碼
這個時候不少人可能不理解,你這麼作的好處是什麼呢?我感受不到這麼作的好處啊。這裏作一個假設,假設這個時候新增了一部高級手機,它能夠保持會話,這個時候協議管理接口須要修改了,須要新增一個保持會話的功能,這個時候實現類也要跟着改變,若是採用第一種設計,那麼全部的電話都要修改。若是有一個玩具手機,它並不會通話,這個時候也要修改這個實現類,這個設計就糟糕了。若是採用了單一職責,玩具手機並不會實現協議管理的接口,只會實現附屬功能接口,因此協議管理的修改並不會致使玩具手機也要修改。ui
2.1.1 單一職責的好處this
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.迪米特原則
5.接口隔離
6.開閉原則
最後,上面六個設計原則,都是一種原則,並非要求咱們生搬硬套這幾種原則去寫代碼,這幾種思想咱們要理解消化,根據項目實際狀況去設計,去寫代碼,沒有最好的設計,沒有萬能的設計,沒有一成不變的設計,只有最合適的設計。這裏,分享無印良品的著名設計師原研哉的一個設計理念:
這樣就好
《設計模式之蟬》
《帶你走進java集合之ConcurrentHashMap》