天天看點

【設計模式】Java設計模式 - 開閉原則1、開閉原則的定義2、什麼是開閉原則3、為什麼要采用開閉原則?4、 如何使用開閉原則5、 最佳實踐

簡單記錄 - 慕課網 Java設計模式精講 Debug方式+記憶體分析 & 設計模式之禅-秦小波

都是書上的

文章目錄

  • 1、開閉原則的定義
  • 2、什麼是開閉原則
  • 3、為什麼要采用開閉原則?
  • 4、 如何使用開閉原則
  • 5、 最佳實踐

開閉原則是Java世界裡最基礎的設計原則,它指導我們如何建立一個穩定的、靈活的系統。

什麼是設計原則呢?設計模式(Design Pattern)就是一套被反複使用、多數人知曉的、經過分類編目的代碼設計經驗的總結。使用設計模式的目的是為了提高代碼的可重用性,讓代碼更容易被他人了解,使系統品質更加有保證、系統更加可靠。

1、開閉原則的定義

開閉原則(OCP:Open-Closed Principle)

  • 定義:一個軟體實體如類、子產品和函數應該對擴充開放,對修改關閉。

    (Software entities like classes,modules and functions should be open for extension but closed for modifications.)

    即軟體實體應盡量在不修改原有代碼的情況下進行擴充。

  • 用抽象建構架構,用實作擴充細節
  • 優點:提高軟體系統的可複用性及可維護性

根據開閉原則,在設計一個軟體系統子產品(類,方法)的時候,應該可以在不修改原有的子產品(修改關閉)的基礎上,能擴充其功能(擴充開放)。遵循開閉原則的系統設計,可以讓軟體系統可複用,并且易于維護。這也是系統設計需要遵循開閉原則的原因:

1.穩定性。開閉原則要求擴充功能不修改原來的代碼,這可以讓 軟體系統在變化中保持穩定。

2.擴充性。開閉原則要求對擴充開放,通過擴充提供新的或改變原有的功能,讓軟體系統具有靈活的可擴充性。

那什麼是開閉原則,為什麼要使用開閉原則,怎麼使用開閉原則呢?

2、什麼是開閉原則

什麼是開閉原則,開閉原則的廬山真面目。

開閉原則的定義明确地告訴我們:軟體實體應該對擴充開放,對修改關閉,

其含義是說一個軟體實體應該通過擴充來實作變化,而不是通過修改已有的代碼來實作變化。那什麼又是軟體實體呢?

軟體實體包括以下幾個部分:

  • 項目或軟體産品中按照一定的邏輯規則劃分的子產品。
  • 抽象和類。
  • 方法。

一個軟體産品隻要在生命期内,都會發生變化,既然變化是一個既定的事實,我們就應該在設計時盡量适應這些變化,以提高項目的穩定性和靈活性,真正實作“擁抱變化”。

開閉原則告訴我們應盡量通過擴充軟體實體的行為來實作變化,而不是通過修改已有的代碼來完成變化,它是為軟體實體的未來事件而制定的對現行開發設計進行限制的一個原則。

注意 (開閉原則對擴充開放,對修改關閉,并不意味着不做任何修改,低層子產品的變更,必然要有高層子產品進行耦合,否則就是一個孤立無意義的代碼片段。底層子產品變更可能導緻高層子產品的少量修改。)

看起來好迷惑,那就舉例說明什麼是開閉原則,我們以書店銷售書籍為例,其類圖如圖所示。

書店售書類圖

【設計模式】Java設計模式 - 開閉原則1、開閉原則的定義2、什麼是開閉原則3、為什麼要采用開閉原則?4、 如何使用開閉原則5、 最佳實踐

實線箭頭 關聯關系

三星空心虛線 實作關系

IBook定義了資料的三個屬性:名稱name、價格price和作者author。小說類NovelBook是一個具體的實作類,是所有小說書籍的總稱,BookStore指的是書店,IBook接口如代碼所示。

