初探設計模式六大原則

  • 2019 年 10 月 31 日
  • 筆記

前言

我想用貼近生活的語句描述一下自己對六種原則的理解。也就是不做專業性的闡述,而是描述一種自己學習後的理解和感受,因為能力一般而且水平有限,也許舉的例子不盡妥當,還請諒解
 
原本我是想用JavaScript編寫的,但是JavaScript到現在還沒有提出介面的概念,而用TypeScript寫又感覺普及度還不算特別高,所以還是決定用Java語言編寫

目錄

設計模式有六大原則
  • 單一職責原則

  • 里氏替換原則

  • 依賴倒置原則

  • 介面隔離原則

  • 迪米特原則

  • 開閉原則

首先要提的是:六大原則的靈魂是面向介面,以及如何合理地運用介面

P1.單一職責原則(Single Responsibility Principle)

應該有且僅有一個原因引起類的變更(There should never be more than one reason for a class to change)。
 
為了達到這個目標,我們需要對類和業務邏輯進行拆分。劃分到合適的粒度,讓這些各自執行單一職責的類,各司其職。讓每個類盡量行使單一的功能,實現“高內聚”,這個結果也使得類和類之間不會有過多冗餘的聯繫,從而“低耦合”。
 
比如我們現在有了這樣一個類
public class People {      public void playZhiHu () {          System.out.println("玩知乎");      }      public void doSports () {          System.out.println("打乒乓球");      }      public void work () {          System.out.println("工作");      }  }

 

現在看起來有點混亂,因為這個類裡面混合了三個職責:
  • 玩知乎,這是知乎er的職責

  • 打乒乓球,這是業餘運動愛好者的職責

  • 工作,這是“普普通通上班族”的職責(似乎暴露了什麼)

OK,正如你所見,既然我們要遵循單一職責,那麼怎麼做呢?當然是要拆分了
 
我們要根據介面去拆,拆分成三個介面去約束People類(不是把People類拆了哈)
// 知乎er  public interface ZhiHuer {      public void playZhiHu();  }  // 上班族  public interface OfficeWorkers {      public void work();  }  // 業餘運動愛好者  public interface AmateurPlayer {      public void doSports();  }

 

然後在People中繼承這幾個介面
public class People implements ZhiHuer,AmateurPlayer,OfficeWorkers{      public void playZhiHu () {          System.out.println("玩知乎");      }      public void doSports () {          System.out.println("打乒乓球");      }      public void work () {          System.out.println("工作");      }  }

 

最後創建實例運行一下
public class Index {      public static  void main (String args []) {          People people = new People();          ZhiHuer zhiHuer = new People();          zhiHuer.playZhiHu(); // 輸出:玩知乎          OfficeWorkers workers = new People();          workers.work(); // 輸出: 工作          AmateurPlayer players = new People();          players.doSports(); // 輸出:打乒乓球      }  }

 

 

備註:這個原則不是死的,而是活的,在實際開發中當然還要和業務相結合,不會純粹為了理論貫徹單一職責,就像資料庫開發時候,不會完全遵循“三大範式”,而是允許一定冗餘的

P2.里氏替換原則(liskov substitution principle)

里氏替換原則,一種比較好的理解方式是: 所有引用基類的地方必須能透明地使用其子類的對象。 換句話說,子類必須完全實現父類的功能。凡是父類出現的地方,就算完全替換成子類也不會有什麼問題。
 
以上描述來自《設計模式之禪》,剛開始看的時候我有些疑惑,因為一開始覺得:只要繼承了父類不都可以調用父類的方法嗎?為什麼還會有里氏替換所要求的:子類必須完全實現父類的功能呢, 難不成繼承的子類還可以主動“消除”父類的方法?
 
還真可以,請看
父類
public abstract class Father {      // 認真工作      public abstract void work();      // 其他方法  }

public class Son extends Father {      @Override      public void work() {       // 我實現了爸爸的work方法,旦我什麼也不做!      }  }

 

子類雖然表面上實現了父類的方法,但是他實際上並沒有實現父類要求的邏輯。里氏替換原則要求我們避免這種“塑料父子情”,如果出現子類不得不脫離父類方法範圍的情況, 採取其他方式處理,詳情參考《設計模式之禪》

(其實個人覺得《禪》的作者其實講的“父類”其實著重指的是抽象類)

P3.依賴倒置原則 (dependence inversion principle)

很多文章闡述依賴倒置原則都會闡述為三個方面
  • 高層的模組不應該依賴於低層的模組,這兩者都應該依賴於其抽象

  • 抽象不應該依賴細節

