- 設(shè)計(jì)模式之禪
- 秦小波
- 2974字
- 2019-01-02 03:55:53
6.2 開閉原則的廬山真面目
開閉原則的定義已經(jīng)非常明確地告訴我們:軟件實(shí)體應(yīng)該對(duì)擴(kuò)展開放,對(duì)修改關(guān)閉,其含義是說(shuō)一個(gè)軟件實(shí)體應(yīng)該通過擴(kuò)展來(lái)實(shí)現(xiàn)變化,而不是通過修改已有的代碼來(lái)實(shí)現(xiàn)變化。那什么又是軟件實(shí)體呢?軟實(shí)體包括以下幾個(gè)部分:
?項(xiàng)目或軟件產(chǎn)品中按照一定的邏輯規(guī)則劃分的模塊。
?抽象和類。
?方法。
一個(gè)軟件產(chǎn)品只要在生命期內(nèi),都會(huì)發(fā)生變化,既然變化是一個(gè)既定的事實(shí),我們就應(yīng)該在設(shè)計(jì)時(shí)盡量適應(yīng)這些變化,以提高項(xiàng)目的穩(wěn)定性和靈活性,真正實(shí)現(xiàn)“擁抱變化”。開閉原則告訴我們應(yīng)盡量通過擴(kuò)展軟件實(shí)體的行為來(lái)實(shí)現(xiàn)變化,而不是通過修改已有的代碼來(lái)完成變化,它是為軟件實(shí)體的未來(lái)事件而制定的對(duì)現(xiàn)行開發(fā)設(shè)計(jì)進(jìn)行約束的一個(gè)原則。我們舉例說(shuō)明什么是開閉原則,以書店銷售書籍為例,其類圖如圖6-1所示。

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

