軟體設計七大原則實戰(二)-開閉原則
1 開閉原則的定義
開閉原則是Java世界裡最基礎的設計原則,它指導我們如何建立一個穩定的、靈活的系統,先來看開閉原則的定義:
Software entities like classes,modules and functions should be open for extension but closed for modifications

定義
初看到這個定義,可能會很迷惑,對擴充套件開放?開放什麼?對修改關閉,怎麼關閉?沒關係,我會一步一步帶領大家解開這些疑惑。
我們做一件事情,或者選擇一個方向,一般需要經歷三個步驟:What——是什麼,Why——為什麼,How——怎麼做(簡稱3W原則,How取最後一個w)。對於開閉原則,我們也採用這三步來分析,即什麼是開閉原則,為什麼要使用開閉原則,怎麼使用開閉原則。
2 開閉原則的廬山真面目
定義已經非常明確地告訴我們:軟體實體應該對擴充套件開放,對修改關閉
其含義是說 一個軟體實體應該通過擴充套件來實現變化,而不是通過修改已有的程式碼來實現變化
那什麼又是軟體實體呢?軟體實體包括以下部分
● 專案或軟體產品中按照一定的邏輯規則劃分的模組
● 抽象和類
● 方法
一個軟體產品只要在生命期內,都會發生變化,既然變化是一個既定的事實,我們就應該在設計時儘量適應這些變化,以提高專案的穩定性和靈活性,真正實現“擁抱變化”。
開閉原則告訴我們應儘量通過擴充套件軟體實體的行為來實現變化,而不是通過修改已有的程式碼來完成變化,它是為軟體實體的未來事件而制定的對現行開發設計進行約束的一個原則。
我們舉例說明什麼是開閉原則,以書店銷售書籍為例

書店售書類圖
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){
ofollow,noindex">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 bookList = new ArrayList();
//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所示。

image
圖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 bookList = new ArrayList();
//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類沒有改變,這屬於已有的業務程式碼,我們保持了歷史的純潔性。放棄修改歷史的想法吧,一個專案的基本路徑應該是這樣的:專案開發、重構、測試、投產、運維,其中的重構可以對原有的設計和程式碼進行修改,運維儘量減少對原有程式碼的修改,保持歷史程式碼的純潔性,提高系統的穩定性。