天天看點

設計模式—— 六:開閉原則什麼是開閉原則?開閉原則詳解為什麼要采用開閉原則?如何應用開閉原則?

文章目錄

開閉原則的定義:

Software entities like classes,modules and functions should be open for extension but closed for modifications.(一個軟體實體如類、子產品和函數應該對擴充開放,對修改關閉。)

在開發軟體的過程中,因為變化 、更新和維護等原因需要對軟體原有的代碼進行修改,可能會将錯誤引入原本已經測試過的舊代碼中,破壞原有的系統,是以,當軟體需求變化時,我們應盡量運用擴充的方式來實作變化,而不是修改原來的代碼。

以書店銷售書籍為例:

6-1:書店售書類圖

設計模式—— 六:開閉原則什麼是開閉原則?開閉原則詳解為什麼要采用開閉原則?如何應用開閉原則?

書籍接口IBook:

public interface IBook {   
     //書籍有名稱
     public String getName();
     //書籍有售價
     public int getPrice();
     //書籍有作者
     public String getAuthor();
}      

小說類NovelBook:

是一個具體的實作類,是所有小說書籍的總稱。

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;
     }
}      

書店售書類:

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)+"元");
             }
     }
}      

運作結果:

-----------------書店賣出去的書籍記錄如下:--------------

書籍名稱:天龍八部  書籍作者:金庸    書籍價格:¥25.60元

書籍名稱:巴黎聖母院 書籍作者:雨果    書籍價格:¥50.40元

書籍名稱:悲慘世界  書籍作者:雨果    書籍價格:¥28.00元

書籍名稱:金瓶梅    書籍作者:蘭陵笑笑生 書籍價格:¥38.70元      

現在新的需求來了,受移動網際網路發展的影響,書店必須打折來維持書店的生存。所有40元以上的書籍9折銷售,其他的8折銷售。

該如何實作這個變化,有三種方式:

● 修改接口

在IBook上新增加一個方法getOffPrice(),專門用于進行打折處理,所有的實作類實作該方法。但是這樣修改的後果就是,實作類NovelBook要修改,BookStore中的main方法也修改,同時IBook作為接口應該是穩定且可靠的,不應該經常發生變化,否則接口作為契約的作用就失去了效能。是以,該方案否定。

● 修改實作類

修改NovelBook類中的方法,直接在getPrice()中實作打折處理。該方法在項目有明确的章程(團隊内限制)或優良的架構設計時,是一個非常優秀的方法,但是該方法還是有缺陷的。例如采購書籍人員也是要看價格的,由于該方法已經實作了打折處理價格,是以采購人員看到的也是打折後的價格,會因資訊不對稱而出現決策失誤的情況。是以,該方案也不是一個最優的方案。

● 通過擴充實作變化

增加一個子類OffNovelBook,覆寫getPrice方法,高層次的子產品(也就是static靜态子產品區)通過OffNovelBook類産生新的對象,完成業務變化對系統的最小化開發。好辦法,修改也少,風險也小,修改後的類圖如圖6-2所示。

6-2:書店售書類圖

設計模式—— 六:開閉原則什麼是開閉原則?開閉原則詳解為什麼要采用開閉原則?如何應用開閉原則?

打折銷售的小說類OffNovelBook:

僅僅覆寫了getPrice方法,通過擴充完成了新增加的業務。

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;
     }
}      

店打折銷售類:

需要依賴子類,稍作修改。

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元      
開閉原則對擴充開放,對修改關閉,并不意味着不做任何修改,低層子產品的變 更,必然要有高層子產品進行耦合,否則就是一個孤立無意義的代碼片段。

開閉原則是最基礎的一個原則,其餘的原則都是開閉原則的具體形态, 也就是說其餘五個原則就是指導設計的工具和方法。換一個角度 來了解,依照Java語言的稱謂,開閉原則是抽象類,其他五大原則是具體的實作類。

可通過以下幾個方面來了解其重要性:

