一、開閉原則
開閉原則(Open-Closed Principle, OCP)是指一個軟件實體如類、模塊和函數應該對 擴展開放,對修改關閉。
所謂的開閉,也正是對擴展和修改兩個行為的一個原則。強調 的是用抽象構建框架,用實現擴展細節。
可以提高軟件系統的可復用性及可維護性。開 閉原則,是面向對象設計中最基礎的設計原則。它指導我們如何建立穩定靈活的系統,實現開閉原則的核心思想就是面向抽象編程。
二、依賴倒置原則
依賴倒置原則(Dependence Inversion Principle,DIP)是指設計代碼結構時,高層模 塊不應該依賴底層模塊,二者都應該依賴其抽象。
通過依賴倒置,可以減少類與類之間的耦合性,提高系統的穩定性,提高代碼的 可讀性和可維護性,並能夠降低修改程序所造成的風險。
下面我們通過一個例子來具體闡述:
首先創建一個MyCar類,假如你現在有兩輛車
public class MyCar { public void AudiCarDriving() { System.out.println("AudiCar is drving ……"); } public void BWMCarDriving() { System.out.println("BWMCar is drving ……"); } }
但是隨着你經濟的逐漸飆升,又買下一輛車,這個時候,業務擴展,我們的代碼要從底層到高層(調用層)依次修改代碼。在 MyCar 類中增加 xxxCarDriving()的方法,在高層也要追加調用。如此一來,系統發布以后,實際上是非 常不穩定的,在修改代碼的同時也會帶來意想不到的風險。接下來我們優化代碼,創建 一個課程的抽象 CarDriving 接口:
public interface CarDriving { void driving(); } /**
* 創建AudiCar 類
*/ public class AudiCar implements CarDriving { @Override public void driving() { System.out.println(this.getClass().getSimpleName()+"is drving ……"); } } /**
* 創建BWMCar類
*/ public class BWMCar implements CarDriving { @Override public void driving() { System.out.println(this.getClass().getSimpleName()+"is driving ……"); } } /**
* 優化之前的MyCar 類
*/ public class MyCar { public void driving(CarDriving carDriving) { carDriving.driving(); } } //調用 public static void main(String[] args) { MyCar myCar = new MyCar(); myCar.driving(new AudiCar()); myCar.driving(new BWMCar()); }
我們這時候再看來代碼,無論你有多少車,對於新車,我只需要新建一 類,通過傳參的方式告訴MyCar,而不需要修改底層代碼。實際上這是一種大家非常熟悉 的方式,叫依賴注入。注入的方式還有構造器方式和 setter 方式。我們來看構造器注入 方式:
/** * @Description 構造器注入 * @Date 2019\5\20 */ public class MyCar_01 { private CarDriving carDriving; public MyCar_01(CarDriving carDriving) { this.carDriving = carDriving; } public void driving() { carDriving.driving(); } } /** * @Description 調用的時候一樣 */ public static void main(String[] args) { new MyCar_01(new AudiCar()).driving(); new MyCar_01(new BWMCar()).driving(); }
根據構造器方式注入,在調用時,每次都要創建實例。那么,如果 MyCar 是全局單例,則 我們就只能選擇用 Setter 方式來注入,繼續修改 MyCar 類的代碼:
/** * @Description setter 注入 * @Date 2019\5\20 */ public class MyCar_02 { private CarDriving carDriving; public void setCarDriving(CarDriving carDriving) { this.carDriving = carDriving; } public void driving() { carDriving.driving(); } } /* * @Description 調用方法如下 */ public static void main(String[] args) { MyCar_02 myCar = new MyCar_02(); myCar.setCarDriving(new AudiCar()); myCar.driving(); myCar.setCarDriving(new BWMCar()); myCar.driving(); }
執行結果
AudiCaris drving …… BWMCaris driving ……
三、單一職責原則
單一職責(Simple Responsibility Pinciple,SRP)是指不要存在多於一個導致類變更的原因。
假設我們有一個 Class 負責兩個職責,一旦發生需求變更,修改其中一個職責的邏輯代碼,有可能會導致另一個職責的功能發生故障。這樣一來,這個 Class 存在兩個導 致類變更的原因。如何解決這個問題呢?我們就要給兩個職責分別用兩個 Class 來實現, 進行解耦。后期需求變更維護互不影響。這樣的設計,可以降低類的復雜度,提高類的 可 讀 性 , 提高系統的可維護性,降低變更引起的風險。總體來說就是一個Class/Interface/Method 只負責一項職責。
public class DriveCar { public void driving(String carType){ if("SUV".equals(carType)){ System.out.println(carType+"動力強悍,適合越野"); }else { System.out.println(carType+"乘坐舒適"); } } } /*調用如下*/ public static void main(String[] args) { DriveCar driveCar = new DriveCar(); driveCar.driving("SUV"); driveCar.driving("MPV"); }
從上面代碼來看,DriveCar 類承擔了兩種處理邏輯。假如,現在要對SUV做更多處理,那么 SUV和MPV的大小、功能都不一樣,必須要修改代碼。而修改代碼邏輯勢必會相互影 響容易造成不可控的風險。我們對職責進行分離解耦,來看代碼,分別創建兩個類 SUVDrive 和 MPVDrive:
public class SUVDrive { public void driving(String carType){ System.out.println(carType + "動力強悍,適合越野"); } } public class MPVDrive { public void driving(String carType){ System.out.println(carType + "乘坐舒適"); } } /*調用如下*/ public static void main(String[] args) { SUVDrive suv = new SUVDrive(); suv.driving("SUV"); MPVDrive mpv = new MPVDrive(); mpv.driving("MPV"); }
如果有跟多的業務需要,可以設計一個頂層接口,然后再根據情況拆分成不同的接口,使其滿足單一職責原則,便於后期維護
四、接口隔離原則
接口隔離原則(Interface Segregation Principle, ISP)是指用多個專門的接口,而不使 用單一的總接口,客戶端不應該依賴它不需要的接口。
這個原則指導我們在設計接口時 應當注意一下幾點:
1、一個類對一類的依賴應該建立在最小的接口之上。
2、建立單一接口,不要建立龐大臃腫的接口。
3、盡量細化接口,接口中的方法盡量少(不是越少越好,一定要適度)。 接口隔離原則符合我們常說的高內聚低耦合的設計思想,從而使得類具有很好的可讀性、 可擴展性和可維護性。我們在設計接口的時候,要多花時間去思考,要考慮業務模型, 包括以后有可能發生變更的地方還要做一些預判。所以,對於抽象,對業務模型的理解 是非常重要的。
public interface IAnimal { void eat(); void fly(); void swim(); } /** * @Description Bird 類實現: */ public class Bird implements IAnimal{ @Override public void eat() { } @Override public void fly() { } @Override public void swim() { } } /** * @Description 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 接口,來看代碼:
public interface IEatAnimal{ void eat(); } public interface IFlyAnimal { void fly(); } public interface ISwimAnimal { void swim(); } /** * @Description Dog 只實現 IEatAnimal 和 ISwimAnimal 接口 */ public class Dog implements ISwimAnimal,IEatAnimal { @Override public void eat() {} @Override public void swim() {} }
五、迪米特法則
迪米特原則(Law of Demeter LoD)是指一個對象應該對其他對象保持最少的了解,又 叫最少知道原則(Least Knowledge Principle,LKP),盡量降低類與類之間的耦合。
迪米特原則主要強調只和朋友交流,不和陌生人說話。出現在成員變量、方法的輸入、輸 出參數中的類都可以稱之為成員朋友類,而出現在方法體內部的類不屬於朋友類。現在來設計一個權限系統,Boss 需要查看目前發布到線上的課程數量。這時候,Boss 要找到 TeamLeader 去進行統計,TeamLeader 再把統計結果告訴 Boss。接下來我們還是來看代碼:
public class Course { } import java.util.List; public class TeamLeader { public void checkNumberOfCourses(List<Course> courseList){ System.out.println("目前已發布的課程數量是:"+courseList.size()); } } import java.util.ArrayList; import java.util.List; public class Boss { public void commandCheckNumber(TeamLeader teamLeader){ //模擬 Boss 一頁一頁往下翻頁,TeamLeader 實時統計 List<Course> courseList = new ArrayList<Course>(); for (int i= 0; i < 20 ;i ++){ courseList.add(new Course()); } teamLeader.checkNumberOfCourses(courseList); } } public static void main(String[] args) { Boss boss = new Boss(); TeamLeader teamLeader = new TeamLeader(); boss.commandCheckNumber(teamLeader); }
寫到這里,其實功能已經都已經實現,代碼看上去也沒什么問題。根據迪米特原則,Boss 只想要結果,不需要跟 Course 產生直接的交流。而 TeamLeader 統計需要引用 Course 對象。Boss 和 Course 並不是朋友,從下面的類圖就可以看出來
下面來對代碼進行改造:
import java.util.ArrayList; import java.util.List; public class TeamLeader { 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()); } } public class Boss { public void commandCheckNumber(TeamLeader teamLeader){ teamLeader.checkNumberOfCourses(); } }
再來看下面的類圖,Course 和 Boss 已經沒有關聯了
六、里氏替換原則
里氏替換原則(Liskov Substitution Principle,LSP)是指如果對每一個類型為 T1 的對 象 o1,都有類型為 T2 的對象 o2,使得以 T1 定義的所有程序 P 在所有的對象 o1 都替換成 o2 時,程序 P 的行為沒有發生變化,那么類型 T2 是類型 T1 的子類型。 定義看上去還是比較抽象,我們重新理解一下,可以理解為一個軟件實體如果適用一個 父類的話,那一定是適用於其子類,所有引用父類的地方必須能透明地使用其子類的對 象,子類對象能夠替換父類對象,而程序邏輯不變。根據這個理解,我們總結一下: 引申含義:子類可以擴展父類的功能,但不能改變父類原有的功能。
1、子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法。
2、子類中可以增加自己特有的方法。
3、當子類的方法重載父類的方法時,方法的前置條件(即方法的輸入/入參)要比父類 方法的輸入參數更寬松。
4、當子類的方法實現父類的方法時(重寫/重載或實現抽象方法),方法的后置條件(即 方法的輸出/返回值)要比父類更嚴格或相等。
使用里氏替換原則有以下優點: 1、約束繼承泛濫,開閉原則的一種體現。 2、加強程序的健壯性,同時變更時也可以做到非常好的兼容性,提高程序的維護性、擴 展性。降低需求變更時引入的風險。 現在來描述一個經典的業務場景,用正方形、矩形和四邊形的關系說明里氏替換原則, 我們都知道正方形是一個特殊的長方形,那么就可以創建一個長方形父類 Rectangle 類:
/** * @Description 創建一個長方形父類 Rectangle 類 */ public class Rectangle { private long height; private long width; public long getWidth() { return width; } public void setWidth(long width) { this.width = width; } public long getHeight() { return height; } public void setHeight(long height) { this.height = height; } } /** * @Description 創建正方形 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 getWidth() { return getLength(); } @Override public long getHeight() { return getLength(); } @Override public void setHeight(long height) { setLength(height); } @Override public void setWidth(long width) { setLength(width); } } public class LspMain { 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 方法結束" + "\nwidth:"+rectangle.getWidth() + ",height:"+rectangle.getHeight()); } public static void main(String[] args) { Rectangle rectangle = new Rectangle(); rectangle.setWidth(20); rectangle.setHeight(10); resize(rectangle); } }
運行結果:
width:20,height:11 width:20,height:12 width:20,height:13 width:20,height:14 width:20,height:15 width:20,height:16 width:20,height:17 width:20,height:18 width:20,height:19 width:20,height:20 width:20,height:21 resize 方法結束 width:20,height:21
發現高比寬還大了,在長方形中是一種非常正常的情況。現在我們再來看下面的代碼, 把長方形 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(); } /** * @Description 修改 Rectangle 類 */ public class Rectangle implements Quadrangle{ private long height; private long width; @Override public long getWidth() { return width; } public long getHeight() { return height; } public void setHeight(long height) { this.height = height; } public void setWidth(long width) { this.width = width; } } /** * @Description 修改 Square 類 */ public class Square implements Quadrangle{ private long length; public long getLength() { return length; } public void setLength(long length) { this.length = length; } @Override public long getWidth() { return length; } @Override public long getHeight() { return length; } }
此時,如果我們把 resize()方法的參數換成四邊形 Quadrangle 類,方法內部就會報錯。 因為正方形 Square 已經沒有了 setWidth()和 setHeight()方法了。因此,為了約束繼承 泛濫,resize()的方法參數只能用 Rectangle 長方形
七、合成復用原則
合成復用原則(Composite/Aggregate Reuse Principle,CARP)是指盡量使用對象組 合(has-a)/聚合(contanis-a),而不是繼承關系達到軟件復用的目的。可以使系統更加靈 活,降低類與類之間的耦合度,一個類的變化對其他類造成的影響相對較少。 繼承我們叫做白箱復用,相當於把所有的實現細節暴露給子類。組合/聚合也稱之為黑箱 復用,對類以外的對象是無法獲取到實現細節的。要根據具體的業務場景來做代碼設計, 其實也都需要遵循 OOP 模型。還是以數據庫操作為例,先來創建 DBConnection 類:
public class DBConnection { public String getConnection(){ return "MySQL 數據庫連接"; } } public class ProductDao { private DBConnection dbConnection; public void setDbConnection(DBConnection dbConnection) { this.dbConnection = dbConnection; } public void addProduct(){ String conn = dbConnection.getConnection(); System.out.println("使用"+conn+"增加產品"); } }
這就是一種非常典型的合成復用原則應用場景。但是,目前的設計來說,DBConnection 還不是一種抽象,不便於系統擴展。目前的系統支持 MySQL 數據庫連接,假設業務發生 變化,數據庫操作層要支持 Oracle 數據庫。當然,我們可以在 DBConnection 中增加對 Oracle 數據庫支持的方法。但是違背了開閉原則。其實,我們可以不必修改 Dao 的代碼, 將 DBConnection 修改為 abstract,來看代碼:
public abstract class DBConnection { public abstract String getConnection(); } /** * @Description 將 MySQL 的邏輯抽離 */ public class MySQLConnection extends DBConnection { @Override public String getConnection() { return "MySQL 數據庫連接"; } } /** * @Description 創建 Oracle 支持的邏輯 */ public class OracleConnection extends DBConnection { @Override public String getConnection() { return "Oracle 數據庫連接"; } }
具體選擇交給應用層,來看一下類圖
設計原則總結
學習設計原則,學習設計模式的基礎。在實際開發過程中,並不是一定要求所有代碼都 遵循設計原則,我們要考慮人力、時間、成本、質量,不是刻意追求完美,要在適當的 場景遵循設計原則,體現的是一種平衡取舍,幫助我們設計出更加優雅的代碼結構。