書籍接口 interface IBook

IBook.java

package ZenofDesignPatterns.ocp.section1;

/**
 * @author Liu Awen 
 * 書籍接口
 */
public interface IBook {
	
	//書籍有名稱
	public String getName();
	
	//書籍有售價
	public int getPrice();
	
	//書籍有作者
	public String getAuthor();
}

           

目前書店隻出售小說類書籍,小說類如代碼所示。

小說類NovelBook實作了接口IBook

NovelBook.java

package ZenofDesignPatterns.ocp.section1;

/**
 * 小說書籍
 */
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倍,減少精度帶來的誤差。價格可以設定為int型,不需要設定double型的。

NumberFormat formatter = 		 NumberFormat.getCurrencyInstance();
formatter.setMaximumFractionDigits(2);//Fraction分數
System,out.println(formatter.format(book.getPrice()/100.0)+"元");
           

在運算過程中擴大100倍,在需要展示時再縮小100倍,減少精度帶來的誤差。

書店售書的過程如代碼所示。

書店售書類 BookStore.java

package ZenofDesignPatterns.ocp.section1;

import java.text.NumberFormat;
import java.util.ArrayList;

/**
 * 書店銷售書籍
 */
public class BookStore {
	private final static ArrayList<IBook> bookList = new ArrayList<IBook>();
	
	//靜态子產品初始化,項目中一般是從持久層初始化産生
	static{
		bookList.add(new NovelBook("設計模式-可複用面向對象軟體的基礎",3500,"GoF"));
		bookList.add(new NovelBook("天龍八部",3200,"金庸"));//價格乘100了
		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中聲明了一個靜态子產品,實作了資料的初始化,這部分應該是從持久層産生的,由持久層架構進行管理,運作結果如下:

------------書店買出去的書籍記錄如下:---------------------
書籍名稱:設計模式-可複用面向對象軟體的基礎	書籍作者:GoF	書籍價格:$35.00元
書籍名稱:天龍八部	書籍作者:金庸	書籍價格:$32.00元
書籍名稱:巴黎聖母院	書籍作者:雨果	書籍價格:$56.00元
書籍名稱:悲慘世界	書籍作者:雨果	書籍價格:$35.00元
書籍名稱:金瓶梅	書籍作者:蘭陵笑笑生	書籍價格:$43.00元

Process finished with exit code 0

           

修改 改變

賣實體書不行了,書店為了生存開始打折銷售:所有40元以上的書籍9折銷售,其他的8折銷售。

對已經投産的項目來說,這就是一個變化,我們應該如何應對這樣一個需求變化?有如下三種方法可以解決這個問題:

  • 修改接口

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

  • 修改實作類

    修改NovelBook類中的方法,直接在getPrice()中實作打折處理,這是個不錯的好辦法,我們在項目中經常使用的就是這樣的辦法,通過class檔案替換的方式可以完成部分業務變化(或是缺陷修複)。

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

    該方法修改了代碼,不是擴充。

  • 通過擴充實作變化

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

    修改後的類圖如圖所示。

    擴充後的書店售書類圖

    【設計模式】Java設計模式 - 開閉原則1、開閉原則的定義2、什麼是開閉原則3、為什麼要采用開閉原則?4、 如何使用開閉原則5、 最佳實踐

    箭頭實線 關聯關系 一個類把另一個類的對象作為屬性

    三角空心虛線 實作關系 NovelBook實作了一個接口IBook

OffNovelBook類繼承了NovelBook,并覆寫了getPrice方法,不修改原有的代碼。新增加的子類OffNovelBook如代碼所示。

OffNovelBook.java

package ZenofDesignPatterns.ocp.section2;

/**
 * 打折銷售的圖書
 */
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折,不大于40元,打八折
			offPrice = selfPrice * 90 /100;
		}else{
			offPrice = selfPrice * 80 /100;
		}
		
