天天看點

軟體架構設計的七大原則

一、開閉原則

開閉原則(Open-Closed Principle  OCP)是指一個軟體實體,如類、子產品和函數應該對擴充開放,對修改關閉。所謂的開始,是用抽象建構架構,用實作擴充細節。可以提高軟體系統的可維護性和可複用性。開閉原則是面向對象中最基礎的原則,實作開閉原則的基本思想就是面向抽象程式設計。

以某保險公司為例,每個保險公司都有很多産品,也就是所謂的險種,那麼先來定義一個險種的接口:

/**
 * 某保險公司險種
 */
public interface Risk {
    //擷取險種編碼
    String getRiskCode();
    //擷取險種名稱
    String getRiskName();
    //保費
    Double getPrem();
}
           

整個保險公司有很多險種,比如重疾險、年金保險、短期意外險、短期醫療、定期壽險.....那麼我們先來建立一個長期重疾保險:

/**
 * 長期重疾險
 */
public class BigDisease implements Risk{
    private String riskCode;
    private String riskName;
    private Double prem;

    public BigDisease(String riskCode, String riskName, Double prem) {
        this.riskCode = riskCode;
        this.riskName = riskName;
        this.prem = prem;
    }

    @Override
    public String getRiskCode() {
        return this.riskCode;
    }

    @Override
    public String getRiskName() {
        return this.riskName;
    }

    @Override
    public Double getPrem() {
        return this.prem;
    }
}
           

現在保險公司推出了一個活動,給予承包重疾險的客戶一個優惠,在保費上打8折,那麼如果直接修改BigDisease中的getPrem方法則存在一定的風險,可能會影響其他地方調用的結果,那麼我們如何在不修改原有方法的基礎上實作這個功能呢?現在,我們再來建立一個BigDiseaseDisCount類處理優惠邏輯。

/**
 * 重疾險打折處理類
 */
public class BigDiseaseDisCount extends BigDisease{
    public BigDiseaseDisCount(String riskCode, String riskName, Double prem) {
        super(riskCode, riskName, prem);
    }
    public Double getDisCountPrem(){
        return super.getPrem()*0.8;
    }
}
           

 這樣就能保證不修改原來代碼的邏輯而進行擴充,降低了修改原代碼的風險。

二、依賴倒置原則

依賴倒置原則(Dependence Inversion Principle DIP)是指設計代碼結構時,高層子產品不應該依賴底層子產品,兩者都應該依賴其抽象。抽象不依賴細節,細節應該依賴抽象。通過依賴倒置,可以減少類與類之間的耦合性,提高系統的穩定性,提高代碼的可讀性和穩定性,降低修改代碼給系統帶來的風險。

大家要切記:以抽象為基準比以細節為基準搭建起來的架構要穩定的多,是以大家在拿到需求之後,要面向接口程式設計,先頂層再細節來進行程式設計。

還是以保險為例,張三想要去買一個保單,這個保單中有兩個險種,一個是重疾險,一個是意外險:

/**
 * 張三要買保險
 */
public class ZhangSan {
    public void byBidDisease(){
        System.out.println("張三買了一份重疾保險");
    }
    public  void byAccidentRisk(){
        System.out.println("張三買了一份意外險");
    }
}
           

 調用一下:

public static void main(String[] args) {
        ZhangSan zhangsan = new ZhangSan();
        zhangsan.byAccidentRisk();
        zhangsan.byBidDisease();
    }
           

不得不說,張三還是很有自我保護意識的,那麼張三又想買一份定期壽險的話,就要修改原代碼,在ZhangSan類中增加byAgeRisk方法,在main方法中也需要增加調用。如此一來釋出代碼風險是很高的,有時會代碼意想不到的風險,那麼要如何來優化代碼。先建立一個byRisk接口:

public interface ByRisk {
    void buy();
}
           

 然後寫重疾險:

public class BigDisease implements ByRisk{
    @Override
    public void buy() {
        System.out.println("張三買一份重疾");
    }
}
           

在買一份意外險:

public class AccidentRisk implements ByRisk{
    @Override
    public void buy() {
        System.out.println("張三買了一份意外險");
    }
}
           

再買一份定期壽險:

public class AgeRisk implements ByRisk{
    @Override
    public void buy() {
        System.out.println("張三買了一份定期壽險");
    }
}
           

 修改後的張三類:

/**
 * 張三要買保險
 */
public class ZhangSan {
    public void buy(ByRisk byRisk){
        byRisk.buy();
    }

    public static void main(String[] args) {
        ZhangSan zhangsan = new ZhangSan();
        zhangsan.buy(new BigDisease());
        zhangsan.buy(new AccidentRisk());
        zhangsan.buy(new AgeRisk());
    }
}
           

 這時候我們再來看代碼,無論張三想買多少個險種,都不需要修改底層的代碼,隻需要建立一個新類并通過傳參的方式告訴張三要買什麼險種即可。實際上這就是依賴注入,注入的方式還有構造器注入和setter方式,這裡就不舉例說明了。

三、單一職責原則