圖6-2 擴(kuò)展后的書店售書類圖
OffNovelBook類繼承了NovelBook,并覆寫了getPrice方法,不修改原有的代碼。新增加的子類OffNovelBook如代碼清單6-4所示。
代碼清單6-4 打折銷售的小說(shuō)類
public class OffNovelBook extends NovelBook { public OffNovelBook(String _name,int _price,String _author){ super(_name,_price,_author); } //覆寫銷售價(jià)格 @Override public int getPrice(){ //原價(jià) int selfPrice=super.getPrice(); int offPrice=0; if(selfPrice>4000){ //原價(jià)大于40元,則打9折 offPrice=selfPrice * 90 /100; }else{ offPrice=selfPrice * 80 /100; } return offPrice; } }
很簡(jiǎn)單,僅僅覆寫了getPrice方法,通過擴(kuò)展完成了新增加的業(yè)務(wù)。書店類BookStore需要依賴子類,代碼稍作修改,如代碼清單6-5所示。
代碼清單6-5 書店打折銷售類
public class BookStore { private final static ArrayList<IBook> bookList=new ArrayList<IBook>(); //static靜態(tài)模塊初始化數(shù)據(jù),實(shí)際項(xiàng)目中一般是由持久層完成 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書籍價(jià)格:"+formatter.format (book.getPrice()/100.0)+"元"); } } }
我們只修改了粗體部分,其他的部分沒有任何改動(dòng),運(yùn)行結(jié)果如下所示。
------------書店買出去的書籍記錄如下:--------------------- 書籍名稱:天龍八部 書籍作者:金庸 書籍價(jià)格:¥25.60元 書籍名稱:巴黎圣母院 書籍作者:雨果 書籍價(jià)格:¥50.40元 書籍名稱:悲慘世界 書籍作者:雨果 書籍價(jià)格:¥28.00元 書籍名稱:金瓶梅 書籍作者:蘭陵笑笑生 書籍價(jià)格:¥38.70元
OK,打折銷售開發(fā)完成了。看到這里,各位可能有想法了:增加了一個(gè)OffNoveBook類后,你的業(yè)務(wù)邏輯還是修改了,你修改了static靜態(tài)模塊區(qū)域。這部分確實(shí)修改了,該部分屬于高層次的模塊,是由持久層產(chǎn)生的,在業(yè)務(wù)規(guī)則改變的情況下高層模塊必須有部分改變以適應(yīng)新業(yè)務(wù),改變要盡量地少,防止變化風(fēng)險(xiǎn)的擴(kuò)散。
注意 開閉原則對(duì)擴(kuò)展開放,對(duì)修改關(guān)閉,并不意味著不做任何修改,低層模塊的變更,必然要有高層模塊進(jìn)行耦合,否則就是一個(gè)孤立無(wú)意義的代碼片段。
我們可以把變化歸納為以下三種類型:
?邏輯變化
只變化一個(gè)邏輯,而不涉及其他模塊,比如原有的一個(gè)算法是a*b+c,現(xiàn)在需要修改為a*b*c,可以通過修改原有類中的方法的方式來(lái)完成,前提條件是所有依賴或關(guān)聯(lián)類都按照相同的邏輯處理。
?子模塊變化
一個(gè)模塊變化,會(huì)對(duì)其他的模塊產(chǎn)生影響,特別是一個(gè)低層次的模塊變化必然引起高層模塊的變化,因此在通過擴(kuò)展完成變化時(shí),高層次的模塊修改是必然的,剛剛的書籍打折處理就是類似的處理模塊,該部分的變化甚至?xí)鸾缑娴淖兓?/p>
?可見視圖變化
可見視圖是提供給客戶使用的界面,如JSP程序、Swing界面等,該部分的變化一般會(huì)引起連鎖反應(yīng)(特別是在國(guó)內(nèi)做項(xiàng)目,做歐美的外包項(xiàng)目一般不會(huì)影響太大)。如果僅僅是界面上按鈕、文字的重新排布倒是簡(jiǎn)單,最司空見慣的是業(yè)務(wù)耦合變化,什么意思呢?一個(gè)展示數(shù)據(jù)的列表,按照原有的需求是6列,突然有一天要增加1列,而且這一列要跨N張表,處理M個(gè)邏輯才能展現(xiàn)出來(lái),這樣的變化是比較恐怖的,但還是可以通過擴(kuò)展來(lái)完成變化,這就要看我們?cè)械脑O(shè)計(jì)是否靈活。
我們?cè)賮?lái)回顧一下書店銷售書籍的程序,首先是我們有一個(gè)還算靈活的設(shè)計(jì)(不靈活是什么樣子?BookStore中所有使用到IBook的地方全部修改為實(shí)現(xiàn)類,然后再擴(kuò)展一個(gè)ComputerBook書籍,你就知道什么是不靈活了);然后有一個(gè)需求變化,我們通過擴(kuò)展一個(gè)子類擁抱了變化;最后把子類投入運(yùn)行環(huán)境中,新邏輯正式投產(chǎn)。通過分析,我們發(fā)現(xiàn)并沒有修改原有的模塊代碼,IBook接口沒有改變,NovelBook類沒有改變,這屬于已有的業(yè)務(wù)代碼,我們保持了歷史的純潔性。放棄修改歷史的想法吧,一個(gè)項(xiàng)目的基本路徑應(yīng)該是這樣的:項(xiàng)目開發(fā)、重構(gòu)、測(cè)試、投產(chǎn)、運(yùn)維,其中的重構(gòu)可以對(duì)原有的設(shè)計(jì)和代碼進(jìn)行修改,運(yùn)維盡量減少對(duì)原有代碼的修改,保持歷史代碼的純潔性,提高系統(tǒng)的穩(wěn)定性。
- 數(shù)據(jù)科學(xué)實(shí)戰(zhàn)手冊(cè)(R+Python)
- Mastering Concurrency Programming with Java 8
- HTML5+CSS3王者歸來(lái)
- OpenShift開發(fā)指南(原書第2版)
- Instant Zepto.js
- Practical Windows Forensics
- Oracle Database 12c Security Cookbook
- QGIS:Becoming a GIS Power User
- ArcGIS By Example
- Swift細(xì)致入門與最佳實(shí)踐
- HTML5從入門到精通 (第2版)
- Instant Debian:Build a Web Server
- Python數(shù)據(jù)預(yù)處理技術(shù)與實(shí)踐
- Effective C++:改善程序與設(shè)計(jì)的55個(gè)具體做法(第三版)中文版(雙色)
- MySQL核心技術(shù)與最佳實(shí)踐