		return offPrice;
	}
	
}

           

僅僅覆寫了getPrice方法,通過擴充完成了新增加的業務。書店類BookStore需要依賴子類,代碼稍作修改,如代碼所示。

書店打折銷售類

package ZenofDesignPatterns.ocp.section2;

import ZenofDesignPatterns.ocp.section1.NovelBook;

import java.awt.print.Book;
import java.text.NumberFormat;
import java.util.ArrayList;

/**
 * 打折的
 * 書店銷售書籍
 */
public class BookStore {
	private final static ArrayList<IBook> bookList = new ArrayList<IBook>();
	
	//靜态子產品初始化,項目中一般是從持久層初始化産生
	static{
		bookList.add(new OffNovelBook("設計模式-可複用面向對象軟體的基礎",3500,"GoF"));
		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)+"元");
		}
	}
}

           

我們隻是簡單修改了下static靜态子產品區,通過OffNovelBook類産生新的對象。

運作結果如下所示

------------書店買出去的書籍記錄如下:---------------------
書籍名稱:設計模式-可複用面向對象軟體的基礎	書籍作者:GoF	書籍價格:$28.00元
書籍名稱:天龍八部	書籍作者:金庸	書籍價格:$25.60元
書籍名稱:巴黎聖母院	書籍作者:雨果	書籍價格:$50.40元
書籍名稱:悲慘世界	書籍作者:雨果	書籍價格:$28.00元
書籍名稱:金瓶梅	書籍作者:蘭陵笑笑生	書籍價格:$38.70元

           

打折銷售開發完成了。我們增加了一個OffNoveBook類後,業務邏輯還是修改了,修改了static靜态子產品區域。這部分确實修改了,該部分屬于高層次的子產品,是由持久層産生的,在業務規則改變的情況下高層子產品必須有部分改變以适應新業務,改變要盡量地少,防止變化風險的擴散。

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

我們可以把變化歸納為以下三種類型:

●邏輯變化

隻變化一個邏輯,而不涉及其他子產品,比如原有的一個算法是ab+c,現在需要修改為ab*c,可以通過修改原有類中的方法的方式來完成,前提條件是所有依賴或關聯類都按照相同的邏輯處理。

●子子產品變化

一個子產品變化,會對其他的子產品産生影響,特别是一個低層次的子產品變化必然引起高層子產品的變化,是以在通過擴充完成變化時,高層次的子產品修改是必然的,剛剛的書籍打折處理就是類似的處理子產品,該部分的變化甚至會引起界面的變化。

●可見視圖變化

可見視圖是提供給客戶使用的界面,如JSP程式、Swing界面等,該部分的變化一般會引起連鎖反應(特别是在國内做項目,做歐美的外包項目一般不會影響太大)。如果僅僅是界面上按鈕、文字的重新排布倒是簡單,最司空見慣的是業務耦合變化,什麼意思呢?一個展示資料的清單,按照原有的需求是6列,突然有一天要增加1列,而且這一列要跨N張表,處理M個邏輯才能展現出來,這樣的變化是比較恐怖的,但還是可以通過擴充來完成變化,這就要看我們原有的設計是否靈活。

我們再來回顧一下書店銷售書籍的程式,首先是我們有一個還算靈活的設計(不靈活是什麼樣子?BookStore中所有使用到IBook的地方全部修改為實作類,然後再擴充一個ComputerBook書籍,你就知道什麼是不靈活了);然後有一個需求變化,我們通過擴充一個子類擁抱了變化;最後把子類投入運作環境中,新邏輯正式投産。通過分析,我們發現并沒有修改原有的子產品代碼,IBook接口沒有改變,NovelBook類沒有改變,這屬于已有的業務代碼,我們保持了曆史的純潔性。放棄修改曆史的想法吧,一個項目的基本路徑應該是這樣的:項目開發、重構、測試、投産、運維,其中的重構可以對原有的設計和代碼進行修改,運維盡量減少對原有代碼的修改,保持曆史代碼的純潔性,提高系統的穩定性。