  • 細節應該依賴抽象

換句話說, 高層次的類不應該依賴於,或耦合於低層次的類,相反,這兩者都應該通過相關的介面去實現。要面向介面編程,而不是面向實現編程,所以編程的時候並不是按照符合我們邏輯思考的“依賴關係”去編程掉的,這種不符,就是依賴倒置
 
舉個例子,類好比是道德,介面好比是法律。
 
道德呢,有上層的也有下層的,春秋時代,孔聖人提出了上層道德理論:“仁”的思想,並進一步細化為低層道德理論:“三綱五常”(高層模組和底層模組),想要以此規約眾生,實現天下大同。可是奈何民眾的道德終究還是靠不住(沒有介面約束的類,可能被混亂修改),何況道德標準是會隨物質經濟的變化而變化的,孔子時代和我們今天的已經大有不同了。(類可能會發生變化)所以才需要法律來進一步框定和要求道德。(我們用介面來約束和維護“類”,就好比用法律來維護和規約道德一樣。)假如未來道德倫理的標杆發生了變化,肯定是先修繕法律,然後再次反向規制和落實道德(面向介面編程,而不是面向實現編程)。
 
我們看下下面沒有遵循依賴倒置原則的程式碼是怎樣的,我們設計了兩個類:Coder類和Linux類,並且讓它們之間產生交互:Coder對象的develop方法接收Linux對象並且輸出系統名
// 底層模組1:開發者  public class Coder {      public void develop (Linux linux) {          System.out.printf("開發者正在%s系統上進行開發%n",linux.getSystemName());      }  }  // 底層模組2:Linux作業系統  public class Linux {      public String name;      public Linux(String name){          this.name = name;      }      public String getSystemName () {          return this.name;      }  }  // 高層模組  public class Index {      public static  void main (String args []) {          Coder coder = new Coder();          Linux ubuntu = new Linux("ubuntu系統"); // ubuntu是一種linux作業系統          coder.develop(ubuntu);      }  }

 

輸出
開發者正在ubuntu系統系統上進行開發 

但是我們能發現其中的問題:

作業系統不僅僅有Linux家族,還有Windows家族,如果我們現在需要讓開發者在windows系統上寫程式碼怎麼辦呢? 我們可能要新建一個Windows類,但是問題來了,Code.develop方法的入參數類型是Linux,這樣以來改造就變得很麻煩。
 
讓我們利用依賴倒置原則改造一下,我們定義OperatingSystem介面,將windows/Linux抽象成作業系統,這樣,OperatingSystem類型的入參就可以接收Windows或者Linux類型的參數了
// 程式設計師介面  public interface Programmer {      public void develop (OperatingSystem OS);  }  // 作業系統介面  public interface OperatingSystem {      public String getSystemName ();  }  // 低層模組:Linux作業系統  public class Linux implements  OperatingSystem{      public String name;      public Linux (String name) {          this.name = name;      }      @Override      public String getSystemName() {          return this.name;      }  }  // 低層模組:Window作業系統  public class Window implements OperatingSystem {      String name;      public Window (String name) {          this.name = name;      }      @Override      public String getSystemName() {          return this.name;      }  }  // 低層模組:開發者  public class Coder implements Programmer{      @Override      public void develop(OperatingSystem OS) {          System.out.printf("開發者正在%s系統上進行開發%n",OS.getSystemName());      }  }  // 高層模組:測試用  public class Index {      public static  void main (String args []) {          Programmer coder = new Coder();          OperatingSystem ubuntu = new Linux("ubuntu系統"); // ubuntu是一種linux作業系統          OperatingSystem windows10 = new Window("windows10系統"); // windows10          coder.develop(ubuntu);          coder.develop(windows10);      }  }

 

雖然介面的加入讓程式碼多了一些,但是現在擴展性變得良好多了,即使有新的作業系統加入進來,Coder.develop也能處理

P4. 介面隔離原則(interface segregation principle)

介面隔離原則的要求是:類間的依賴關係應該建立在最小的介面上。這個原則又具體分為兩點
  1. 介面要足夠細化,當然了,這會讓介面的數量變多,但是每個介面會具有更加明確的功能

  2. 在1的前提下,類應該依賴於“最小”的介面上

 
舉個例子,中秋節其實只過了一個多月,現在假設你有一大盒“五仁月餅”想帶回家餵豬,但是無奈的是包包太小放不下,而且一盒沉重的月餅對瘦弱的你是個沉重的負擔。這個時候,我們可以把月餅盒子拆開,選出一部分自己需要(wei zhu)的月餅,放進包包里就好啦,既輕便又靈活。
 
還是上程式碼吧,比如我們有這樣一個知乎er的介面,裡面涵蓋了一些可能的行為。許多知乎用戶還會保持友善,同時根據自己的專業知識認真寫文章。但也有少數的人會把生活中的負面能量帶到網路中
public interface ZhiHuer {      // 認真撰文      public void seriouslyWrite();      // 友好評論      public void friendlyComment();      // 無腦抬杠      public void argue();      // 鍵盤攻擊      public void keyboardAttack ();  }

 

我們發現,這個介面可以進一步拆分成兩個介面,分別命名為PositiveZhiHuer,NegativeZhihuer。這樣,我們就把介面細化到了一個合理的範圍
public interface PositiveZhiHuer {      // 認真撰文      public void seriouslyWrite();      // 友好評論      public void friendlyComment();  }    public interface NegativeZhihuer {      // 無腦抬杠      public void argue();      // 鍵盤攻擊      public void keyboardAttack ();  }

 
>> 備註:妥善處理 單一職責原則 和 介面隔離原則的關係
事實上,有兩點要說明一下
  1. 單一職責原則和介面隔離原則雖然看起來有點像,好像都是拆分,但是其實側重點是不一樣的,“職責”的粒度其實是比“隔離介面”的粒度要大的

