前言
今天開始我們專題的第二課了,本章節繼續分享軟件架構設計原則的下篇,將介紹:接口隔離原則、迪米特原則、里氏替換原則和合成複用原則。本章節參考資料書籍《Spring 5核心原理》中的第一篇 Spring 內功心法(沒有電子檔,都是我取其精華並結合自己的理解,一個字一個字手敲出來的)。
接口隔離原則
接口隔離原則(Interface Segregation Principke,ISP)是指用多個專門的接口,而不使用單一的總接口,客戶端不應該依賴它不需要的接口。這個原則知道我們在設計接口時應當注意以下幾點:
(1)一個類對另一個類的依賴應該建立在最小接口之上。
(2)建立單一接口,不要建立龐大臃腫的接口。
(3)儘量細化接口,接口中的方法儘量少(不是越少越好,一定要適度)。
接口隔離原則符合我們常說的高內聚、低耦合的設計思想,可以使類有很好的可讀性、可擴展性和可維護性。我們在設計接口的時候,要多花時間去思考,要考慮業務模型,包括對以後可能發生變化的地方做一些預判。所以,對於抽象、對於業務模型的理解是非常重要的。
下面我們來看一段代碼,對一個動物行爲進行抽象描述。
//描述動物行爲的接口
public interface IAnimal {
void eat();
void fly();
void swim();
}
//鳥類
public class Bird implements IAnimal {
public void eat() {
}
public void fly() {
}
public void swim() {
}
}
//狗
public class Dog implements IAnimal {
public void eat() {
}
public void fly() {
}
public void swim() {
}
}
可以看出,Brid的swim()方法只能空着,並且Dog的fly()方法顯然不可能的。這時候,我們針對不同動物行爲來設計不同的接口,分別設計IEatAnimal、IFlyAnimal和ISwimAnimal接口,來看代碼:
public interface IEatAnimal {
void eat();
}
public interface IFlyAnimal {
void fly();
}
public interface ISwimAnimal {
void swim();
}
此時Dog只需要實現IEatAnimal和ISwimAnimal接口即可,這樣就清晰明瞭了。
public class Dog implements IEatAnimal,ISwimAnimal {
public void eat() {
}
public void swim() {
}
}
迪米特原則
迪米特原則(Law of Demeter LoD)是指一個對象應該對其他對象保持最少的瞭解,又叫最少知道原則(Least Knowledge Principle,LKP),儘量降低類與類之間的耦合度。迪米特原則主要強調:只和朋友交流,不和陌生人說話。出現在成員變量、方法的輸入、輸出參數中的類可以稱爲成員朋友類,而出現在方法體內部的類不屬於朋友類。
現在設計一個權限系統,Boss需要查看目前發佈到線上的課程數量。這時候,Boss要找到TeamLeader進行統計,TeamLeader再把統計結果告訴Boss,接下來我們來看看代碼:
//課程類
public class Course {
}
//TeamLeader類
public class TeamLeader {
public void checkNumberOfCourses(List<Course> courses){
System.out.println("目前已經發布的課程數量:"+courses.size());
}
}
//Boss類
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並不是朋友,從下面的類圖可以看出來:
下面對代碼進行改造:
//TeamLeader做與course的交流
public class TeamLeader {
public void checkNumberOfCourses(){
List<Course> courses = new ArrayList<Course>();
for (int i = 0; i < 20; i++) {
courses.add(new Course());
}
System.out.println("目前已經發布的課程數量:"+courses.size());
}
}
//Boss直接與TeamLeader交流,不再直接與Course交流
public class Boss {
public void commandCheckNumber(TeamLeader teamLeader){
//模擬BOSS一頁一頁往下翻頁,TeamLeader實時統計
teamLeader.checkNumberOfCourses();
}
}
再看下類圖,Boss與Course已經沒有聯繫了
這裏切記,學習軟件設計規則,千萬不能形成強迫症,碰到業務複雜的場景,我們需要隨機應變。
里氏替換原則
里氏替換原則(Liskov Substitution Priciple,LSP)是指如果對每一個類型爲T1的對象O1,都有類型爲T2的對象O2,使得以T1定義的所有程序P在所有對象O1都替換成O2時,程序P的行爲沒有發生變化,那麼類型T2是類型T1的子類型。
這個定義看上去還是比較抽象的,我們要重新理解一下。可以理解爲一個軟件實體如果適用於一個父類,那麼一定適用其子類,所以引用父類的地方必須能透明的使用其子類的對象,子類對象能夠替換父類對象,而程序邏輯不變,根據這個理解,引申含義爲:子類可以擴展父類的功能,但不能改變父類原有的功能。
(1)子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法。
(2)子類可以增加自己特有的方法。
(3)當子類的方法重載父類的方法時,方法的前置條件(即方法的輸入、入參)要比父類的方法輸入參數更寬鬆。
(4)當子類的方法實現父類的方法時(重寫、重載或實現抽象方法),方法的後置條件(即方法的輸出、返回值)要比父類更嚴格或與父類一樣。
使用里氏替換原則有以下優點:
(1)約束繼承氾濫,是開閉原則的一種體現。
(2)加強程序的健壯性,同事變更時也可以做到非常好的兼容性,提高程序的可維護性和擴展性,降低需求變成時引入的風險。
現在來描述一個經典的業務場景,用正方形、矩形和四邊形的關係說明裏氏替換原則,我們都知道正方形一個特殊的矩形,所以就可以創建一個父類Rectangle:
//矩形類
public class Rectangle {
private long hight;
private long width;
public long getHight() {
return hight;
}
public void setHight(long hight) {
this.hight = hight;
}
public long getWidth() {
return width;
}
public void setWidth(long width) {
this.width = width;
}
}
//正方形類
public class Square extends Rectangle {
private long length;
public long getLength() {
return length;
}
public void setLength(long length) {
this.length = length;
}
@Override
public long getHight() {
return super.getHight();
}
@Override
public void setHight(long hight) {
super.setHight(hight);
}
@Override
public long getWidth() {
return super.getWidth();
}
@Override
public void setWidth(long width) {
super.setWidth(width);
}
}
public class DemoTest {
//在測試類中創建resize方法,長方形的寬應該大於等於高,我們讓高一直增加,直至高等於寬,變成正方形。
public static void resize(Rectangle rectangle) {
while (rectangle.getWidth() >= rectangle.getHight()){
rectangle.setHight(rectangle.getHight()+1);
System.out.println("寬度:"+rectangle.getWidth()+"高度:"+rectangle.getHight());
}
System.out.println("resize方法結束!");
}
//測試代碼如下
public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
rectangle.setHight(10);
rectangle.setWidth(20);
resize(rectangle);
}
看下控制檯輸出,發現高度最後大於了寬度,這個情況在正方形中是正常的情況,現在我們把Rectangle類替換成它的子類的話,就不符合邏輯了,違背了里氏替換原則,將父類替換成子類後,程序運行結果沒有達到預期。因此,我們的代碼設計存在一定的風險的。里氏替換原則只存在於父類與子類之間,約束繼承氾濫。我們再來創建一個基於正方形和長方形共同的抽象接口四邊形接口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 long getWidth() {
return width;
}
public void setHeight(long height) {
this.height = height;
}
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 0;
}
public long getHeight() {
return 0;
}
}
此時,如果我們把 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 數據庫連接";
}
}
創建 ProductDao 類:
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();
}
然後,將 MySQL 的邏輯抽離:
public class MySQLConnection extends DBConnection {
@Override
public String getConnection() {
return "MySQL 數據庫連接";
}
}
再創建 Oracle 支持的邏輯:
public class OracleConnection extends DBConnection {
@Override
public String getConnection() {
return "Oracle 數據庫連接";
}
}
具體選擇交給應用層,來看一下類圖:
設計原則總結
學習設計原則,學習設計模式的基礎。在實際開發過程中,並不是一定要求所有代碼都 遵循設計原則,我們要考慮人力、時間、成本、質量,不是刻意追求完美,要在適當的 場景遵循設計原則,體現的是一種平衡取捨,幫助我們設計出更加優雅的代碼結構。