3、為什麼要采用開閉原則?

每個事物的誕生都有它存在的必要性,存在即合理,那開閉原則的存在也是合理的,為什麼這麼說呢?

首先,開閉原則非常著名,隻要是做面向對象程式設計的,甭管是什麼語言,Java也好,C++也好,或者是Smalltalk,在開發時都會提及開閉原則。

其次,開閉原則是最基礎的一個原則,其他的設計原則都是開閉原則的具體形态,也就是說其他的設計原則就是指導設計的工具和方法,而開閉原則才是其精神領袖。換一個角度來了解,依照Java語言的稱謂,開閉原則是抽象類,其他五大原則是具體的實作類,開閉原則在面向對象設計領域中的地位就類似于牛頓第一定律在力學、勾股定律在幾何學、質能方程在狹義相對論中的地位,其地位無人能及。最後,開閉原則是非常重要的,可通過以下幾個方面來了解其重要性。

1.開閉原則對測試的影響

所有已經投産的代碼都是有意義的,并且都受系統規則的限制,這樣的代碼都要經過“千錘百煉”的測試過程,不僅保證邏輯是正确的,還要保證苛刻條件(高壓力、異常、錯誤)下不産生“有毒代碼”(Poisonous Code),是以有變化提出時,我們就需要考慮一下,原有的健壯代碼是否可以不修改,僅僅通過擴充實作變化呢?否則,就需要把原有的測試過程回籠一遍,需要進行單元測試、功能測試、內建測試甚至是驗收測試,現在雖然在大力提倡自動化測試工具,但是仍然代替不了人工的測試工作。

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

小說類的單元測試

NovelBookTest.java

【設計模式】Java設計模式 - 開閉原則1、開閉原則的定義2、什麼是開閉原則3、為什麼要采用開閉原則?4、 如何使用開閉原則5、 最佳實踐

單元測試通過,顯示綠條。在單元測試中,有一句非常有名的話,叫做"Keep the bar green to keep the code clean",即保持綠條有利于代碼整潔,這是什麼意思呢?綠條就是Junit運作的兩種結果中的一種:要麼是紅條,單元測試失敗;要麼是綠條,單元測試通過。一個方法的測試方法一般不少于3種,為什麼呢?首先是正常的業務邏輯要保證測試到,其次是邊界條件要測試到,然後是異常要測試到,比較重要的方法的測試方法甚至有十多種,而且單元測試是對類的測試,類中的方法耦合是允許的,在這樣的條件下,如果再想着通過修改一個方法或多個方法代碼來完成變化,基本上就是癡人說夢,該類的所有測試方法都要重構,想象一下你在一堆你并不熟悉的代碼中進行重構時的感覺吧!

重新測試 修改了代碼 還是直接擴充吧

在書店售書的例子中,增加了一個打折銷售的需求,如果我們直接修改getPrice方法來實作業務需求的變化,那就要修改單元測試類。想想看,我們舉的這個例子是非常簡單的,如果是一個複雜的邏輯,你的測試類就要修改得面目全非。還有,在實際的項目中,一個類一般隻有一個測試類,其中可以有很多的測試方法,在一堆本來就很複雜的斷言中進行大量修改,難免會出現測試遺漏情況,這是項目經理很難容忍的事情。

是以,我們需要通過擴充來實作業務邏輯的變化,而不是修改。上面的例子中通過增加一個子類OffNovelBook來完成了業務需求的變化,這對測試有什麼好處呢?我們重新生成一個測試檔案OffNovelBookTest,然後對getPrice進行測試,單元測試是孤立測試,隻要保證我提供的方法正确就成了,其他的我不管,OffNovelBookTest如代碼所示。

打折銷售的小說類單元測試

【設計模式】Java設計模式 - 開閉原則1、開閉原則的定義2、什麼是開閉原則3、為什麼要采用開閉原則?4、 如何使用開閉原則5、 最佳實踐

新增加的類,新增加的測試方法,隻要保證新增加類是正确的就可以了。

使用*extends(繼承)*的方法實作原有的類的方法以及擴充其中的應用,應用去系統更新,替換實作類即可,不需要太多變動。

直接擴充 不用修改

2.開閉原則可以提高複用性

在面向對象的設計中,所有的邏輯都是從原子邏輯組合而來的,而不是在一個類中獨立實作一個業務邏輯。隻有這樣代碼才可以複用,粒度越小,被複用的可能性就越大。

那為什麼要複用呢?減少代碼量,避免相同的邏輯分散在多個角落,避免日後的維護人員為了修改一個微小的缺陷或增加新功能而要在整個項目中到處查找相關的代碼,然後發出對開發人員“極度失望”的感慨。

那怎麼才能提高複用率呢?縮小邏輯粒度,直到一個邏輯不可再拆分為止。

3.開閉原則可以提高可維護性

一款軟體投産後,維護人員的工作不僅僅是對資料進行維護,還可能要對程式進行擴充,維護人員最樂意做的事情就是擴充一個類,而不是修改一個類,甭管原有的代碼寫得多麼優秀還是多麼糟糕,讓維護人員讀懂原有的代碼,然後再修改,是一件很痛苦的事情,不要讓他在原有的代碼海洋裡遊弋完畢後再修改,那是對維護人員的一種折磨和摧殘。

修改類遠不如擴充一個類

4.面向對象開發的要求

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

接口程式設計

4、 如何使用開閉原則

開閉原則是一個非常虛的原則,其他的設計原則是對開閉原則的具體解釋,但是開閉原則并不局限于這麼多,它“虛”得沒有邊界,就像“好好學習,天天向上”的口号一樣,告訴我們要好好學習,但是學什麼,怎麼學并沒有告訴我們,需要去體會和掌握,開閉原則也是一個口号,那我們怎麼把這個口号應用到實際工作中呢?

  • 抽象限制。首先通過接口或抽象類限制擴充,對擴充進行邊界限定,不允許出現在接口或抽象類中不存在的public方法;第二,參數類型,引用對象盡量使用接口或者抽象類,而不是實作類;第三,抽象層盡量保持穩定,一旦确定既不允許修改。
  • 中繼資料(metadata)控制子產品行為。盡量使用中繼資料(用來描述環境和資料的資料,通俗的說就是配置參數)來控制程式的行為,減少重複開發。
  • 制定項目章程。對于項目來說,約定優于配置。
  • 封裝變化。第一,将相同的變化封裝到一個接口或抽象類中;第二,将不同的變化封裝到不同的接口或抽象類中,不應該有兩個不同的變化出現在同一個接口或抽象類中。

1.抽象限制

抽象是對一組事物的通用描述,沒有具體的實作,也就表示它可以有非常多的可能性,可以跟随需求的變化而變化。是以,通過接口或抽象類可以限制一組可能變化的行為,并且能夠實作對擴充開放,其包含三層含義:

第一,通過接口或抽象類限制擴充,對擴充進行邊界限定,不允許出現在接口或抽象類中不存在的public方法;

第二,參數類型、引用對象盡量使用接口或者抽象類,而不是實作類;

第三,抽象層盡量保持穩定,一旦确定即不允許修改。還是以書店為例,目前隻是銷售小說類書籍,單一經營畢竟是有風險的,于是書店新增加了計算機書籍,它不僅包含書籍名稱、作者、價格等資訊,還有一個獨特的屬性:面向的是什麼領域,也就是它的範圍,比如是和程式設計語言相關的,還是和資料庫相關的,等等,修改後的類圖如圖所示。

【設計模式】Java設計模式 - 開閉原則1、開閉原則的定義2、什麼是開閉原則3、為什麼要采用開閉原則?4、 如何使用開閉原則5、 最佳實踐

增加了一個接口IComputerBook和實作類Computer- Book,而BookStore不用做任何修改就可以完成書店銷售計算機書籍的業務。計算機書籍接口如代碼所示。

計算機書籍接口 IComputerBook.java

package ZenofDesignPatterns.ocp.section4;

/**
 * 計算機書籍
 */
public interface IComputerBook extends IBook{
	
	//計算機書籍是有一個範圍
	public String getScope();
}

           

很簡單,計算機書籍增加了一個方法,就是獲得該書籍的範圍,同時繼承IBook接口,畢竟計算機書籍也是書籍,其實作如代碼所示。計算機書籍類

package ZenofDesignPatterns.ocp.section4;

/**
 * 計算機書籍
 */
public class ComputerBook implements IComputerBook {
	private String name;
	private String scope;
	private String author;
	private int price;
	
	public ComputerBook(String _name,int _price,String _author,String _scope){
		this.name=_name;
		this.price = _price;
		this.author = _author;
		this.scope = _scope;
	}
	
	public String getScope() {
		return this.scope;
	}

	public String getAuthor() {
		return this.author;
	}

	public String getName() {
		return this.name;
	}
	
	public int getPrice() {
		return this.price;
	}

}

           

這也很簡單,實作IComputerBook就可以,而BookStore類沒有做任何的修改,隻是在static靜态子產品中增加一條資料,如代碼所示。書店銷售計算機書籍

package ZenofDesignPatterns.ocp.section4;

import java.text.NumberFormat;
import java.util.ArrayList;

/**
 * 書店銷售書籍
 */
public class BookStore {
	private final static ArrayList<IBook> bookList = new ArrayList<IBook>();
	
	//靜态子產品初始化,項目中一般是從持久層初始化産生
	static{
		bookList.add(new NovelBook("設計模式-可複用面向對象軟體的基礎",3500,"GoF"));
		bookList.add(new NovelBook("天龍八部",3200,"金庸"));//價格乘100了
		bookList.add(new NovelBook("巴黎聖母院",5600,"雨果"));
		bookList.add(new NovelBook("悲慘世界",3500,"雨果"));
		bookList.add(new NovelBook("金瓶梅",4300,"蘭陵笑笑生"));
		//增加計算機書籍
		bookList.add(new ComputerBook("Think in Java",4300,"Bruce Eckel","程式設計語言"));
	}
	
	//模拟書店買書
	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)+"元");
		}
	}
}

           

書店開始銷售計算機書籍,運作結果如下所示。

------------書店買出去的書籍記錄如下:---------------------
書籍名稱:設計模式-可複用面向對象軟體的基礎	書籍作者:GoF	書籍價格:$35.00元
書籍名稱:天龍八部	書籍作者:金庸	書籍價格:$32.00元
書籍名稱:巴黎聖母院	書籍作者:雨果	書籍價格:$56.00元
書籍名稱:悲慘世界	書籍作者:雨果	書籍價格:$35.00元
書籍名稱:金瓶梅	書籍作者:蘭陵笑笑生	書籍價格:$43.00元
書籍名稱:Think in Java	書籍作者:Bruce Eckel	書籍價格:$43.00元

Process finished with exit code 0
           

如果我是負責維護的,我就非常樂意做這樣的事情,簡單而且不需要與其他的業務進行耦合。

我唯一需要做的事情就是在原有的代碼上添磚加瓦,然後就可以實作業務的變化。

我們來看看這段代碼有哪幾層含義。

首先,ComputerBook類必須實作IBook的三個方法,是通過IComputerBook接口傳遞進來的限制,也就是我們制定的IBook接口對擴充類ComputerBook産生了限制力,正是由于該限制力,BookStore類才不需要進行大量的修改。

