原文鏈接:http://www.lxweimin.com/p/a9f397c4ff98
1 場景問題#
大家都知道,在Java應用開發中,要“面向接口編程”。那么什么是接口?接口有什么作用?接口如何使用?一起來回顧一下。
1.1 接口回顧##
- Java中接口的概念
在Java中接口是一種特殊的抽象類,跟一般的抽象類相比,接口里面的所有方法都是抽象方法,接口里面的所有屬性都是常量。也就是說,接口里面是只有方法定義而不會有任何方法實現。
- 接口用來干什么
通常用接口來定義實現類的外觀,也就是實現類的行為定義,用來約束實現類的行為。接口就相當于一份契約,根據外部應用需要的功能,約定了實現類應該要實現的功能,但是具體的實現類除了實現接口約定的功能外,還可以根據需要實現一些其它的功能,這是允許的,也就是說實現類的功能包含但不僅限于接口約束的功能。
通過使用接口,可以實現不相關類的相同行為,而不需考慮這些類之間的層次關系,接口就是實現類對外的外觀。
- 接口的思想
根據接口的作用和用途,濃縮下來,接口的思想就是“封裝隔離”。
通常提到封裝是指對數據的封裝,但是這里的封裝是指“對被隔離體的行為的封裝”,或者是“對被隔離體的職責的封裝”;而隔離指的是外部調用和內部實現,外部調用只能通過接口進行調用,而外部調用是不知道內部具體實現的,也就是說外部調用和內部實現是被接口隔離開的。
- 接口的好處
由于外部調用和內部實現被接口隔離開了,那么只要接口不變,內部實現的變化就不會影響到外部應用,從而使得系統更靈活,具有更好的擴展性和可維護性,這也就是所謂“接口是系統可插拔性的保證”這句話的意思。
- 接口和抽象類的選擇
既然接口是一種特殊的抽象類,那么在開發中,何時選用接口,何時選用抽象類呢?對于它們的選擇,在開發中是一個很重要的問題,特別總結兩句話給大家:優先選用接口。在如下情況應選擇抽象類:既要定義子類的行為,又要為子類提供公共的功能。
1.2 面向接口編程##
面向接口編程是Java編程中的一個重要原則。
在Java 程序設計里面,非常講究層的劃分和模塊的劃分。通常按照三層來劃分Java程序,分別是表現層、邏輯層、數據層,它們之間都要通過接口來通訊。
在每一個層里面,又有很多個小模塊,一個小模塊對外也應該是一個整體,那么一個模塊對外也應該提供接口,其它地方需要使用到這個模塊的功能,都應該通過此接口來進行調用。這也就是常說的“接口是被其隔離部分的外觀”。基本的三層結構如圖所示:
在一個層內部的各個模塊交互也要通過接口,如圖所示:
上面頻頻提到“組件”,那么什么是組件呢?先簡單的名詞解釋一下:
所謂組件:從設計上講,組件就是能完成一定功能的封裝體。小到一個類,大到一個系統,都可以稱為組件,因為一個小系統放到更大的系統里面去,也就當個組件而已。事實上,從設計的角度看,系統、子系統、模塊、組件等說的其實是同一回事情,都是完成一定功能的封裝體,只不過功能多少不同而已。
繼續剛才的思路,大家會發現,不管是一層還是一個模塊或者一個組件,都是一個被接口隔離的整體,那么下面我們就不去區分它們,統一認為都是接口隔離體即可,如圖所示:
既然在Java中需要面向接口編程,那么在程序中到底如何使用接口,來做到真正的面向接口編程呢?
1.3 不用模式的解決方案##
回憶一下,以前是如何使用接口的呢,假設有一個接口叫Api,然后有一個實現類Impl實現了它,在客戶端怎么用這個接口呢?
通常都是在客戶端創建一個Impl的實例,把它賦值給一個Api接口類型的變量,然后客戶端就可以通過這個變量來操作接口的功能了,此時具體的結構圖如圖:
- 先定義接口Api,示例代碼如下:
/**
* 某個接口(通用的、抽象的、非具體的功能)
*/
public interface Api {
/**
* 某個具體的功能方法的定義,用test1來演示一下。
* 這里的功能很簡單,把傳入的s打印輸出即可
* @param s 任意想要打印輸出的字符串
*/
public void test1(String s);
}
- 既然有了接口,自然就要有實現,定義實現Impl,示例代碼如下:
/**
* 對接口的實現
*/
public class Impl implements Api{
public void test1(String s) {
System.out.println("Now In Impl. The input s=="+s);
}
}
- 那么此時的客戶端怎么寫呢?按照Java的知識,接口不能直接使用,需要使用接口的實現類,示例代碼如下:
/**
* 客戶端:測試使用Api接口
*/
public class Client {
public static void main(String[] args) {
Api api = new Impl();
api.test1("哈哈,不要緊張,只是個測試而已!");
}
}
1.4 有何問題##
上面寫得沒錯吧,在Java的基礎知識里面就是這么學的,難道這有什么問題嗎?請仔細看位于客戶端的下面這句話:
Api api = new Impl();
然后再想想接口的功能和思想,發現什么了?仔細再想想?
你會發現在客戶端調用的時候,客戶端不但知道了接口,同時還知道了具體的實現就是Impl。而接口的思想是“封裝隔離”,而Impl這個實現類,應該是被接口Api封裝并同客戶端隔離開的,也就是說,客戶端根本就不應該知道具體的實現類是Impl。
把這個問題描述一下:
在Java編程中,出現只知接口而不知實現,該怎么辦?
就像現在的Client,它知道要使用Api接口,但是不知由誰實現,也不知道如何實現,從而得不到接口對象,就無法使用接口,該怎么辦呢?
2 解決方案#
2.1 簡單工廠來解決問題##
用來解決上述問題的一個合理的解決方案就是簡單工廠,那么什么是簡單工廠呢?
-
簡單工廠定義
簡單工廠定義 - 應用簡單工廠來解決的思路
分析上面的問題,雖然不能讓模塊外部知道模塊內的具體實現,但是模塊內部是可以知道實現類的,而且創建接口是需要具體實現類的。
這樣一來,客戶端就可以通過這個Factory來獲取需要的接口對象,然后調用接口的方法來實現需要的功能,而且客戶端也不用再關心具體實現了。
2.2 簡單工廠結構和說明##
簡單工廠的結構如圖所示:
Api:定義客戶所需要的功能接口
Impl:具體實現Api的實現類,可能會有多個
Factory:工廠,選擇合適的實現類來創建Api接口對象
Client:客戶端,通過Factory去獲取Api接口對象,然后面向Api接口編程
2.3 簡單工廠示例代碼##
- 先看看Api的定義,示例代碼如下
/**
* 接口的定義,該接口可以通過簡單工廠來創建
*/
public interface Api {
/**
* 示意,具體的功能方法的定義
* @param s 示意,需要的參數
*/
public void operation(String s);
}
- 定義了接口,該來實現它了,ImplA和ImplB的示例代碼如下:
/**
* 接口的具體實現對象A
*/
public class ImplA implements Api{
public void operation(String s) {
//實現功能的代碼,示意一下
System.out.println("ImplA s=="+s);
}
}
/**
* 接口的具體實現對象B
*/
public class ImplB implements Api{
public void operation(String s) {
// 實現功能的代碼,示意一下
System.out.println("ImplB s=="+s);
}
}
- 該來看看簡單工廠的實現,示例代碼如下:
/**
* 工廠類,用來創造Api對象
*/
public class Factory {
/**
* 具體的創造Api對象的方法
* @param condition 示意,從外部傳入的選擇條件
* @return 創造好的Api對象
*/
public static Api createApi(int condition) {
//應該根據某些條件去選擇究竟創建哪一個具體的實現對象,
//這些條件可以從外部傳入,也可以從其它途徑獲取。
//如果只有一個實現,可以省略條件,因為沒有選擇的必要。
//示意使用條件
Api api = null;
if(condition == 1) {
api = new ImplA();
} else if(condition == 2) {
api = new ImplB();
}
return api;
}
}
- 再來看看客戶端的示意,示例代碼如下:
/**
* 客戶端,使用Api接口
*/
public class Client {
public static void main(String[] args) {
//通過簡單工廠來獲取接口對象
Api api = Factory.createApi(1);
api.operation("正在使用簡單工廠");
}
}
2.4 使用簡單工廠重寫示例##
要使用簡單工廠來重寫前面的示例,主要就是要創建一個簡單工廠對象,讓簡單工廠來負責創建接口對象。然后讓客戶端通過工廠來獲取接口對象,而不再由客戶端自己去創建接口的對象了。此時系統的結構如圖所示:
- 接口Api和實現類Impl都和前面的示例一樣,就不去贅述了。
- 新創建一個簡單工廠的對象,示例代碼如下:
/**
* 工廠類,用來創造Api對象
*/
public class Factory {
/**
* 具體的創造Api對象的方法
* @return 創造好的Api對象
*/
public static Api createApi(){
//由于只有一個實現,就不用條件判斷了
return new Impl();
}
}
- 客戶端如何使用簡單工廠提供的功能呢?這個時候,客戶端就不用再自己去創建接口的對象了,應該使用工廠來獲取,經過改造,客戶端代碼如下:
/**
* 客戶端:測試使用Api接口
*/
public class Client {
public static void main(String[] args) {
//重要改變,沒有new Impl()了,取而代之Factory.createApi()
Api api = Factory.createApi();
api.test1("哈哈,不要緊張,只是個測試而已!");
}
}
就如同上面的示例,客戶端通過簡單工廠創建了一個實現接口的對象,然后面向接口編程,從客戶端來看,它根本就不知道具體的實現是什么,也不知道是如何實現的,它只知道通過工廠獲得了一個接口對象,然后就能通過這個接口來獲取想要的功能。
事實上,簡單工廠能幫助我們真正開始面向接口編程,像以前的做法,其實只是用到了接口的多態那部分的功能,最重要的“封裝隔離性”并沒有體現出來。
3 模式講解#
3.1 典型疑問##
首先來解決一個常見的疑問:可能有朋友會認為,上面示例中的簡單工廠看起來不就是把客戶端里面的“new Impl()”移動到簡單工廠里面嗎?不還是一樣通過new一個實現類來得到接口嗎?把“new Impl()”這句話放到客戶端和放到簡單工廠里面有什么不同嗎?
理解這個問題的重點就在于理解簡單工廠所處的位置。
根據前面的學習,我們知道接口是用來封裝隔離具體的實現的,目標就是不要讓客戶端知道封裝體內部的具體實現。簡單工廠的位置是位于封裝體內的,也就是簡單工廠是跟接口和具體的實現在一起的,算是封裝體內部的一個類,所以簡單工廠知道具體的實現類是沒有關系的。整理一下簡單工廠的結構圖,新的圖如圖所示:
圖中虛線框,就好比是一個組件的包裝邊界,表示接口、實現類和工廠類組合成了一個組件,在這個封裝體里面,只有接口和工廠是對外的,也就是讓外部知道并使用的,所以故意漏了一些在虛線框外,而具體的實現類是不對外的,被完全包含在虛線框內。
對于客戶端而言,只是知道了接口Api和簡單工廠Factory,通過Factory就可以獲得Api了,這樣就達到了讓Client在不知道具體實現類的情況下獲取接口Api。所以看似簡單的把“new Impl()”這句話從客戶端里面移動到了簡單工廠里面,其實是有了質的變化的。
3.2 認識簡單工廠##
- 簡單工廠的功能
工廠嘛,就是用來造東西的。在Java里面,通常情況下是用來造接口的,但是也可以造抽象類,甚至是一個具體的類實例。一定要注意,雖然前面的示例是利用簡單工廠來創建的接口,但是也是可以用簡單工廠來創建抽象類或者是普通類的實例的。
- 靜態工廠
使用簡單工廠的時候,通常不用創建簡單工廠類的類實例,沒有創建實例的必要。因此可以把簡單工廠類實現成一個工具類,直接使用靜態方法就可以了,也就是說簡單工廠的方法通常都是靜態的,所以也被稱為靜態工廠。如果要防止客戶端無謂的創造簡單工廠實例,還可以把簡單工廠的構造方法私有化了。
- 萬能工廠
一個簡單工廠可以包含很多用來構造東西的方法,這些方法可以創造不同的接口、抽象類或者是類實例,一個簡單工廠理論上可以構造任何東西,所以又稱之為“萬能工廠”。
雖然上面的實例中,在簡單工廠里面只有一個方法,但事實上,是可以有很多這樣創建方法的,這點要注意。
- 簡單工廠創建對象的范圍
雖然從理論上講,簡單工廠什么都能造,但對于簡單工廠可創建對象的范圍,通常不要太大,建議控制在一個獨立的組件級別或者一個模塊級別,也就是一個組件或模塊一個簡單工廠。否則這個簡單工廠類會職責不明,有點大雜燴的感覺。
-
簡單工廠的調用順序示意圖
簡單工廠的調用順序示意圖 簡單工廠命名的建議
類名建議為“模塊名稱+Factory”,比如:用戶模塊的工廠就稱為:UserFactory
方法名稱通常為“get+接口名稱”或者是“create+接口名稱”,比如:有一個接口名稱為UserEbi,那么方法名稱通常為:getUserEbi 或者是 createUserEbi。
當然,也有一些朋友習慣于把方法名稱命名為“new+接口名稱”,比如:newUserEbi,我們不是很建議。因為new在Java中代表特定的含義,而且通過簡單工廠的方法來獲取對象實例,并不一定每次都是要new一個新的實例。如果使用newUserEbi,這會給人錯覺,好像每次都是new一個新的實例一樣。
3.3 簡單工廠中方法的寫法##
雖然說簡單工廠的方法多是用來造接口的,但是仔細分析就會發現,真正能實現功能的是具體的實現類,這些實現類是已經做好的,并不是真的靠簡單工廠來創造出來的,簡單工廠的方法無外乎就是:實現了選擇一個合適的實現類來使用。
所以簡單工廠方法的內部主要實現的功能是“選擇合適的實現類”來創建實例對象。既然要實現選擇,那么就需要選擇的條件或者是選擇的參數,選擇條件或者是參數的來源通常又有幾種:
來源于客戶端,由Client來傳入參數
來源于配置文件,從配置文件獲取用于判斷的值
來源于程序運行期的某個值,比如從緩存中獲取某個運行期的值
下面來看個示例,看看由客戶端來傳入參數,如何寫簡單工廠中的方法。
- 在剛才的示例上再添加一個實現,稱為Impl2,示例代碼如下:
/**
* 對接口的一種實現
*/
public class Impl2 implements Api {
public void test1(String s) {
System.out.println("Now In Impl The input s=="+s);
}
}
- 現在對Api這個接口,有了兩種實現,那么工廠類該怎么辦呢?到底如何選擇呢?不可能兩個同時使用吧,看看新的工廠類,示例代碼如下:
/**
* 工廠類,用來創造Api的
*/
public class Factory {
/**
* 具體的創造Api的方法,根據客戶端的參數來創建接口
* @param type 客戶端傳入的選擇創造接口的條件
* @return 創造好的Api對象
*/
public static Api createApi(int type) {
//這里的type也可以不由外部傳入,而是直接讀取配置文件來獲取
//為了把注意力放在模式本身上,這里就不去寫讀取配置文件的代碼了
//根據type來進行選擇,當然這里的1和2應該做成常量
Api api = null;
if(type == 1) {
api = new Impl();
}else if(type == 2) {
api = new Impl2();
}
return api;
}
}
- 客戶端沒有什么變化,只是在調用Factory的createApi方法的時候需要傳入參數,示例代碼如下:
public class Client {
public static void main(String[] args) {
//注意這里傳遞的參數,修改參數就可以修改行為,試試看吧
Api api = Factory.createApi(2);
api.test1("哈哈,不要緊張,只是個測試而已!");
}
}
- 要注意這種方法有一個缺點
由于是從客戶端在調用工廠的時候,傳入選擇的參數,這就說明客戶端必須知道每個參數的含義,也需要理解每個參數對應的功能處理。這就要求必須在一定程度上,向客戶暴露一定的內部實現細節。
3.4 可配置的簡單工廠##
現在已經學會通過簡單工廠來選擇具體的實現類了,可是還有問題。比如:在現在的實現中,再新增加一種實現,會怎樣呢?
那就需要修改工廠類,才能把新的實現添加到現有系統中。比如現在新加了一個實現Impl3,那么需要類似下面這樣來修改工廠類:
public class Factory {
public static Api createApi(int type) {
Api api = null;
if(type == 1) {
api = new Impl();
}else if(type == 2) {
api = new Impl2();
}else if(type == 3) {
api = new Impl3();
}
return api;
}
}
每次新增加一個實現類都來修改工廠類的實現,肯定不是一個好的實現方式。那么現在希望新增加了實現類過后不修改工廠類,該怎么辦呢?
一個解決的方法就是使用配置文件,當有了新的實現類過后,只要在配置文件里面配置上新的實現類就好了,在簡單工廠的方法里面可以使用反射,當然也可以使用IoC/DI(控制反轉/依賴注入,這個不在這里討論)來實現。
看看如何使用反射加上配置文件,來實現添加新的實現類過后,無須修改代碼,就能把這個新的實現類加入應用中。
- 配置文件用最簡單的properties文件,實際開發中多是xml配置。定義一個名稱為“FactoryTest.properties”的配置文件,放置到Factory同一個包下面,內容如下:
ImplClass=cn.javass.dp.simplefactory.example5.Impl
如果新添加了實現類,修改這里的配置就可以了,就不需要修改程序了。
- 此時的工廠類實現如下:
/**
* 工廠類,用來創造Api對象
*/
public class Factory {
/**
* 具體的創造Api的方法,根據配置文件的參數來創建接口
* @return 創造好的Api對象
*/
public static Api createApi(){
//直接讀取配置文件來獲取需要創建實例的類
//至于如何讀取Properties,還有如何反射這里就不解釋了
Properties p = new Properties();
InputStream in = null;
try {
in = Factory.class.getResourceAsStream("FactoryTest.properties");
p.load(in);
} catch (IOException e) {
System.out.println("裝載工廠配置文件出錯了,具體的堆棧信息如下:");
e.printStackTrace();
}finally{
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//用反射去創建,那些例外處理等完善的工作這里就不做了
Api api = null;
try {
api = (Api)Class.forName(p.getProperty("ImplClass")).newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return api;
}
}
- 此時的客戶端就變得很簡單了,不再需要傳入參數,代碼示例如下:
public class Client {
public static void main(String[] args) {
Api api = Factory.createApi();
api.test1("哈哈,不要緊張,只是個測試而已!");
}
}
3.5 簡單工廠的優缺點##
幫助封裝:簡單工廠雖然很簡單,但是非常友好的幫助我們實現了組件的封裝,然后讓組件外部能真正面向接口編程。
解耦:通過簡單工廠,實現了客戶端和具體實現類的解耦。如同上面的例子,客戶端根本就不知道具體是由誰來實現,也不知道具體是如何實現的,客戶端只是通過工廠獲取它需要的接口對象。
可能增加客戶端的復雜度:如果通過客戶端的參數來選擇具體的實現類,那么就必須讓客戶端能理解各個參數所代表的具體功能和含義,這會增加客戶端使用的難度,也部分暴露了內部實現,這種情況可以選用可配置的方式來實現。
3.6 思考簡單工廠##
- 簡單工廠的本質
簡單工廠的本質是:選擇實現。
注意簡單工廠的重點在選擇,實現是已經做好了的。就算實現再簡單,也要由具體的實現類來實現,而不是在簡單工廠里面來實現。簡單工廠的目的在于為客戶端來選擇相應的實現,從而使得客戶端和實現之間解耦,這樣一來,具體實現發生了變化,就不用變動客戶端了,這個變化會被簡單工廠吸收和屏蔽掉。
- 何時選用簡單工廠?
建議在如下情況中,選用簡單工廠:
如果想要完全封裝隔離具體實現,讓外部只能通過接口來操作封裝體,那么可以選用簡單工廠,讓客戶端通過工廠來獲取相應的接口,而無需關心具體實現;
如果想要把對外創建對象的職責集中管理和控制,可以選用簡單工廠,一個簡單工廠可以創建很多的、不相關的對象,可以把對外創建對象的職責集中到一個簡單工廠來,從而實現集中管理和控制
3.7 相關模式##
- 簡單工廠和抽象工廠模式
簡單工廠是用來選擇實現的,可以選擇任意接口的實現,一個簡單工廠可以有多個用于選擇并創建對象的方法,多個方法創建的對象可以有關系也可以沒有關系。
抽象工廠模式是用來選擇產品簇的實現的,也就是說一般抽象工廠里面有多個用于選擇并創建對象的方法,但是這些方法所創建的對象之間通常是有關系的,這些被創建的對象通常是構成一個產品簇所需要的部件對象。
所以從某種意義上來說,簡單工廠和抽象工廠是類似的,如果抽象工廠退化成為只有一個實現,不分層次,那么就相當于簡單工廠了。
- 簡單工廠和工廠方法模式
簡單工廠和工廠方法模式也是非常類似的。工廠方法的本質也是用來選擇實現的,跟簡單工廠的區別在于工廠方法是把選擇具體實現的功能延遲到子類去實現。如果把工廠方法中選擇的實現放到父類直接實現,那就等同于簡單工廠。
- 簡單工廠和能創建對象實例的模式
簡單工廠的本質是選擇實現,所以它可以跟其它任何能夠具體的創建對象實例的模式配合使用,比如:單例模式、原型模式、生成器模式等等。