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

蟬沐風發表於2022-02-09

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

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

/**
 * @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屬性的新增而言,在模組或類的粒度下,可以被認為是修改,但是在方法的粒度下,我們並沒有修改之前存在的方法和屬性,因此可以被認為是擴充套件。

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

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

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

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

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

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

相關文章