七大軟件設計原則之一 | 開閉原則

開閉原則是指一個軟件實體(模塊、類、方法等)應該對擴展開放,對修改關閉

我舉一個例子,陀螺是個程序喵,創辦了一個生產貓糧的公司——跑碼場,手下有個小徒弟叫招財,寫了一個下單的邏輯。

/**
 * @author 蟬沐風
 * @description 原始代碼
 * @date 2022/2/8
 */
public class PaoMaChangV1 {
​
    public void order(String flavor) {
​
        if (flavor.equals("毛血旺")) {
            orderMaoXueWangCatFood();
        } else if (flavor.equals("魚香肉絲")) {
            orderFishCatFood();
        }
    }
​
    private void orderMaoXueWangCatFood() {
        System.out.println("售賣一袋「毛血旺」風味貓糧");
    }
​
    private void orderFishCatFood() {
        System.out.println("售賣一袋「魚香肉絲」風味貓糧");
    }
​
}

邏輯本身很簡單,核心業務邏輯主要是order()函數,客戶需要傳入相應的貓糧口味flavor進行下單。

現在跑碼場擴展了業務,新增了一種「大腸刺身」口味的貓糧,而且支持用戶自定義貓糧購買數量(畢竟這種口味可能會供不應求)。在以上代碼的基礎上,招財做了如下修改:

/**
 * @author 蟬沐風
 * @description 原始代碼功能擴展
 * @date 2022/2/8
 */
public class PaoMaChangV1Expand {
​
    public void order(String flavor, Integer count) {
​
        if (flavor.equals("毛血旺")) {
            orderMaoXueWangCatFood(count);
        } else if (flavor.equals("魚香肉絲")) {
            orderFishCatFood(count);
        }
        // 更改1:添加口味的邏輯判斷
        else if (flavor.equals("大腸刺身")) {
            orderDaChangFood(count);
        }
    }
​
    private void orderMaoXueWangCatFood(Integer count) {
        System.out.println("售賣" + count + "袋「毛血旺」風味貓糧");
    }
​
    private void orderFishCatFood(Integer count) {
        System.out.println("售賣" + count + "袋「魚香肉絲」風味貓糧");
    }
​
    // 更改2:添加售賣邏輯
    private void orderDaChangFood(Integer count) {
        System.out.println("售賣" + count + "一袋「大腸刺身」風味貓糧");
    }
}

這種修改方式確實能解決目前的業務問題,但同時也存在很多問題。

首先,修改了order()方法,添加了一個參數,相應的客戶端調用必須修改;其次,每當有新的口味貓糧產品誕生時,都必須在order()方法中添加口味的判斷,同時需要添加該產品的售賣邏輯。這些操作都是通過「修改」來實現新功能的,不符合「開閉原則」。

如果我們要遵循「開閉原則」,必須對修改關閉,對擴展開放。

我們重構一下初始代碼,主要做以下兩方面的修改:

  1. 創建CatFood基類,然後創建對應口味的貓糧繼承基類;
  2. 將每種口味貓糧的售賣邏輯寫在具體類中。
  3. 修改客戶調用的order方法
/**
 * @author 蟬沐風
 * @description 貓糧基類
 * @date 2022/2/8
 */
public abstract class CatFood {
   
    public abstract void order();
​
}
​
/**
 * @author 蟬沐風
 * @description 「毛血旺」貓糧
 * @date 2022/2/8
 */
public class MaoXueWangCatFood extends CatFood {
​
    @Override
    public void order() {
        System.out.println("售賣一袋「毛血旺」風味貓糧");
    }
}
​
​
/**
 * @author 蟬沐風
 * @description 「魚香肉絲」貓糧
 * @date 2022/2/8
 */
public class FishCatFood extends CatFood {
​
    @Override
    public void order() {
        System.out.println("售賣一袋「魚香肉絲」風味貓糧");
    }
​
}

order()方法修改如下

/**
 * @author 蟬沐風
 * @description 遵循「開閉原則」之後的代碼
 * @date 2022/2/8
 */
public class PaoMaChangV2 {
​
    public void order(CatFood catFood) {
       catFood.order();
    }
​
}

重構之後的客戶端調用方式如下

/**
 * @author 蟬沐風
 * @description 客戶端調用
 * @date 2022/2/8
 */
public class ClientV2 {
    public static void main(String[] args) {
        PaoMaChangV2 paoMaChang  = new PaoMaChangV2();
​
        // 創建對應口味的貓糧
        FishCatFood fish = new FishCatFood();
        paoMaChang.order(fish);
    }
}

現在我們再來看,基於重構之後的代碼,我們要實現剛才講到的業務需求,我們需要進行怎樣的改動。主要的修改內容有如下:

  1. CatFood基類中添加屬性count,為子類添加構造函數;
  2. 添加新類DaChangCatFood

擴展之後的代碼如下

/**
 * @author 蟬沐風
 * @description 貓糧類
 * @date 2022/2/8
 */
public abstract class CatFood {
    
    //訂購數量
    private Integer count;
​
    public abstract void order();
​
    public Integer getCount() {
        return count;
    }
​
    public void setCount(Integer count) {
        this.count = count;
    }
​
    public CatFood(Integer count) {
        this.count = count;
    }
​
    public CatFood() {
    }
}
​
/**
 * @author 蟬沐風
 * @description 「毛血旺」貓糧
 * @date 2022/2/8
 */
public class MaoXueWangCatFood extends CatFood {
​
    public MaoXueWangCatFood(Integer count) {
        this.setCount(count);
    }
​
    @Override
    public void order() {
        System.out.println("售賣" + this.getCount() + "袋「毛血旺」風味貓糧");
    }
}
​
/**
 * @author 蟬沐風
 * @description 「魚香肉絲」貓糧
 * @date 2022/2/8
 */
public class FishCatFood extends CatFood {
​
    public FishCatFood(Integer count) {
        this.setCount(count);
    }
​
    @Override
    public void order() {
        System.out.println("售賣" + this.getCount() + "袋「魚香肉絲」風味貓糧");
    }
​
}
​
/**
 * @author 蟬沐風
 * @description 「大腸刺身」貓糧
 * @date 2022/2/8
 */
public class DaChangCatFood extends CatFood {
​
    public DaChangCatFood(Integer count) {
        this.setCount(count);
    }
​
    @Override
    public void order() {
        System.out.println("售賣" + this.getCount() + "袋「大腸刺身」風味貓糧");
    }
​
}

客戶端調用方式變為

public class ClientV2 {
    public static void main(String[] args) {
        PaoMaChangV2 paoMaChang  = new PaoMaChangV2();
​
        // 創建對應口味的貓糧
        DaChangCatFood dachang = new DaChangCatFood(2);
        paoMaChang.order(dachang);
    }
}

image.png

重構之後的代碼在擴展上更加的靈活

  1. 如果有了新口味的貓糧產品,只需創建新的class對象,重寫order()方法就可以了,不需要改動其他的代碼;
  2. 如果order方法中需要其他參數,可以根據實際情況,在CatFood中添加相關屬性。

是不是修改代碼就違背開閉原則?

你可能會有疑問,我們為了完成新業務功能,不僅在CatFood類中添加了count屬性,而且還添加了getter/setter方法,這難道不算修改代碼嗎?

首先我們需要認識到,添加新功能的時候,我們不可能一點代碼都不修改!其次,「開閉原則」的定義是軟件實體(模塊、類、方法等)應該對擴展開放,對修改關閉。對於count屬性的添加而言,在模塊或類的粒度下,可以被認為是修改,但是在方法的粒度下,我們並沒有修改之前存在的方法和屬性,因此可以被認為是擴展。

實際編碼過程中怎麼遵守開閉原則?

我的理解是不需要刻意遵守。

你只需要頭腦中有這個印象就行了,你需要知道的就是你的代碼需要具有一定的擴展性。所有的設計原則都只有一個最終歸宿——不破壞原有代碼的正常運行,方便擴展

隨着你的理論知識和實戰經驗的提高,同時對業務有了足夠了解,你在設計代碼結構時會很自然地向未來靠攏(這需要稍加練習,這種技能不是單純靠工作時長就能獲得的),識別出未來可能會發生的擴展點。

但是想識別出所有可能的擴展點既不可能也沒必要,最合理的做法是對一些比較確定的、短期內可能會發生的需求進行擴展設計。

還是那句話,設計原則和設計模式不是金科玉律,只要適合當前需求,並具備一定彈性的設計就是好設計。要平衡代碼擴展性和可讀性,切勿濫用設計原則和設計模式,犧牲代碼的可讀性。