其次,如果原有的程式設計采用的不是接口,而是實作類,那會出現什麼問題呢?

之前是

private final static ArrayList<IBook> bookList = new ArrayList<IBook>();

我們把BookStore類中的私有變量bookList修改一下,如下面的代碼所示。

private final static ArrayList<NovelBook> bookList = new ArrayList<NovelBook>();

把原有IBook的依賴修改為對NovelBook實作類的依賴,想想看,我們這次的擴充是否還能繼續下去呢?

一旦這樣設計,我們就根本沒有辦法擴充,需要修改原有的業務邏輯(也就是main方法),這樣的擴充基本上就是形同虛設。

最後,如果我們在IBook上增加一個方法getScope,是否可以呢?

答案是不可以,因為原有的實作類NovelBook已經在投産運作中,它不需要該方法,而且接口是與其他子產品交流的契約,修改契約就等于讓其他子產品修改。是以,**接口或抽象類一旦定義,就應該立即執行,不能有修改接口的思想,除非是徹底的大返工。**是以,要實作對擴充開放,首要的前提條件就是抽象限制。

2.中繼資料(metadata)控制子產品行為

程式設計是一個很苦很累的活,那怎麼才能減輕我們的壓力呢?答案是盡量使用中繼資料來控制程式的行為,減少重複開發。什麼是中繼資料?用來描述環境和資料的資料,通俗地說就是配置參數,參數可以從檔案中獲得,也可以從資料庫中獲得。舉個非常簡單的例子,login方法中提供了這樣的邏輯:先檢查IP位址是否在允許通路的清單中,然後再決定是否需要到資料庫中驗證密碼(如果采用SSH架構,則可以通過Struts的攔截器來實作),該行為就是一個典型的中繼資料控制子產品行為的例子,其中達到極緻的就是控制反轉(Inversion of Control),使用最多的就是Spring容器,在SpringContext配置檔案中,基本配置如代碼所示。

SpringContext的基本配置檔案

【設計模式】Java設計模式 - 開閉原則1、開閉原則的定義2、什麼是開閉原則3、為什麼要采用開閉原則?4、 如何使用開閉原則5、 最佳實踐

通過建立一個Father類的子類Son,完成一個新的業務,同時修改SpringContext檔案,修改後的檔案如代碼所示。

【設計模式】Java設計模式 - 開閉原則1、開閉原則的定義2、什麼是開閉原則3、為什麼要采用開閉原則?4、 如何使用開閉原則5、 最佳實踐

擴充後的SpringContext配置檔案[插圖]通過擴充一個子類,修改配置檔案,完成了業務變化,這也是采用架構的好處。

3.制定項目章程

在一個團隊中,建立項目章程是非常重要的,因為章程中指定了所有人員都必須遵守的約定,對項目來說,約定優于配置。相信大家都做過項目,會發現一個項目會産生非常多的配置檔案。舉個簡單的例子,以SSH項目開發為例,一個項目中的Bean配置檔案就非常多,管理非常麻煩。如果需要擴充,就需要增加子類,并修改SpringContext檔案。然而,如果你在項目中指定這樣一個章程:所有的Bean都自動注入,使用Annotation進行裝配,進行擴充時,甚至隻用寫一個子類,然後由持久層生成對象,其他的都不需要修改,這就需要項目内限制,每個項目成員都必須遵守,該方法需要一個團隊有較高的自覺性,需要一個較長時間的磨合,一旦項目成員都熟悉這樣的規則,比通過接口或抽象類進行限制效率更高,而且擴充性一點也沒有減少。

約定優于配置

4.封裝變化

對變化的封裝包含兩層含義:

第一,将相同的變化封裝到一個接口或抽象類中;

第二,将不同的變化封裝到不同的接口或抽象類中,不應該有兩個不同的變化出現在同一個接口或抽象類中。

