快速記憶口訣:單一的里氏依賴接口合成開閉的迪米特法則前端
開閉原則(Open-ClosedPrinciple,OCP)是指一個軟件實體如類,模塊和函數應該對擴展開放,對修改關閉.所謂的開閉,也正是對擴展和修改兩個行爲的一個原則.強調的是用抽象構建框架,用實現擴展細節.能夠提升軟件系統的可複用性及可維護性.開閉原則,是面向對象設計中最基礎的設計原則.它指導咱們如何創建穩定靈活的系統,例如:咱們版本更新,我儘量不修改源代碼,可是能夠增長新功能.java
在現實生活中對於開閉原則也有體現.好比,不少互聯網公司都實行彈性製做息時間,規定天天工做8小時.意思就是說,對於天天工做8小時這個規定是關閉的,可是你何時來,何時走是開放的.早來早走,晚來晚走.編程
實現開閉原則的核心思想就是面向抽象編程,接下來咱們來看一段代碼:以咕泡學院的課程體系爲例,首先建立一個課程接口 ICourse:設計模式
public interface ICourse { Integer getId(); String getName(); Double getPrice(); }
整個課程生態有 Java 架構,大數據,人工智能,前端,軟件測試等,咱們來建立一個 Java 架構課程的類 JavaCourse:架構
public class JavaCourse implements ICourse{ private Integer Id; private String name; private Double price; public JavaCourse(Integer id, String name, Double price) { this.Id = id; this.name = name; this.price = price; } public Integer getId() { return this.Id; } public String getName() { return this.name; } public Double getPrice() { return this.price; } }
如今咱們要給 Java 架構課程作活動,價格優惠.若是修改 JavaCourse 中的 getPrice()方法,則會存在必定的風險,可能影響其餘地方的調用結果.咱們如何在不修改原有代碼前提早下,實現價格優惠這個功能呢?如今,咱們再寫一個處理優惠邏輯的類,JavaDiscountCourse 類(思考一下爲何要叫 JavaDiscountCourse,而不叫 DiscountCourse):框架
public class JavaDiscountCourse extends JavaCourse { public JavaDiscountCourse(Integer id, String name, Double price) { super(id, name, price); } public Double getOriginPrice(){ return super.getPrice(); } public Double getPrice(){ return super.getPrice() * 0.61; } }
回顧一下,簡單一下類結構圖:ide
依賴倒置原則(DependenceInversionPrinciple,DIP)是指設計代碼結構時,高層模塊不該該依賴底層模塊,兩者都應該依賴其抽象.抽象不該該依賴細節;細節應該依賴抽象.經過依賴倒置,能夠減小類與類之間的耦合性,提升系統的穩定性,提升代碼的可讀性和可維護性,並可以下降修改程序所形成的風險.接下來看一個案例,仍是以課程爲例,先來建立一個類 Tom:函數
public class Tom { public void studyJavaCourse() { System.out.println("Tom 在學習 Java 的課程"); } public void studyPythonCourse() { System.out.println("Tom 在學習 Python 的課程"); } }
來調用一下:學習
public static void main(String[] args) { Tom tom = new Tom(); tom.studyJavaCourse(); tom.studyPythonCourse(); }
Tom 熱愛學習,目前正在學習 Java 課程和 Python 課程.你們都知道,學習也是會上癮的.隨着學習興趣的暴漲,如今 Tom 還想學習 AI 人工智能的課程.這個時候,業務擴展,咱們的代碼要從底層到高層(調用層)一次修改代碼.在 Tom 類中增長 studyAICourse()的方法,在高層也要追加調用.如此一來,系統發佈之後,其實是很是不穩定的,在修改代碼的同時也會帶來意想不到的風險.接下來咱們優化代碼,建立一個課程的抽象 ICourse 接口:測試
public interface ICourse { void study(); }
而後寫 JavaCourse 類:
public class JavaCourse implements ICourse { @Override public void study() { System.out.println("Tom 在學習 Java 課程"); } }
再實現 PythonCourse 類:
public class PythonCourse implements ICourse { @Override public void study() { System.out.println("Tom 在學習 Python 課程"); } }
修改 Tom 類:
public class Tom { public void study(ICourse course){ course.study(); } }
來看調用:
public static void main(String[] args) { Tom tom = new Tom(); tom.study(new JavaCourse()); tom.study(new PythonCourse()); }
咱們這時候再看來代碼,Tom 的興趣不管怎麼暴漲,對於新的課程,我只須要新建一個類,經過傳參的方式告訴 Tom,而不須要修改底層代碼.實際上這是一種你們很是熟悉的方式,叫依賴注入.注入的方式還有構造器方式和 setter 方式.咱們來看構造器注入方式:
public class Tom { private ICourse course; public Tom(ICourse course){ this.course = course; } public void study(){ course.study(); } }
看調用代碼:
public static void main(String[] args) { Tom tom = new Tom(new JavaCourse()); tom.study(); }
根據構造器方式注入,在調用時,每次都要建立實例.那麼,若是 Tom 是全局單例,則咱們就只能選擇用 Setter 方式來注入,繼續修改 Tom 類的代碼:
public class Tom { private ICourse course; public void setCourse(ICourse course) { this.course = course; } public void study(){ course.study(); } }
看調用代碼:
public static void main(String[] args) { Tom tom = new Tom(); tom.setCourse(new JavaCourse()); tom.study(); tom.setCourse(new PythonCourse()); tom.study(); }
如今咱們再來看最終的類圖:
你們要切記:以抽象爲基準比以細節爲基準搭建起來的架構要穩定得多,所以你們在拿到需求以後,要面向接口編程,先頂層再細節來設計代碼結構
單一職責(SimpleResponsibilityPinciple,SRP)是指不要存在多於一個致使類變動的緣由.假設咱們有一個 Class 負責兩個職責,一旦發生需求變動,修改其中一個職責的邏輯代碼,有可能會致使另外一個職責的功能發生故障.這樣一來,這個 Class 存在兩個致使類變動的緣由.如何解決這個問題呢?咱們就要給兩個職責分別用兩個 Class 來實現,進行解耦.後期需求變動維護互不影響.這樣的設計,能夠下降類的複雜度,提升類的可讀性,提升系統的可維護性,下降變動引發的風險.整體來講就是一個 Class/Interface/Method 只負責一項職責
接下來,咱們來看代碼實例,仍是用課程舉例,咱們的課程有直播課和錄播課.直播課不能快進和快退,錄播能夠能夠任意的反覆觀看,功能職責不同.仍是先建立一個 Course 類:
public class Course { public void study(String courseName) { if ("直播課".equals(courseName)) { System.out.println("不能快進"); } else { System.out.println("能夠任意的來回播放"); } } }
看代碼調用:
public static void main(String[] args) { Course course = new Course(); course.study("直播課"); course.study("錄播課"); }
從上面代碼來看,Course 類承擔了兩種處理邏輯.假如,如今要對課程進行加密,那麼直播課和錄播課的加密邏輯都不同,必需要修改代碼.而修改代碼邏輯勢必會相互影響容易形成不可控的風險.咱們對職責進行分離解耦,來看代碼,分別建立兩個類 ReplayCourse 和 LiveCourse:
LiveCourse 類:
public class LiveCourse { public void study(String courseName) { System.out.println(courseName + "不能快進看"); } }
ReplayCourse 類:
public class ReplayCourse { public void study(String courseName) { System.out.println("能夠任意的來回播放"); } }
調用代碼:
public static void main(String[] args) { LiveCourse liveCourse = new LiveCourse(); liveCourse.study("直播課"); ReplayCourse replayCourse = new ReplayCourse(); replayCourse.study("錄播課"); }
業務繼續發展,課程要作權限.沒有付費的學員能夠獲取課程基本信息,已經付費的學員能夠得到視頻流,即學習權限.那麼對於控制課程層面上至少有兩個職責.咱們能夠把展現職責和管理職責分離開來,都實現同一個抽象依賴.設計一個頂層接口,建立 ICourse 接口:
public interface ICourse { //得到基本信息 String getCourseName(); //得到視頻流 byte[] getCourseVideo(); //學習課程 void studyCourse(); //退款 void refundCourse(); }
咱們能夠把這個接口拆成兩個接口,建立一個接口 ICourseInfo 和 ICourseManager:
ICourseInfo 接口:
public interface ICourseInfo { String getCourseName(); byte[] getCourseVideo(); }
ICourseManager 接口:
public interface ICourseManager { void studyCourse(); void refundCourse(); }
來看一下類圖:
下面咱們來看一下方法層面的單一職責設計.有時候,咱們爲了偷懶,一般會把一個方法寫成下面這樣:
private void modifyUserInfo(String userName,String address) { userName = "Tom"; address = "Changsha"; }
還可能寫成這樣:
private void modifyUserInfo(String userName,String... fileds) { userName = "Tom"; // address = "Changsha"; } private void modifyUserInfo(String userName,String address,boolean bool) { if (bool) { } else{ } userName = "Tom"; address = "Changsha"; }
顯然,上面的 modifyUserInfo()方法中都承擔了多個職責,既能夠修改 userName,也能夠修改 address,甚至更多,明顯不符合單一職責.那麼咱們作以下修改,把這個方法拆成兩個:
private void modifyUserName(String userName){ userName = "Tom"; } private void modifyAddress(String address){ address = "Changsha"; }
這修改以後,開發起來簡單,維護起來也容易.可是,咱們在實際開發中會項目依賴,組合,聚合這些關係,還有還有項目的規模,週期,技術人員的水平,對進度的把控,不少類都不符合單一職責.可是,咱們在編寫代碼的過程,儘量地讓接口和方法保持單一職責,對咱們項目後期的維護是有很大幫助的
接口隔離原則(InterfaceSegregationPrinciple,ISP)是指用多個專門的接口,而不使用單一的總接口,客戶端不該該依賴它不須要的接口.這個原則指導咱們在設計接口時應當注意一下幾點:
接口隔離原則符合咱們常說的高內聚低耦合的設計思想,從而使得類具備很好的可讀性,可擴展性和可維護性.咱們在設計接口的時候,要多花時間去思考,要考慮業務模型,包括之後有可能發生變動的地方還要作一些預判.因此,對於抽象,對業務模型的理解是很是重要的.下面咱們來看一段代碼,寫一個動物行爲的抽象:
IAnimal 接口:
public interface IAnimal { void eat(); void fly(); void swim(); }
Bird 類實現:
public class Bird implements IAnimal { @Override public void eat() {} @Override public void fly() {} @Override public void swim() {} }
Dog 類實現:
public class Dog implements IAnimal { @Override public void eat() {} @Override public void fly() {} @Override public void swim() {} }
能夠看出,Bird 的 swim()方法可能只能空着,Dog 的 fly()方法顯然不可能的.這時候,咱們針對不一樣動物行爲來設計不一樣的接口,分別設計 IEatAnimal, IFlyAnimal 和 ISwimAnimal 接口,來看代碼:
IEatAnimal 接口:
public interface IEatAnimal { void eat(); }
IFlyAnimal 接口:
public interface IFlyAnimal { void fly(); }
ISwimAnimal 接口:
public interface ISwimAnimal { void swim(); }
Dog 只實現 IEatAnimal 和 ISwimAnimal 接口:
public class Dog implements ISwimAnimal,IEatAnimal { @Override public void eat() {} @Override public void swim() {} }
來看下兩種類圖的對比,仍是很是清晰明瞭的:
迪米特原則(LawofDemeterLoD)是指一個對象應該對其餘對象保持最少的瞭解,又叫最少知道原則(LeastKnowledgePrinciple,LKP),儘可能下降類與類之間的耦合.迪米特原則主要強調只和朋友交流,不和陌生人說話.出如今成員變量,方法的輸入,輸出參數中的類均可以稱之爲成員朋友類,而出如今方法體內部的類不屬於朋友類.
如今來設計一個權限系統,TeamLeader 須要查看目前發佈到線上的課程數量.這時候,TeamLeader 要找到員工 Employee 去進行統計,Employee 再把統計結果告訴 TeamLeader.接下來咱們仍是來看代碼:
Course 類:
public class Course { }
Employee 類:
public class Employee{ public void checkNumberOfCourses(List<Course> courseList){ System.out.println("目前已發佈的課程數量是:" + courseList.size()); } }
TeamLeader 類:
public class TeamLeader{ public void commandCheckNumber(Employee employee){ List<Course> courseList = new ArrayList<Course>(); for (int i= 0; i < 20 ;i ++){ courseList.add(new Course()); } employee.checkNumberOfCourses(courseList); } }
測試代碼:
public static void main(String[] args) { TeamLeader teamLeader = new TeamLeader(); Employee employee = new Employee(); teamLeader.commandCheckNumber(employee); }
寫到這裏,其實功能已經都已經實現,代碼看上去也沒什麼問題.根據迪米特原則,TeamLeader 只想要結果,不須要跟 Course 產生直接的交流.而 Employee 統計須要引用 Course 對象.TeamLeader 和 Course 並非朋友,從下面的類圖就能夠看出來:
下面來對代碼進行改造:
Employee 類:
public class Employee { public void checkNumberOfCourses(){ List<Course> courseList = new ArrayList<Course>(); for (int i= 0; i < 20 ;i ++){ courseList.add(new Course()); } System.out.println("目前已發佈的課程數量是:"+courseList.size()); } }
TeamLeader 類:
public class TeamLeader { public void commandCheckNumber(Employee employee){ employee.checkNumberOfCourses(); } }
再來看下面的類圖,Course 和 TeamLeader 已經沒有關聯了.
學習軟件設計原則,千萬不能造成強迫症.碰到業務複雜的場景,咱們須要隨機應變
里氏替換原則(LiskovSubstitutionPrinciple,LSP)是指若是對每個類型爲 T1 的對象 o1,都有類型爲 T2 的對象 o2,使得以 T1 定義的全部程序 P 在全部的對象 o1 都替換成 o2 時,程序 P 的行爲沒有發生變化,那麼類型 T2 是類型 T1 的子類型.
定義看上去仍是比較抽象,咱們從新理解一下,能夠理解爲一個軟件實體若是適用一個父類的話,那必定是適用於其子類,全部引用父類的地方必須能透明地使用其子類的對象,子類對象可以替換父類對象,而程序邏輯不變.根據這個理解,咱們總結一下:
引伸含義:子類能夠擴展父類的功能,但不能改變父類原有的功能.
在前面講開閉原則的時候埋下了一個伏筆,咱們記得在獲取折後時重寫覆蓋了父類的 getPrice()方法,增長了一個獲取源碼的方法 getOriginPrice(),顯然就違背了里氏替換原則.咱們修改一下代碼,不該該覆蓋 getPrice()方法,增長 getDiscountPrice()方法:
public class JavaDiscountCourse extends JavaCourse { public JavaDiscountCourse(Integer id, String name, Double price) { super(id, name, price); } public Double getDiscountPrice(){ return super.getPrice() * 0.61; } }
使用里氏替換原則有如下優勢:
增強程序的健壯性,同時變動時也能夠作到很是好的兼容性,提升程序的維護性,擴展性.下降需求變動時引入的風險.如今來描述一個經典的業務場景,用正方形,矩形和四邊形的關係說明裏氏替換原則,咱們都知道正方形是一個特殊的長方形,那麼就能夠建立一個長方形父類 Rectangle 類:
public class Rectangle { private long height;
private long width;
public long getHeight() {
return height;
}
public void setHeight(long height) {
this.height = height;
}
public long getWidth() {
return width;
}
public void setWidth(long width) {
this.width = width;
}
}
建立正方形 Square 類繼承長方形:
public class Square extends Rectangle { private long length; public long getLength() { return length; } public void setLength(long length) { this.length = length; } @Override public long getHeight() { return getLength(); } @Override public void setHeight(long height) { setLength(height); } @Override public long getWidth() { return getLength(); } @Override public void setWidth(long width) { setLength(width); } }
在測試類中建立 resize()方法,根據邏輯長方形的寬應該大於等於高,咱們讓高一直自增,知道高等於寬變成正方形:
public static void resize(Rectangle rectangle){ while (rectangle.getWidth() >= rectangle.getHeight()){ rectangle.setHeight(rectangle.getHeight() + 1); System.out.println("Width:" +rectangle.getWidth() +",Height:" + rectangle.getHeight()); } System.out.println("Resize End,Width:" +rectangle.getWidth() +",Height:" + rectangle.getHeight()); }
測試代碼:
public static void main(String[] args) { Rectangle rectangle = new Rectangle(); rectangle.setWidth(20); rectangle.setHeight(10); resize(rectangle); }
運行結果:
發現高比寬還大了,在長方形中是一種很是正常的狀況.如今咱們再來看下面的代碼,把長方形 Rectangle 替換成它的子類正方形 Square,修改測試代碼:
public static void main(String[] args) { Square square = new Square(); square.setLength(10); resize(square); }
這時候咱們運行的時候就出現了死循環,違背了里氏替換原則,將父類替換爲子類後,程序運行結果沒有達到預期.所以,咱們的代碼設計是存在必定風險的.里氏替換原則只存在父類與子類之間,約束繼承氾濫.咱們再來建立一個基於長方形與正方形共同的抽象四邊形 Quadrangle 接口:
public interface QuadRangle { long getWidth(); long getHeight(); }
修改長方形 Rectangle 類:
public class Rectangle implements QuadRangle { private long height; private long width; public long getHeight() { return height; } public void setHeight(long height) { this.height = height; } public long getWidth() { return width; } public void setWidth(long width) { this.width = width; } }
修改正方形類 Square 類:
public class Square implements QuadRangle { private long length; public long getLength() { return length; } public void setLength(long length) { this.length = length; } public long getWidth() { return length; } public long getHeight() { return length; } }
此時,若是咱們把 resize()方法的參數換成四邊形 Quadrangle 類,方法內部就會報錯.由於正方形 Square 已經沒有了 setWidth()和 setHeight()方法了.所以,爲了約束繼承濫,resize()的方法參數只能用 Rectangle 長方形.固然,咱們在後面的設計模式課程中還會繼續深刻講解
public interface IAnimal { void eat(); void fly(); void swim(); }
Bird 類實現:
public class Bird implements IAnimal { @Override public void eat() {} @Override public void fly() {} @Override public void swim() {} }
Dog 類實現:
public class Dog implements IAnimal { @Override public void eat() {} @Override public void fly() {} @Override public void swim() {} }
能夠看出,Bird 的 swim()方法可能只能空着,Dog 的 fly()方法顯然不可能的.這時候,咱們針對不一樣動物行爲來設計不一樣的接口,分別設計 IEatAnimal,IFlyAnimal 和 ISwimAnimal 接口,來看代碼:
IEatAnimal 接口:
public interface IEatAnimal { void eat(); }
IFlyAnimal 接口:
public interface IFlyAnimal { void fly(); }
ISwimAnimal 接口:
public interface ISwimAnimal { void swim(); }
Dog 只實現 IEatAnimal 和 ISwimAnimal 接口:
public class Dog implements ISwimAnimal,IEatAnimal { @Override public void eat() {} @Override public void swim() {} }
來看下兩種類圖的對比,仍是很是清晰明瞭的: