Spring軟件架構設計原則

1.1 開閉原則

定義:
一個軟件實體(類、模塊和函數)應該對擴展開放,對修改關閉。強調用抽象構建框架,用實現擴展細節。
舉例:
首先創建一個課程接口:

public interface ICourse {
    Integer getId();
    String getName();
    Double getPrice();
}

在創建一個具體的實現類,比如叫Java架構課程類:

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;
    }
    @Override
    public Integer getId() {
        return this.id;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public Double getPrice() {
        return this.price;
    }
}

這時突然提了一個需求,比如課程的價格有變動,需要對getPrice()方法進行修改,如果直接去改動這個方法,則其它調用這個方法的代碼可能存在風險。可以通過如下方式:

public class JavaDiscussCourse extends JavaCourse{
    public JavaDiscussCourse(Integer id, String name, Double price) {
        super(id, name, price);
    }

    public Double getOriginPrice() {
        return super.getPrice();
    }

    public Double getPrice() {
        return super.getPrice() * 0.61;
    }
}

1.2 依賴倒置原則

定義:
設計代碼結構時,高層模塊不應該依賴底層模塊,抽象不應該依賴細節。
舉例:
Tom正在學兩門課程, 分別是Java和Python:

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();
    }
}

此時,他突然還想學AI,於是準備在Tom類裏在家一個study方法,這樣的方式真的太差勁了,Tom和課程耦合度太高。
如下從Tom抽象出課程接口,然後Tom面向接口調用課程,具體的實現類由調用層自己指定。這樣不管增加多少課程,Tom都不需要管。
首先創建一個課程接口,裏面只有一個可以獲取自己的名稱方法:

public interface ICourse {
    public String getName();
}

在分別創建三個課程實現類

public class JavaCourse implements ICourse {
    private String name;
    public JavaCourse() {
        this.name = "Java";
    }

    @Override
    public String getName() {
        return name;
    }
}

public class PythonCourse implements ICourse {
    private String name;
    public PythonCourse() {
        this.name = "Python";
    }

    @Override
    public String getName() {
        return name;
    }
}

public class AiCourse implements ICourse {
    private String name;
    public AiCourse() {
        this.name = "Ai";
    }

    @Override
    public String getName() {
        return name;
    }
}

最後在修改Tom類,使其面向課程接口調用:

public class Tom {
    public void study(ICourse course) {
        System.out.println("Tom正在學習" + course.getName());
    }

    public static void main(String[] args) {
        Tom tom = new Tom();
        tom.study(new JavaCourse());
        tom.study(new PythonCourse());
        tom.study(new AiCourse());
    }
}

1.3 單一職責原則

定義:
不要存在多於一個導致類變更的原因。
舉例:
假如課程分成了直播課和錄播課,直播課不能快進,錄播課可以,很明顯這兩種課程功能職責已經不一樣。下面展示一端看似好像不復雜的代碼,但其實已經埋下了複雜的隱患:

public class Course {
    public void study(String courseName) {
        if("直播課".equals(courseName)) {
            System.out.println(courseName + "不能快進");
        } else {
            System.out.println(courseName + "可以反覆觀看");
        }
    }

    public static void main(String[] args) {
        Course course = new Course();
        course.study("直播課");
        course.study("錄播課");
    }
}

這個時候,如果需求變了,假如現在要對課程加密,直播課和錄播課的加密邏輯不一樣,那修改的邏輯就會相互影響,充滿了風險。所以我們需要對上面的代碼進行解耦,可以分別創建LiveCourse和ReplayCourse:

public class LiveCourse {
    public void study(String courseName) {
        System.out.println(courseName + "不能快進");
    }
}


public class ReplayCourse {
    public void study(String courseName) {
        System.out.println(courseName + "可以反覆觀看");
    }
}

public class Test {
    public static void main(String[] args) {
        LiveCourse liveCourse = new LiveCourse();
        ReplayCourse replayCourse = new ReplayCourse();
        liveCourse.study("直播課");
        replayCourse.study("錄播課");
    }
}

1.4 接口隔離原則

定義:用多個專門的接口,而不適用單一的總接口。
舉例:
先舉一個不合適的例子,假如不遵從接口隔離原則,則設計的接口一般比較臃腫,比如如下的IAnimal接口:

public interface IAnimal {
    void eat();
    void fly();
    void swim();
}

此時如果有兩個接口的實現類,分別是Bird鳥類和Dog狗類,則將會出現和奇葩的問題。

public class Bird implements IAnimal {
    @Override
    public void eat() {
        
    }

    @Override
    public void fly() {

    }

    @Override
    public void swim() {

    }
}

public class Dog implements IAnimal {
    @Override
    public void eat() {

    }

    @Override
    public void fly() {

    }

    @Override
    public void swim() {

    }
}

什麼問題,還沒發現嗎?仔細一看發現鳥居然有一個swim(),狗居然有一個fly()方法,你說奇怪不奇怪。所以啊,接口是需要細分的,在這裏接口要針對不同的行爲來細分,比如分別設計出IEatAnimal、IFlyAnimal和ISwimAnimal接口,那鳥和狗肯定都需要實現公共接口IEatAnimal,不然不餓死了嘛。除了實現公共接口,他們還需實現自己特有行爲的接口,鳥實現IFlyAnimal接口,狗實現ISwimAnimal接口。

1.5 迪米特原則

定義:一個對象應該對其他對象保持最少的瞭解,儘量降低類與類之間的耦合度。
舉例:
假如現在有個老闆類Boss想要看看線上的課程數量,這個時候他去找到開發團隊領導TeamLeader去進行統計,TeamLeader需要將結果給Boss。