  2. 基於1中闡述的原因,其實 單一職責原則 和 介面隔離原則是可能會產生衝突的,因為介面隔離原則要求粒度儘可能要細,但是單一職責原則卻不同,它要求拆分既不能過粗,但也不能過細,如果把原本單一職責的介面分成了“兩個0.5職責的介面”,那麼這就是單一職責所不能允許的了。

  3. 當兩者衝突時,優先遵循 單一職責原則

P5.迪米特原則 (law of demeter)

迪米特原則又叫最少知道原則,在實現功能的前提下,一個對象接觸的其他對象應該儘可能少,也即類和類之間的耦合度要低。
 
舉個例子,我們經常說要“減少無效社交”,不要總是一昧的以交朋友的數量衡量自己的交際能力,否則會讓自己很累的,也會難以打理好複雜的人際關係。對於並不很外向的人,多數時候和自己有交集的朋友交往就可以了。
 
我們看下程式碼:
有如下場景,現在你和你的朋友想要玩一個活動,也許是鬥地主等遊戲,這個時候需要再喊一個人,於是你讓你的朋友幫你再叫一個人,有程式碼如下
// 我的直接朋友  public class MyFriend {      // 找他的朋友      public void findHisFriend (FriendOfMyFriend fof) {        System.out.println("這是朋友的朋友:"+ fof.name);      }  }    // 朋友的朋友,但不是我的朋友  public class FriendOfMyFriend {      public String name;      public FriendOfMyFriend(String name) {        this.name = name;      }  }    //  public class Me {      public void findFriend (MyFriend myFriend) {        System.out.println("我找我朋友");        // 注意這段程式碼        FriendOfMyFriend fmf = new FriendOfMyFriend("陌生人");        myFriend.findHisFriend(fmf);      };  }

 

這時我們發現一個問題,你和你朋友的朋友並不認識,但是他卻出現在了你的“找朋友”的動作當中(在findFriend方法內),這個時候,我們認為這違反了迪米特原則(最少知道原則),迪米特原則我們對於對象關係的處理,要減少“無效社交”,具體原則是
  • 一個類只和朋友類交流,朋友類指的是出現在成員變數、方法的輸入輸出參數中的類

  • 一個類不和陌生類交流,即沒有出現在成員變數、方法的輸入輸出參數中的類

所謂的“不交流”,就是不要在程式碼里看到他們
 
我們改造一下上面的程式碼
// 我朋友  public class MyFriend {      public void findHisFriend () {          FriendOfMyFriend fmf = new FriendOfMyFriend("陌生人");          System.out.println("這是朋友的朋友:"+ fmf.name);      }  }  // 朋友的朋友,但不是我的朋友  public class FriendOfMyFriend {      public String name;      public FriendOfMyFriend(String name) {          this.name = name;      }  }    //  public class Me {      public void findFriend (MyFriend myFriend) {          System.out.println("我找我朋友");          myFriend.findHisFriend();      };  }

 

P6. 開閉原則(open closed principle)

開閉原則的意思是,軟體架構要:對修改封閉,對擴展開放
 
舉個例子
 
比如我們現在在玩某一款喜歡的遊戲,A鍵攻擊,F鍵閃現。這個時候我們想,如果遊戲能額外給我訂製一款“K”鍵,殘血時解鎖從而一擊OK對手完成5殺,那豈不美哉,這就好比是“對擴展開放”。
 
但是呢,如果遊戲突然搞個活動,把閃現/攻擊/技能釋放的鍵盤通通換個位置,給你一個“雙十一的驚喜”,這恐怕就給人帶來慘痛的回憶了。所以我們希望已有的結構不要動,也不能動,要“對修改封閉”
(本人不玩遊戲,這些是自己查到的,如果錯誤還請指正)

總結

  1. 原則不是死板的而是靈活的

  2. 一些原則其實是存在一定的衝突的,重要的是權衡,是掌握好度

  3. 六大原則是23種設計模式的靈魂,六大原則指導了設計模式,設計模式體現了六大原則