以上面提到的書店售書為例,IBook接口寫完了,實作類NovelBook也寫好了,需要寫一個測試類進行測試。

小說類的單元測試:

public class NovelBookTest extends TestCase {
     private String name = "平凡的世界";
     private int price = 6000;
     private String author = "路遙";      
     private IBook novelBook = new NovelBook(name,price,author);
     //測試getPrice方法
     public void testGetPrice() {
             //原價銷售,根據輸入和輸出的值是否相等進行斷言
             super.assertEquals(this.price, this.novelBook.getPrice());
     }
}      

一般一個方法的測試方法一般不少于3種——首先是正常的業務邏輯要保證測試到,其次是邊界條件要測試到,然後是異常要測試到,比較重要的方法的測試方法甚至有十多種,而且單元測試是對類的測試,類中的方法耦合是允許的,在這樣的條件下,如果再想着通過修改一個方法或多個方法代碼來完成變化,是很難做到的。

如果用擴充的方式,新增加的類,新增加的測試方法,隻要保證新增加類是正确的就可以了。

在面向對象的設計中,所有的邏輯都是從原子邏輯組合而來的,而不是在一個類中獨立實作一個業務邏輯。隻有這樣代碼才可以複用,粒度越小,被複用的可能性就越大。那為什麼要複用呢?減少代碼量,避免相同的邏輯分散在多個角落,避免日後的維護人員為了修改一個微小的缺陷或增加新功能而要在整個項目中到處查找相關的代碼。那怎麼才能提高複用率呢?縮小邏輯粒度,直到一個邏輯不可再拆分為止。

一款軟體投産後,維護人員的工作不僅僅是對資料進行維護,還可能要對程式進行擴充,維護人員最更意做的事情就是擴充一個類,而不是修改一個類。

萬物皆對象,我們需要把所有的事物都抽象成對象,然後針對對象進行操作,但是萬物皆運動,有運動就有變化,有變化就要有政策去應對,怎麼快速應對呢?這就需要在設計之初考慮到所有可能變化的因素,然後留下接口,等待“可能”轉變為“現實”。

開閉原則是一個比較抽象的原則,前面5個原則是對開閉原則的具體解釋,但是開閉原則并不局限于這麼多,它更多地像一句口号,一個目标,而沒有提出具體的實作辦法。這就需要自己在工作中領會精神,總結辦法。

通過接口或抽象類可以限制一組可能變化的行為,并且能夠實作對擴充開放,其包含三層含義:第一,通過接口或抽象類限制擴充,對擴充進行邊 界限定,不允許出現在接口或抽象類中不存在的public方法;第二,參數類型、引用對象盡 量使用接口或者抽象類,而不是實作類;第三,抽象層盡量保持穩定,一旦确定即不允許修改。

盡量使用中繼資料來控制程式的行為,減少重複開發。什麼是中繼資料?用來描述環境和資料的資料,通俗地說就是 配置參數,參數可以從檔案中獲得,也可以從資料庫中獲得。舉個非常簡單的例子,login方 法中提供了這樣的邏輯:先檢查IP位址是否在允許通路的清單中,然後再決定是否需要到數 據庫中驗證密碼(,該行為就是一個 典型的中繼資料控制子產品行為的例子。

在一個團隊中,建立項目章程是非常重要的,因為章程中指定了所有人員都必須遵守的 約定,對項目來說,約定優于配置。

對變化的封裝包含兩層含義:第一,将相同的變化封裝到一個接口或抽象類中;第二, 将不同的變化封裝到不同的接口或抽象類中,不應該有兩個不同的變化出現在同一個接口或 抽象類中。封裝變化,也就是受保護的變化(protected variations),找出預計有變化或不穩 定的點,為這些變化點建立穩定的接口,準确地講是封裝可能發生的變化,一旦預測到 或“第六感”發覺有變化,就可以進行封裝,23個設計模式都是從各個不同的角度對變化進行 封裝的。