public class Course {
}

public class TeamLeader {
    public void checkNumberOfCourses(List<Course> coursesList) {
        System.out.println("目前已發佈的課程數量是:" + coursesList.size());
    }
}

public class Boss {
    public void checkNumberOfCourses(TeamLeader teamLeader) {
        List<Course> coursesList = new ArrayList<>();
        for(int i=0; i<20; i++) {
            coursesList.add(new Course());
        }
        teamLeader.checkNumberOfCourses(coursesList);
    }
}

如果是按上面的方法來完成課程數量統計,想必老闆Boss早把開發團隊領導TeamLeader炒掉了吧,爲啥?很明顯老闆招人肯定是給他幹活,他自己安排一件事,最希望的結果是你去幹,我啥都不管,最後給我一個結果就行,你們說是不是,一個簡單的事情都幹不好怎麼能不被炒,哈哈。看看下面的代碼,一個合格的TeamLeader總是能表現的優秀(老闆少操心):

public class TeamLeader {
    public void checkNumberOfCourses() {
        List<Course> coursesList = new ArrayList<>();
        for(int i=0; i<20; i++) {
            coursesList.add(new Course());
        }
        System.out.println("目前已發佈的課程數量是:" + coursesList.size());
    }
}

public class Boss {
    public void checkNumberOfCourses(TeamLeader teamLeader) {
        teamLeader.checkNumberOfCourses();
    }
}

1.6 裏式替換原則

定義:一個軟件實體如果適用於一個父類,那麼一定適用於子類。
隱含意思:子類可以擴展父類,但是不能改變父類原有功能。
(1)子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法。
(2)子類可以添加新的方法。
(3)重載父類時,方法的入參要比父類方法的輸入參數更寬鬆。
(4)重載父類時,方法的出參要比父類方法的輸出參數更嚴格。
在1.1描述開閉原則時,增加了一個獲取父類屬性的方法,還重寫了父類的非抽象方法,很明顯違背了裏式替換原則。
舉例:
正方形是一種特殊的長方形。
首先創建一個父類Rectangle:

public class Rectangle {
    private long height;
    private long weight;

    public long getHeight() {
        return height;
    }

    public void setHeight(long height) {
        this.height = height;
    }

    public long getWeight() {
        return weight;
    }

    public void setWeight(long weight) {
        this.weight = weight;
    }
}

違背裏式替換原則創建一個正方形Square類:

public class Square extends Rectangle {
    private long length;

    public long getLength() {
        return length;
    }

    public void setLength(long length) {
        this.length = length;
    }

    public long getHeight() {
        return getLength();
    }

    public void setHeight(long height) {
        setLength(height);
    }

    public long getWeight() {
        return getLength();
    }

    public void setWeight(long weight) {
        setLength(weight);
    }
}

最後在測試類中創建resize()方法,長方形的寬應該大於等於高,我們讓高一直增長,直到高和寬相等變成正方形:

public class Test {
    public static void resize(Rectangle rectangle) {
        while(rectangle.getWeight() >= rectangle.getHeight()) {
            rectangle.setHeight(rectangle.getHeight() + 1);
            System.out.println("width:" + rectangle.getWeight() + " ,height:" + rectangle.getHeight());
        }
        System.out.println("resize方法結束" + "\nwidth:" + rectangle.getWeight() + " ,height:" + rectangle.getHeight());
    }
    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle();
        rectangle.setHeight(10);
        rectangle.setWeight(20);
        resize(rectangle);
    }
}

現在將Rectangle類替換成子類Square。

public class Test {
    public static void resize(Rectangle rectangle) {
        while(rectangle.getWeight() >= rectangle.getHeight()) {
            rectangle.setHeight(rectangle.getHeight() + 1);
            System.out.println("width:" + rectangle.getWeight() + " ,height:" + rectangle.getHeight());
        }
        System.out.println("resize方法結束" + "\nwidth:" + rectangle.getWeight() + " ,height:" + rectangle.getHeight());
    }
    public static void main(String[] args) {
        Square square = new Square();
        square.setHeight(10);
        square.setWeight(20);
        resize(square);
    }
}

上面代碼運行時出現死循環,爲啥?這個應該不用我解釋了吧,因爲重寫了父類非抽象方法,使長方形的寬高始終相等,所以while就死了唄。
下面演示一個遵循裏式替換原則的案例。
只需稍稍改動一下正方形類,使其不覆蓋父類非抽象方法。

public class Square extends Rectangle {
    private long length;

    public long getLength() {
        return length;
    }

    public void setLength(long length) {
        this.length = length;
    }

}

這個時候運行Test類,將Rectangle換成Square是不會報錯的。

1.7 合成複用原則

定義:儘量使用對象組合/聚合而不是繼承來達到軟件複用。
繼承叫白箱複用,因爲實現細節暴露給子類了。
組合/聚合叫黑箱複用,因爲無法獲取到組合對象的實現細節。
舉例:
下面舉一個合成複用原則的案例。

public abstract class DBConnection {
    public abstract String getConnection();
}

public class MySQLConnection extends DBConnection {
    @Override
    public String getConnection() {
        return "MySQL數據庫連接";
    }
}

public class OracleConnection extends DBConnection {
    @Override
    public String getConnection() {
        return "Oracle數據庫連接";
    }
}

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 + "增加產品");
    }
}

1.8 設計原則總結
理想與現實總是不能畫上等號,爲毛?因爲現實開發中要考慮人力、時間、成本、質量等種種因素,不能只追求完美。適當的場景遵循合適的設計原則纔是力求最好的標準,所以魚和熊掌不可兼得。

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