6.2 開閉原則的廬山真面目
開閉原則的定義已經非常明確地告訴我們:軟件實體應該對擴展開放,對修改關閉,其含義是說一個軟件實體應該通過擴展來實現變化,而不是通過修改已有的代碼來實現變化。那什么又是軟件實體呢?軟件實體包括以下幾個部分:
● 項目或軟件產品中按照一定的邏輯規則劃分的模塊。
● 抽象和類。
● 方法。
一個軟件產品只要在生命期內,都會發生變化,既然變化是一個既定的事實,我們就應該在設計時盡量適應這些變化,以提高項目的穩定性和靈活性,真正實現“擁抱變化”。開閉原則告訴我們應盡量通過擴展軟件實體的行為來實現變化,而不是通過修改已有的代碼來完成變化,它是為軟件實體的未來事件而制定的對現行開發設計進行約束的一個原則。我們舉例說明什么是開閉原則,以書店銷售書籍為例,其類圖如圖6-1所示。

圖6-1 書店售書類圖
IBook定義了數據的三個屬性:名稱、價格和作者。小說類NovelBook是一個具體的實現類,是所有小說書籍的總稱,BookStore指的是書店,IBook接口如代碼清單6-1所示。
代碼清單6-1 書籍接口
public?interface?IBook?{???
?????//書籍有名稱
?????public?String?getName();
?????//書籍有售價
?????public?int?getPrice();
?????//書籍有作者
?????public?String?getAuthor();
}
目前書店只出售小說類書籍,小說類如代碼清單6-2所示。
代碼清單6-2 小說類
public?class?NovelBook?implements?IBook?{
?????//書籍名稱
?????private?String?name;???????
?????//書籍的價格
?????private?int?price;?
?????//書籍的作者
?????private?String?author;?????????????
?????//通過構造函數傳遞書籍數據
?????public?NovelBook(String?_name,int?_price,String?_author){
?????????????this.name?=?_name;
?????????????this.price?=?_price;
?????????????this.author?=?_author;
?????}??
?????//獲得作者是誰
?????public?String?getAuthor()?{
?????????????return?this.author;
?????}
?????//書籍叫什么名字
?????public?String?getName()?{
?????????????return?this.name;
?????}
?????//獲得書籍的價格
?????public?int?getPrice()?{
?????????????return?this.price;
?????}
}
注意 我們把價格定義為int類型并不是錯誤,在非金融類項目中對貨幣處理時,一般取2位精度,通常的設計方法是在運算過程中擴大100倍,在需要展示時再縮小100倍,減少精度帶來的誤差。
書店售書的過程如代碼清單6-3所示。
代碼清單6-3 書店售書類
public?class?BookStore?{
?????private?final?static?ArrayList<IBook>?bookList?=?new?ArrayList<IBook>();
?????//static靜態模塊初始化數據,實際項目中一般是由持久層完成
?????static{
?????????????bookList.add(new?NovelBook("天龍八部",3200,"金庸"));
?????????????bookList.add(new?NovelBook("巴黎圣母院",5600,"雨果"));
?????????????bookList.add(new?NovelBook("悲慘世界",3500,"雨果"));
?????????????bookList.add(new?NovelBook("金瓶梅",4300,"蘭陵笑笑生"));
?????}
?????//模擬書店買書
?????public?static?void?main(String[]?args)?{
?????????????NumberFormat?formatter?=?NumberFormat.getCurrencyInstance();
?????????????formatter.setMaximumFractionDigits(2);
?????????????System.out.println("-----------書店賣出去的書籍記錄如下:-----------");
?????????????for(IBook?book:bookList){
?????????????????????System.out.println("書籍名稱:"?+?book.getName()+"\t書籍作者:"?+
????????book.getAuthor()+"\t書籍價格:"+?formatter.format?(book.getPrice()/
????????100.0)+"元");
?????????????}
?????}
}
在BookStore中聲明了一個靜態模塊,實現了數據的初始化,這部分應該是從持久層產生的,由持久層框架進行管理,運行結果如下:
-----------------書店賣出去的書籍記錄如下:--------------
書籍名稱:天龍八部 書籍作者:金庸 書籍價格:¥25.60元
書籍名稱:巴黎圣母院 書籍作者:雨果 書籍價格:¥50.40元
書籍名稱:悲慘世界 書籍作者:雨果 書籍價格:¥28.00元
書籍名稱:金瓶梅 書籍作者:蘭陵笑笑生 書籍價格:¥38.70元
項目投產了,書籍正常銷售出去,書店也贏利了。從2008年開始,全球經濟開始下滑,對零售業影響比較大,書店為了生存開始打折銷售:所有40元以上的書籍9折銷售,其他的8折銷售。對已經投產的項目來說,這就是一個變化,我們應該如何應對這樣一個需求變化?有如下三種方法可以解決這個問題:
● 修改接口
在IBook上新增加一個方法getOffPrice(),專門用于進行打折處理,所有的實現類實現該方法。但是這樣修改的后果就是,實現類NovelBook要修改,BookStore中的main方法也修改,同時IBook作為接口應該是穩定且可靠的,不應該經常發生變化,否則接口作為契約的作用就失去了效能。因此,該方案否定。
● 修改實現類
修改NovelBook類中的方法,直接在getPrice()中實現打折處理,好辦法,我相信大家在項目中經常使用的就是這樣的辦法,通過class文件替換的方式可以完成部分業務變化(或是缺陷修復)。該方法在項目有明確的章程(團隊內約束)或優良的架構設計時,是一個非常優秀的方法,但是該方法還是有缺陷的。例如采購書籍人員也是要看價格的,由于該方法已經實現了打折處理價格,因此采購人員看到的也是打折后的價格,會因信息不對稱而出現決策失誤的情況。因此,該方案也不是一個最優的方案。
● 通過擴展實現變化
增加一個子類OffNovelBook,覆寫getPrice方法,高層次的模塊(也就是static靜態模塊區)通過OffNovelBook類產生新的對象,完成業務變化對系統的最小化開發。好辦法,修改也少,風險也小,修改后的類圖如圖6-2所示。

