開閉原則是指一個軟體實體(模組、類、方法等)應該對擴充套件開放,對修改關閉
我舉一個例子,陀螺是個程式喵,創辦了一個生產貓糧的公司——跑碼場,手下有個小徒弟叫招財,寫了一個下單的邏輯。
/**
* @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()
方法中新增口味的判斷,同時需要新增該產品的售賣邏輯。這些操作都是通過「修改」來實現新功能的,不符合「開閉原則」。
如果我們要遵循「開閉原則」,必須對修改關閉,對擴充套件開放。
我們重構一下初始程式碼,主要做以下兩方面的修改:
- 建立
CatFood
基類,然後建立對應口味的貓糧繼承基類; - 將每種口味貓糧的售賣邏輯寫在具體類中。
- 修改客戶呼叫的
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);
}
}
現在我們再來看,基於重構之後的程式碼,我們要實現剛才講到的業務需求,我們需要進行怎樣的改動。主要的修改內容有如下:
CatFood
基類中新增屬性count
,為子類新增建構函式;- 新增新類
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);
}
}
重構之後的程式碼在擴充套件上更加的靈活
- 如果有了新口味的貓糧產品,只需建立新的class物件,重寫
order()
方法就可以了,不需要改動其他的程式碼; - 如果
order
方法中需要其他引數,可以根據實際情況,在CatFood
中新增相關屬性。
是不是修改程式碼就違背開閉原則?
你可能會有疑問,我們為了完成新業務功能,不僅在CatFood
類中新增了count
屬性,而且還新增了getter/setter
方法,這難道不算修改程式碼嗎?
首先我們需要認識到,新增新功能的時候,我們不可能一點程式碼都不修改!其次,「開閉原則」的定義是軟體實體(模組、類、方法等)應該對擴充套件開放,對修改關閉。對於count
屬性的新增而言,在模組或類的粒度下,可以被認為是修改,但是在方法的粒度下,我們並沒有修改之前存在的方法和屬性,因此可以被認為是擴充套件。
實際編碼過程中怎麼遵守開閉原則?
我的理解是不需要刻意遵守。
你只需要頭腦中有這個印象就行了,你需要知道的就是你的程式碼需要具有一定的擴充套件性。所有的設計原則都只有一個最終歸宿——不破壞原有程式碼的正常執行,方便擴充套件。
隨著你的理論知識和實戰經驗的提高,同時對業務有了足夠了解,你在設計程式碼結構時會很自然地向未來靠攏(這需要稍加練習,這種技能不是單純靠工作時長就能獲得的),識別出未來可能會發生的擴充套件點。
但是想識別出所有可能的擴充套件點既不可能也沒必要,最合理的做法是對一些比較確定的、短期內可能會發生的需求進行擴充套件設計。
還是那句話,設計原則和設計模式不是金科玉律,只要適合當前需求,並具備一定彈性的設計就是好設計。要平衡程式碼擴充套件性和可讀性,切勿濫用設計原則和設計模式,犧牲程式碼的可讀性。