單一職責(Simple Responsibility Principle SRP)是指不要存在多于一個導緻類變更的原因。假設我們的類有兩個職責,一旦需求發生變更,修改其中一個職責的代碼,有可能會導緻另外一個職責的代碼功能發生故障。這樣一來,這個類存在兩個可能導緻類變更的原因。如何解決這個問題,我們就要給這兩個職責分别用兩個類來實作,進行解耦。後期需求變更維護互不影響。這樣的設計可以降低類的複雜度,提高類的可讀性,提高系統的可維護性,降低變更引起的風險。

用現在比較流行的網課進行舉例吧,網課通常分為直播課和錄播課,直播課不能進行快進,而錄播課可以進行快進,功能職責不一樣,先建立一個類:

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

從上面代碼來看,Course 類承擔了兩種處理邏輯。假如,現在要對課程進行加密,那麼直播課和錄播課的加密邏輯都不一樣,必須要修改代碼。而修改代碼邏輯勢必會互相影響容易造成不可控的風險。我們對職責進行分離解耦,來看代碼,分别建立兩個類ReplayCourse 和 LiveCourse: 

public class LiveCourse {
    public void study(String courseName){
        System.out.println(courseName + "可以反複回看");
    }
}

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

四、接口隔離原則

接口隔離原則(Interface Segregation Principle ISP)是指使用多個專門的接口,而不是使用單一的總接口,用戶端不應該依賴它不需要的接口。這個原則指導我們在設計接口的時候要注意如下幾點:

1.一個類對一個類的依賴應該建立在最小的接口上。

2.建立單一的接口,不要建立龐大臃腫的接口。

3.盡量細化接口,接口中的方法盡量少,但不是越少越好,要适度。

接口隔離原則是我們常說的高内聚低耦合的設計思想,進而使得類具有更好的可讀性、可拓展性和可維護性。我們在設計接口的時候要多花時間去思考,要考慮業務模型,包括以後可能會發生變更的地方還要做一些預判。

那麼我們來寫一個動物行為的抽象:

public interface IAnimal {
    void eat();
    void fly();
    void swim();
}
           
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() {}
}
           

 可以看出,Bird 的 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 ISwimAnimal,IEatAnimal {
    @Override
    public void eat() {}
    @Override
    public void swim() {}
}
           

五、迪米特法則

迪米特法則(LOD)是指一個對象應該對其他對象保持最少的了解,又叫最少知道原則,盡量降低類與類之間的耦合。迪米特原則主要強調之和朋友交流,不和陌生人說話。出現在成員變量、方法的輸入、輸出參數中的類都可以稱之為成員朋友類,而出現在方法内的類不屬于朋友類。

現在來設計一個權限系統,Boss 需要檢視目前釋出到線上的課程數量。這時候,Boss要找到 TeamLeader 去進行統計,TeamLeader 再把統計結果告訴 Boss。接下來我們還是來看代碼:

Course 類:

public class Course {
}
           
public class TeamLeader {
    public void checkNumberOfCourses(List<Course> courseList){
        System.out.println("目前已釋出的課程數量是:"+courseList.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 并不是朋友 。那麼就要做出如下修改:

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

六、裡式替換原則

裡式替換原則(LSP)是指如果對每一個類型為T1的對象o1,都有類型為T2的對象o2,使得以T1定義的所有程式P在所有對象o1都替換成o2時,程式P的行為沒有發生變化,那麼類型T2是T1的子類型。

引申含義就是,子類可以擴充父類的功能,但是不能修改父類原有的功能。

1.子類可以實作父類的抽象方法,但是不能覆寫父類的非抽象方法。

2.子類中可以增加自己的方法。

3.當子類重載父類的方法時,方法的輸入參數要比父類的輸入參數更加寬松。

4.當子類的方法實作父類的方法時(重寫、重載、實作抽象方法),方法的輸出、傳回值要比父類的輸出、傳回值更加嚴格或相等。

使用裡氏原則有如下優點:

1.限制內建泛濫,開閉原則的一種展現。

2.加強程式的健壯性,同時變更時也可以做到更好的相容性,提高程式的維護性、擴充性。降低需求變更時引入的風險。

現在來描述一個經典的業務場景,用正方形、矩形和四邊形的關系說明裡氏替換原則,我們都知道正方形是一個特殊的長方形,那麼就可以建立一個長方形父類 Rectangle 類:

public class Rectangle {
    private long height;
    private long width;
    @Override
    public long getWidth() {
        return width;
    }
    @Override
    public long getLength() {
        return length;
    }
    public void setLength(long length) {
        this.length = length;
    }
    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 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 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;
    @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;
    }
}
           

 修改正方形類 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 長方形。 

七、合成複用原則

合成複原則(CARP)是指盡量使用對象組合、聚合,而不是內建達到軟體複用的目的。可以使系統更加靈活,降低類和類之間的耦合度,一個類的變化對其他的類造成的影響較小。

繼承我們叫白箱複用,相當于把所有的實作細節暴露給子類。組合、聚合也稱為黑箱複用,對類以外的對象是無法獲得實作細節的。

以資料庫操作為例,先來建立 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 資料庫連接配接";
    }
}
           

具體選擇交給應用層。 

設計原則總結

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