圖6-2 擴展后的書店售書類圖
OffNovelBook類繼承了NovelBook,并覆寫了getPrice方法,不修改原有的代碼。新增加的子類OffNovelBook如代碼清單6-4所示。
代碼清單6-4 打折銷售的小說類
public?class?OffNovelBook?extends?NovelBook?{
?????public?OffNovelBook(String?_name,int?_price,String?_author){
?????????????super(_name,_price,_author);
?????}
?????//覆寫銷售價格
?????@Override
?????public?int?getPrice(){
?????????????//原價
?????????????int?selfPrice?=?super.getPrice();
?????????????int?offPrice=0;
?????????????if(selfPrice>4000){??//原價大于40元,則打9折
?????????????????????offPrice?=?selfPrice?*?90?/100;
?????????????}else{
?????????????????????offPrice?=?selfPrice?*?80?/100;
?????????????}
?????????????return?offPrice;
?????}
}
很簡單,僅僅覆寫了getPrice方法,通過擴展完成了新增加的業務。書店類BookStore需要依賴子類,代碼稍作修改,如代碼清單6-5所示。
代碼清單6-5 書店打折銷售類
public?class?BookStore?{
?????private?final?static?ArrayList<IBook>?bookList?=?new?ArrayList<IBook>();
?????//static靜態模塊初始化數據,實際項目中一般是由持久層完成
?????static{
?????????????bookList.add(new?OffNovelBook("天龍八部",3200,"金庸"));
?????????????bookList.add(new?OffNovelBook("巴黎圣母院",5600,"雨果"));
?????????????bookList.add(new?OffNovelBook("悲慘世界",3500,"雨果"));
?????????????bookList.add(new?OffNovelBook("金瓶梅",4300,"蘭陵笑笑生"));
?????}??
?????//模擬書店買書
?????public?static?void?main(String[]?args)?{
?????????????NumberFormat?formatter?=?NumberFormat.getCurrencyInstance();
?????????????formatter.setMaximumFractionDigits(2);
?????????????System.out.println("-----------書店賣出去的書籍記錄如下:-----------");
?????????????for(IBook?book:bookList){
??????????????????????System.out.println("書籍名稱:"?+?book.getName()+"\t書籍作者:"?+?book.getAuthor()+?"\t書籍價格:"?+?formatter.format?(book.getPrice()/100.0)+"元");
?????????????}
?????}
}
我們只修改了粗體部分,其他的部分沒有任何改動,運行結果如下所示。
----------------------書店賣出去的書籍記錄如下:---------------------
書籍名稱:天龍八部 書籍作者:金庸 書籍價格:¥25.60元
書籍名稱:巴黎圣母院 書籍作者:雨果 書籍價格:¥50.40元
書籍名稱:悲慘世界 書籍作者:雨果 書籍價格:¥28.00元
書籍名稱:金瓶梅 書籍作者:蘭陵笑笑生 書籍價格:¥38.70元
OK,打折銷售開發完成了。看到這里,各位可能有想法了:增加了一個OffNoveBook類后,你的業務邏輯還是修改了,你修改了static靜態模塊區域。這部分確實修改了,該部分屬于高層次的模塊,是由持久層產生的,在業務規則改變的情況下高層模塊必須有部分改變以適應新業務,改變要盡量地少,防止變化風險的擴散。
注意 開閉原則對擴展開放,對修改關閉,并不意味著不做任何修改,低層模塊的變更,必然要有高層模塊進行耦合,否則就是一個孤立無意義的代碼片段。
我們可以把變化歸納為以下三種類型:
● 邏輯變化
只變化一個邏輯,而不涉及其他模塊,比如原有的一個算法是a*b+c,現在需要修改為a*b*c,可以通過修改原有類中的方法的方式來完成,前提條件是所有依賴或關聯類都按照相同的邏輯處理。
● 子模塊變化
一個模塊變化,會對其他的模塊產生影響,特別是一個低層次的模塊變化必然引起高層模塊的變化,因此在通過擴展完成變化時,高層次的模塊修改是必然的,剛剛的書籍打折處理就是類似的處理模塊,該部分的變化甚至會引起界面的變化。
● 可見視圖變化
可見視圖是提供給客戶使用的界面,如JSP程序、Swing界面等,該部分的變化一般會引起連鎖反應(特別是在國內做項目,做歐美的外包項目一般不會影響太大)。如果僅僅是界面上按鈕、文字的重新排布倒是簡單,最司空見慣的是業務耦合變化,什么意思呢?一個展示數據的列表,按照原有的需求是6列,突然有一天要增加1列,而且這一列要跨N張表,處理M個邏輯才能展現出來,這樣的變化是比較恐怖的,但還是可以通過擴展來完成變化,這就要看我們原有的設計是否靈活。
我們再來回顧一下書店銷售書籍的程序,首先是我們有一個還算靈活的設計(不靈活是什么樣子?BookStore中所有使用到IBook的地方全部修改為實現類,然后再擴展一個ComputerBook書籍,你就知道什么是不靈活了);然后有一個需求變化,我們通過擴展一個子類擁抱了變化;最后把子類投入運行環境中,新邏輯正式投產。通過分析,我們發現并沒有修改原有的模塊代碼,IBook接口沒有改變,NovelBook類沒有改變,這屬于已有的業務代碼,我們保持了歷史的純潔性。放棄修改歷史的想法吧,一個項目的基本路徑應該是這樣的:項目開發、重構、測試、投產、運維,其中的重構可以對原有的設計和代碼進行修改,運維盡量減少對原有代碼的修改,保持歷史代碼的純潔性,提高系統的穩定性。
- 前言
- 第一部分 大旗不揮,誰敢沖鋒——6大設計原則全新解讀
- 第1章 單一職責原則
- 1.2 絕殺技,打破你的傳統思維
- 1.3 我單純,所以我快樂
- 1.4 最佳實踐
- 第2章 里氏替換原則
- 2.2 糾紛不斷,規則壓制
- 2.3 最佳實踐
- 第3章 依賴倒置原則
- 3.2 言而無信,你太需要契約
- 3.3 依賴的三種寫法
- 3.4 最佳實踐
- 第4章 接口隔離原則
- 4.2 美女何其多,觀點各不同
- 4.3 保證接口的純潔性
- 4.4 最佳實踐
- 第5章 迪米特法則
- 5.2 我的知識你知道得越少越好
- 5.3 最佳實踐
- 第6章 開閉原則
- 6.2 開閉原則的廬山真面目
- 6.3 為什么要采用開閉原則
- 6.4 如何使用開閉原則
- 6.5 最佳實踐
- 第二部分 真刀實槍 ——23種設計模式完美演繹
- 第7章 單例模式
- 7.2 單例模式的定義
- 7.3 單例模式的應用
- 7.4 單例模式的擴展
- 7.5 最佳實踐
- 第8章 工廠方法模式
- 8.2 工廠方法模式的定義
- 8.3 工廠方法模式的應用
- 8.4 工廠方法模式的擴展
- 8.5 最佳實踐
- 第9章 抽象工廠模式
- 9.2 抽象工廠模式的定義
- 9.3 抽象工廠模式的應用
- 9.4 最佳實踐
- 第10章 模板方法模式
- 10.2 模板方法模式的定義
- 10.3 模板方法模式的應用
- 10.4 模板方法模式的擴展
- 10.5 最佳實踐
- 第11章 建造者模式
- 11.2 建造者模式的定義
- 11.3 建造者模式的應用
- 11.4 建造者模式的擴展
- 11.5 最佳實踐
- 第12章 代理模式
- 12.2 代理模式的定義
- 12.3 代理模式的應用
- 12.4 代理模式的擴展
- 12.5 最佳實踐
- 第13章 原型模式
- 13.2 原型模式的定義
- 13.3 原型模式的應用
- 13.4 原型模式的注意事項
- 13.5 最佳實踐
- 第14章 中介者模式
- 14.2 中介者模式的定義
- 14.3 中介者模式的應用
- 14.4 中介者模式的實際應用
- 14.5 最佳實踐
- 第15章 命令模式
- 15.2 命令模式的定義
- 15.3 命令模式的應用
- 15.4 命令模式的擴展
- 15.5 最佳實踐
- 第16章 責任鏈模式
- 16.2 責任鏈模式的定義
- 16.3 責任鏈模式的應用
- 16.4 最佳實踐
- 第17章 裝飾模式
- 17.2 裝飾模式的定義
- 17.3 裝飾模式應用
- 17.4 最佳實踐
- 第18章 策略模式
- 18.2 策略模式的定義
- 18.3 策略模式的應用
- 18.4 策略模式的擴展
- 18.5 最佳實踐
- 第19章 適配器模式
- 19.2 適配器模式的定義
- 19.3 適配器模式的應用
- 19.4 適配器模式的擴展
- 19.5 最佳實踐
- 第20章 迭代器模式
- 20.2 迭代器模式的定義
- 20.3 迭代器模式的應用
- 20.4 最佳實踐
- 第21章 組合模式
- 21.2 組合模式的定義
- 21.3 組合模式的應用
- 21.4 組合模式的擴展
- 21.5 最佳實踐
- 第22章 觀察者模式
- 22.2 觀察者模式的定義
- 22.3 觀察者模式的應用
- 22.4 觀察者模式的擴展
- 22.5 最佳實踐
- 第23章 門面模式
- 23.2 門面模式的定義
- 23.3 門面模式的應用
- 23.4 門面模式的注意事項
- 23.5 最佳實踐
- 第24章 備忘錄模式
- 24.2 備忘錄模式的定義
- 24.3 備忘錄模式的應用
- 24.4 備忘錄模式的擴展
- 24.5 最佳實踐
- 第25章 訪問者模式
- 25.2 訪問者模式的定義
- 25.3 訪問者模式的應用
- 25.4 訪問者模式的擴展
- 25.5 最佳實踐
- 第26章 狀態模式
- 26.2 狀態模式的定義
- 26.3 狀態模式的應用
- 第27章 解釋器模式
- 27.2 解釋器模式的定義
- 27.3 解釋器模式的應用
- 27.4 最佳實踐
- 第28章 享元模式
- 28.2 享元模式的定義
- 28.3 享元模式的應用
- 28.4 享元模式的擴展
- 28.5 最佳實踐
- 第29章 橋梁模式
- 29.2 橋梁模式的定義
- 29.3 橋梁模式的應用
- 29.4 最佳實踐
- 第三部分 誰的地盤誰做主 ——設計模式PK
- 第30章 創建類模式大PK
- 30.1 工廠方法模式VS建造者模式
- 30.2 抽象工廠模式VS建造者模式
- 第31章 結構類模式大PK
- 31.1 代理模式VS裝飾模式
- 31.2 裝飾模式VS適配器模式
- 第32章 行為類模式大PK
- 32.1 命令模式VS策略模式
- 32.2 策略模式VS狀態模式
- 32.3 觀察者模式VS責任鏈模式
- 第33章 跨戰區PK
- 33.1 策略模式VS橋梁模式
- 33.2 門面模式VS中介者模式
- 33.3 包裝模式群PK
- 第四部分 完美世界 ——設計模式混編
- 第34章 命令模式+責任鏈模式
- 34.2 混編小結
- 第35章 工廠方法模式+策略模式
- 35.2 混編小結
- 第36章 觀察者模式+中介者模式
- 36.2 混編小結
- 第五部分 擴展篇
- 第37章 MVC框架
- 37.2 最佳實踐
- 第38章 新模式
- 38.1 規格模式
- 38.2 對象池模式
- 38.3 雇工模式
- 38.4 黑板模式
- 38.5 空對象模式
- 附錄 23種設計模式彩圖