封裝變化,也就是受保護的變化(protected variations),找出預計有變化或不穩定的點,我們為這些變化點建立穩定的接口,準确地講是封裝可能發生的變化,一旦預測到或“第六感”發覺有變化,就可以進行封裝,23個設計模式都是從各個不同的角度對變化進行封裝的,要學好這些設計模式。

5、 最佳實踐

軟體設計最大的難題就是應對需求的變化,但是紛繁複雜的需求變化又是不可預料的。我們要為不可預料的事情做好準備,這本身就是一件非常痛苦的事情,但是大師們還是給我們提出了非常好的6大設計原則以及23個設計模式來“封裝”未來的變化。

Single Responsibility Principle:單一職責原則

Open Closed Principle:開閉原則

Liskov Substitution Principle:裡氏替換原則

Law of Demeter:迪米特法則

Interface Segregation Principle:接口隔離原則

Dependence Inversion Principle:依賴倒置原則

把這6個原則的首字母(裡氏替換原則和迪米特法則的首字母重複,隻取一個)聯合起來就是SOLID(solid,穩定的),

其代表的含義也就是把這6個原則結合使用的好處:建立穩定、靈活、健壯的設計,而開閉原則又是重中之重,是最基礎的原則,是其他5大原則的精神領袖。我們在使用開閉原則時要注意以下幾個問題。

●開閉原則也隻是一個原則

開閉原則隻是精神口号,實作擁抱變化的方法非常多,并不局限于這6大設計原則,但是遵循這6大設計原則基本上可以應對大多數變化。

是以,我們在項目中應盡量采用這6大原則,适當時候可以進行擴充,例如通過類檔案替換的方式完全可以解決系統中的一些缺陷。大家在開發中比較常用的修複缺陷的方法就是類替換,比如一個軟體産品已經在運作中,發現了一個缺陷,需要修正怎麼辦?如果有自動更新功能,則可以下載下傳一個.class檔案直接覆寫原有的class,重新啟動應用(也不一定非要重新啟動)就可以解決問題,也就是通過類檔案的替換方式修正了一個缺陷,當然這種方式也可以應用到項目中,正在運作中的項目發現需要增加一個新功能,通過修改原有實作類的方式就可以解決這個問題,前提條件是:類必須做到高内聚、低耦合,否則類檔案的替換會引起不可預料的故障。

●項目規章

非常重要如果你是一位項目經理或架構師,應盡量讓自己的項目成員穩定,穩定後才能建立高效的團隊文化,章程是一個團隊所有成員共同的知識結晶,也是所有成員必須遵守的約定。優秀的章程能帶給項目帶來非常多的好處,如提高開發效率、降低缺陷率、提高團隊士氣、提高技術成員水準,等等。

●預知變化

在實踐中過程中,架構師或項目經理一旦發現有發生變化的可能,或者變化曾經發生過,則需要考慮現有的架構是否可以輕松地實作這一變化。架構師設計一套系統不僅要符合現有的需求,還要适應可能發生的變化,這才是一個優良的架構。開閉原則是一個終極目标,任何人包括大師級人物都無法百分之百做到,但朝這個方向努力,可以非常顯著地改善一個系統的架構,真正做到“擁抱變化”。

“開-閉”原則,即“對擴充開放,對修改關閉”,指的是在軟體設計中,當對某一軟體實體進行擴充時,盡量不要去修改原有的軟體實體,換句話說就是在不修改原有軟體實體的情況下對其進行擴充。這種軟體設計方式其實是一種目标,它考慮了軟體後期的維護等其他因素。一個軟體系統如果可以達到這個目标,那它就是可擴充、易維護的。在軟體設計領域中,衆多設計模式的應用就是為了達到這個目标。“開-閉”原則是一種很抽象的設計原則,更像是一種倡導的口号,其他設計原則都是為了實作“開-閉”原則的具體原則。“開-閉”原則與其他設計原則就好比抽象父類和子類的關系一樣。