“圍城”式困境中的依賴注入模式及Spring(3)
來源:互聯網
上載者:User
“圍城”式困境中的依賴注入模式及Spring(3)
依賴注入模式和原廠模式由於依賴注入模式的功能之一是初始化一個類,所以可以通過依賴注入模式來擷取對象。我們知道,這個功能一向是原廠模式的領地。於是,有很多人就公然宣稱:依賴注入模式能夠代替原廠模式。他們的理由之一是,既然有了IOC容器,就可以通過它來初始化對象。何必再用原廠模式呢?因為如果用原廠模式的話,我們得多增加一個工廠類。同時,IOC模式的狂熱追隨者宣稱:原廠模式只能獲得特定介面的類的執行個體;而IOC模式則可以擷取任意類的對象。由此可以看出,IOC模式比原廠模式更加靈活。因此IOC模式可以取代原廠模式。可事實真的是這樣的嗎?假如我們有五種動物:Checken,Sheep,Cat,Dog,Pig;它們都繼承Animal介面,如下:public interface Animal{ public void eat(); public String say();}其中,Checken類的實現如下:public class Checken implements Animal{ public void eat() { System.out.println(“The checken cats insert……”);}public String say(){ System.out.println(“The checken’s saying sounds like crow……”); return “crow”;}}其他,關於Sheep類,Cat類,Dog類和Pig類的實現在這裡不再給出,它們都實現了Animal介面。如果我們用IOC容器來擷取各個類的執行個體,這裡以Spring的IOC容器為例,一般要經過如下的兩步的配置:<bean id="checkenTarget" class="Checken"> <constructor-arg> <ref bean="AppCtx" /> </constructor-arg></bean> <bean id="checken" parent="parentService"> <property name="target"> <ref bean=" checkenTarget " /> </property></bean>以上是關於Checken類的配置,要完全實現上述的五個類的IOC容器的配置,還需要和上面雷同的四個配置。如下:<bean id="sheepTarget" class="Sheep"> <constructor-arg> <ref bean="AppCtx" /> </constructor-arg></bean> <bean id="sheep" parent="parentService"> <property name="target"> <ref bean="sheepTarget " /> </property></bean> <bean id="catTarget" class="Cat"> <constructor-arg> <ref bean="AppCtx" /> </constructor-arg></bean> <bean id="cat" parent="parentService"> <property name="target"> <ref bean=" catTarget " /> </property></bean> <bean id="dogTarget" class="Dog"> <constructor-arg> <ref bean="AppCtx" /> </constructor-arg></bean> <bean id="dog" parent="parentService"> <property name="target"> <ref bean="dogTarget " /> </property></bean> <bean id="pigTarget" class="Pig"> <constructor-arg> <ref bean="AppCtx" /> </constructor-arg></bean> <bean id="pig" parent="parentService"> <property name="target"> <ref bean="pigTarget " /> </property></bean> 這樣,這五個類的IOC容器的配置才算完成。上面的五個類如果用簡單原廠模式來實現的話,代碼如下:public class Factory{ public static Animal getInstance(String type) { if(“checken”.equals(type)) { return new Checken();}else if(“sheep”.equals(type)){ return new Sheep();}else if(“cat”.equals(type)){ return new Cat();}else if(“dog”.equals(type)){ return new Dog();}else{ return new Pig();}}}把兩者加以比較,可以看出,簡單原廠模式的工廠類的編寫,的確要比IOC容器的繁瑣的配置要來得簡單得多。如此,我們可以看到使用IOC容器的第一個不便:繁瑣複雜的組態管理,這樣的配置既枯燥乏味,又容易出錯。經常需要我們拷貝來拷貝去,但仍然保證不了不出錯。我們再來看一看我們是如何在IOC容器裡取得執行個體的:String[] contextFiles = null; if(contextFiles == null) { contextFiles = new String[]{"applicationContext.xml", "dataAccessContext-local.xml"}; } appCtx = new ClassPathXmlApplicationContext(contextFiles);Animal checken = (Animal)appCtx.getBean("checken"); 而原廠模式又是如何取得各個類的執行個體的呢?Animal checken = Factory.getInstance(“checken”);我們可以看到,使用原廠模式取得類的執行個體非常簡單,而使用IOC容器則由於容器的普適性,需要對取得的對象進行強制類型轉化,以獲得我們所需要的Animal類型。這是IOC容器的使用不如原廠模式方便的第二個方面。第三個方面:所有的所謂的“外掛程式式”編程所帶來的同一個問題,IOC容器同樣不能避免,當在工廠類我們誤將Checken類的類名寫錯,如下:if(“checken”.equals(type)){ return new Check();}那麼Factory類編譯的時候就會出錯,很顯然,這有利於我們修改錯誤。但是如果我們在IOC容器的設定檔中寫錯,如下:<bean id="checkenTarget" class="Check"> <constructor-arg> <ref bean="AppCtx" /> </constructor-arg></bean> <bean id="checken" parent="parentService"> <property name="target"> <ref bean=" checkenTarget " /> </property></bean>那麼IDE在編譯的時候是檢查不出錯誤來的,只能是在啟動並執行時候拋出ClassNotFoundException這樣的違例來告訴我們出了錯。很顯然,這種查錯方式不如編譯的時候方便。上面的討論可以看出,依賴注入模式,或者說IOC容器在實現對依賴注入對象的普適性的時候,給我們引入了組態管理的麻煩。同時,“外掛程式式”的編程可以使我們很輕鬆的把一個系統的兩個部分在運行期內耦合到一起,但也給我們帶來了一些本該在編譯器就調試出來的錯誤直到運行期內才能被發現,而且查錯也沒有編譯期那麼容易。而原廠模式則可以避免這些麻煩,所以原廠模式和依賴注入模式兩者都有他們不同的適用範圍。但是它們之間的區別又是非常微妙的,需要我們來細細的品位。首先,如果產品是設計者能夠把握的有限幾種,如上面的Animal介面的幾個實現,系統設計完成之後,可預期的產品擴充的可能性比較小。也就是除了上面的五個類以外,不大可能擴充出新的類來。這時候用原廠模式比較好,省去使用IOC容器所帶來的配置麻煩。如果想實現“外掛程式式”編程,即把前後台分給不同的開發人員或開發小組去同時實現。這時候,IOC容器是一個不二的選擇。原廠模式是無能為力的。除此之外,依賴注入模式和原廠模式還有一個重要的區別:就是原廠模式只關心生產產品,而對依賴的注入不太關心,或者說原廠模式對依賴注入的處理能力稍顯不足。而依賴注入模式則強調了依賴的注入。兩者的區別是原廠模式中變化的是產品,即有多種的產品;而依賴注入模式中變化的是依賴,即有千變萬化的依賴。就上面的例子,假如你只想聽到各種動物的叫聲,那麼你只關心如何取得產品,則使用原廠模式比較方便,代碼如下:Animal animal = Factory.getInstance(type);animal.say();但是,如果你想找出像“bark”這樣的叫聲的動物來:public class AnimalManager{ private Animal animal; public void setAnimal(Animal animal) { this.animal = animal;}public void findBarkAnimal(){ if(this.animal.say.equals(“bark”)) { System.out.println(animal.Class.getName()+” is a bark animal!”);}}}你的AnimalManager類可能給你無法控制的系統使用,那些系統可能有各種各樣你所無法預知的動物。這時候使用依賴注入模式比較好。話又說回來,前面我們所說的“外掛程式式”編程,前後台分開開發的模式,其實就是一種依賴注入的變形。它是將後台服務作為一種依賴,注入到前台的系統中去。所以,我們在使用IOC容器的時候,總是在往容器中插入依賴,而不是單純的擷取對象。而且是依賴IOC容器將依賴注入到我們所要擷取的對象中去,這才是我們使用IOC容器的最大的目的。總之,使用原廠模式的時候,變化的是我們所要擷取的對象;而使用IOC容器的時候,變化的是我們需要往擷取對象中注入的依賴,而我們所要擷取的對象可能是變化的,也可能不變。依賴注入模式還有一個常用的地方:還是以上面的animal為例,如果我們在一個版本中只有一個Cat類,其配置如下。<bean id="animalTarget" class="Cat"> <constructor-arg> <ref bean="AppCtx" /> </constructor-arg></bean> <bean id="animal" parent="parentService"> <property name="target"> <ref bean=" animalTarget " /> </property></bean> 我們對這個Animal的實現的使用如下://取得animal的對象Animal animla = appCtx.getBean("animal");;//對該對象進行操作…… 現在在下一個版本中,確定具體的Animal實現是要變化的,即肯定不是Cat對象了。假如是Dog對象。但是對Animal的實現的使用代碼不變。那麼我們只需增加Dog對象,然後修改上面的設定檔,將<bean id="animalTarget" class="Cat">中的Cat改成Dog就行。如果我們對Animal的使用的代碼是這樣的:Animal animla = new Cat();……那麼當我們在下一個版本需要將Cat換成Dog的話,就不得不上面的對Animal的使用代碼了。當然,你可能會說,我使用原廠模式也能做到啊。不錯,當你的系統只有一個Animal介面的時候,你使用原廠模式也可以。但如果你的系統有很多的介面,它們的實現遇到的情況也和Animal介面遇到的情況一模一樣,那麼你使用原廠模式還好嗎?你將不得不為每一個介面做一個工廠。 囉裡囉唆地說了好多,現在在總結一下吧,以此作為這一節文字的蛇足吧!第一、 使用原廠模式的情況,工廠類的設計者可以確定有多少個產品,而且產品的擴充性不大,就是說不太可能有新的產品會添加到已經設計好的系統中。第二、 “外掛程式式”編程,即前後台分開並行編碼的系統中,應該使用依賴注入模式。第三、 需要往重用的代碼注入依賴,而且依賴由於重用代碼的使用者不同而不同,即重用代碼的設計者無法控制依賴的時候,應該使用依賴注入模式。第四、 一個系統裡有很多的介面,而系統的不同的版本對同一個介面的實現的使用是相同的,而不同的版本的實現是不同的。這時候也應該使用依賴注入模式。