大型Java進階專題(三) 軟件架構設計原則(下)

前言

​ 今天開始我們專題的第二課了,本章節繼續分享軟件架構設計原則的下篇,將介紹:接口隔離原則、迪米特原則、里氏替換原則和合成複用原則。本章節參考資料書籍《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 數據庫連接"; 
    } 
}

具體選擇交給應用層,來看一下類圖:

設計原則總結

​ 學習設計原則,學習設計模式的基礎。在實際開發過程中,並不是一定要求所有代碼都 遵循設計原則,我們要考慮人力、時間、成本、質量,不是刻意追求完美,要在適當的 場景遵循設計原則,體現的是一種平衡取捨,幫助我們設計出更加優雅的代碼結構。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章