當(dāng)前位置:
首頁 >
论面向组合子程序设计方法 之 重构
發(fā)布時(shí)間:2025/3/21
33
豆豆
生活随笔
收集整理的這篇文章主要介紹了
论面向组合子程序设计方法 之 重构
小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
迄今,發(fā)現(xiàn)典型的幾種疑問是:?
1。組合子的設(shè)計(jì)要求正交,要求最基本,這是不是太難達(dá)到呢??
2。面對(duì)一些現(xiàn)實(shí)中更復(fù)雜的需求,組合子怎樣scale up呢??
其實(shí),這兩者都指向一個(gè)答案:重構(gòu)。?
要設(shè)計(jì)一個(gè)完全正交,原子到不可再分的組合子,也許不是總是那么容易。但是,我們并不需要一開始就設(shè)計(jì)出來完美的組合子設(shè)計(jì)。?
比如,我前面的logging例子,TimestampLogger負(fù)責(zé)給在一行的開頭打印當(dāng)前時(shí)間。?
然后readonly提出了一個(gè)新的需要:打印調(diào)用這個(gè)logger的那個(gè)java文件的類名字和行號(hào)。?
分析這個(gè)需求,可以發(fā)現(xiàn),兩者都要求在一行的開始打印一些東西。似乎有些共性.?
這個(gè)"在行首打印一些前綴"就成了一個(gè)可以抽象出來的共性.于是重構(gòu):?
Java代碼??interface?Factory{?? ??String?create();;?? }?? class?PrefixLogger?implements?Logger{?? ??private?final?Logger?logger;?? ??private?final?Factory?factory;?? ??private?boolean?freshline?=?true;?? ?? ??private?void?prefix(int?lvl);{?? ????if(freshline);{?? ??????Object?r?=?factory.create();;?? ??????if(r!=null);?? ????????logger.print(lvl,?r);;?? ??????freshline?=?false;?? ????}?? ??}?? ??public?void?print(int?lvl,?String?s);{?? ????prefix(lvl);;?? ????logger.print(lvl,?s);;?? ??}?? ??public?void?println(int?lvl,?String?s);{?? ????prefix(lvl);;?? ????logger.println(lvl,?s);;?? ????freshline?=?true;?? ??}?? ??public?void?printException(int?lvl,?Throwable?e);{?? ????prefix(lvl);;?? ????logger.printException(lvl,?e);;?? ????freshline?=?true;?? ??}?? }??
這里,Factory接口用來抽象往行首打印的前綴。這個(gè)地方之所以不是一個(gè)String,是因?yàn)榭紤]到生成這個(gè)前綴可能是比較昂貴的(比如打印行號(hào),這需要?jiǎng)?chuàng)建一個(gè)臨時(shí)異常對(duì)象)?
另外,真正的Logger接口,會(huì)負(fù)責(zé)打印所有的原始類型和Object類型,例子中我們簡化了這個(gè)接口,為了演示方便。?
然后,先重構(gòu)timestamp:?
Java代碼??class?TimestampFactory?implements?Factory{?? ??private?final?DateFormat?fmt;?? ??public?String?create();{?? ????return?fmt.format(new?Date(););;?? ??}?? }??
這樣,就把timestamp和“行首打印”解耦了出來。?
下面添加TraceBackFactory,負(fù)責(zé)打印當(dāng)前行號(hào)等源代碼相關(guān)信息。?
Java代碼??interface?SourceLocationFormat{?? ??String?format(StackTraceElement?frame);;?? }?? class?TraceBackFactory?implements?Factory{?? ??private?final?SourceLocationFormat?fmt;?? ??public?String?create();{?? ????final?StackTraceElement?frame?=?getNearestUserFrame();;?? ????if(frame!=null);?? ??????return?fmt.format(frame);;?? ????else?return?null;?? ??}?? ??private?StackTraceElement?getNearestUserFrame();{?? ????final?StackTraceElement[]?frames?=?new?Throwable();.getStackTrace();;?? ????foreach(frame:?frames);{?? ??????if(!frame.getClassName();.startsWith("org.mylogging"););{?? ????????//user?frame?? ????????return?frame;?? ??????}?? ????}?? ????return?null;?? ??}?? }??
具體的SourceLocationFormat的實(shí)現(xiàn)我就不寫了。?
注意,到現(xiàn)在為止,這個(gè)重構(gòu)都是經(jīng)典的oo的思路,劃分責(zé)任,按照責(zé)任定義Factory, SourceLocationFormat等等接口,依賴注入等。完全沒有co的影子。?
這也說明,在co里面,我們不是不能采用oo,就象在oo里面,我們也可以圍繞某個(gè)接口按照co來提供一整套的實(shí)現(xiàn)一樣,就象在oo里面,我們也可以在函數(shù)內(nèi)部用po的方法來實(shí)現(xiàn)某個(gè)具體功能一樣。?
下面開始對(duì)factory做一些co的勾當(dāng):?
先是最簡單的:?
Java代碼??class?ReturnFactory?implements?Factory{?? ??private?final?String?s;?? ??public?String?create();{return?s;}?? }??
然后是兩個(gè)factory的串聯(lián),?
Java代碼??class?ConcatFactory?implements?Factory{?? ??private?final?Factory[]?fs;?? ??public?String?create();{?? ????StringBuffer?buf?=?new?StringBuffer();;?? ????foreach(f:?fs);{?? ??????buf.append(f.create(););;?? ????}?? ????return?buf.toString();;?? ??}?? }??
最后,我們把這幾個(gè)零件組合在一起:?
Java代碼??Logger?myprefix(Logger?l);{?? ??Factory?timestamp?=?new?TimestampFactory(some_date_format);;?? ??Factory?traceback?=?new?TraceBackFactory(some_location_format);;?? ??Factory?both?=?new?ConcatFactory(?? ????timestamp,??? ????new?ReturnFactory("?-?");,??? ????traceback,?? ????new?ReturnFactory("?:?");?? ??);;?? ??return?new?PrefixLogger(both,?l);;?? }??
如此,基本上,在行首添加?xùn)|西的需求就差不多了,我們甚至也可以在行尾添加?xùn)|西,還可以重用這些factory的組合子。?
另一點(diǎn)我想說明的是:這種重構(gòu)是相當(dāng)局部的,僅僅影響幾個(gè)組合子,而并不影響整個(gè)組合子框架。?
真正影響組合子框架的,是Logger接口本身的變化。假設(shè),readonly提出了一個(gè)非常好的意見:printException應(yīng)該也接受level,因?yàn)槲覀儜?yīng)該也可以選擇一個(gè)exception的重要程度。?
那么,如果需要做這個(gè)變化,很不幸的是,所有的實(shí)現(xiàn)這個(gè)接口的類都要改變。?
這是不是co的一個(gè)缺陷呢??
我說不是。?
即使是oo,如果你需要改動(dòng)接口,所有的實(shí)現(xiàn)類也都要改動(dòng)。co對(duì)這種情況,其實(shí)還是做了很大的貢獻(xiàn)來避免的:?
只有原子組合子需要實(shí)現(xiàn)這個(gè)接口,而派生的組合子和客戶代碼,根本就不會(huì)被波及到。?
而co相比于oo,同樣面對(duì)相同復(fù)雜的需求,往往原子組合子的數(shù)目遠(yuǎn)遠(yuǎn)小于實(shí)際上要實(shí)現(xiàn)的語義數(shù),大量的需求要求的語義,被通過組合基本粒子來實(shí)現(xiàn)。也因此會(huì)減少直接實(shí)現(xiàn)這個(gè)接口的類的數(shù)目,降低了接口變化的波及范圍。?
那么,這個(gè)Logger接口是怎么來的呢??
它的形成來自兩方面:?
1。需求。通過oo的手段分配責(zé)任,最后分析出來的一個(gè)接口。這個(gè)接口不一定是最簡化的,因?yàn)樗耆峭獠啃枨篁?qū)動(dòng)的。?
2。組合子自身接口簡單性和完備性的需要。有些時(shí)候,我們發(fā)現(xiàn),一個(gè)組合子里面如果沒有某個(gè)方法,或者某個(gè)方法如果沒有某個(gè)參數(shù),一些組合就無法成立。這很可能說明我們的接口不是完備的。(比如那個(gè)print函數(shù))。?
此時(shí),就需要改動(dòng)接口,并且修改原子組合子的實(shí)現(xiàn)。?
因?yàn)檫@個(gè)變化完全是基于組合需求的完備性的,所以是co方法本身帶來的問題,而不能推諉于oo設(shè)計(jì)出來的接口。?
也因?yàn)槿绱?#xff0c;基本組合子個(gè)數(shù)的盡量精簡就是一個(gè)目標(biāo)。能夠通過基本組合子組合而成的,就可以考慮不要直接實(shí)現(xiàn)這個(gè)接口。?
當(dāng)然,這里面仍然有個(gè)權(quán)衡:?
通過組合出來的不如直接實(shí)現(xiàn)的直接,可理解性,甚至可調(diào)試性,性能都會(huì)有所下降。?
而如果選擇直接實(shí)現(xiàn)接口,那么就要做好接口一旦變化,就多出一個(gè)類要改動(dòng)這個(gè)類的心理準(zhǔn)備。?
如何抉擇,沒有一定之規(guī)。?
而因?yàn)?和2的目標(biāo)并不完全一致,很多時(shí)候,我們還需要在1和2之間架一個(gè)adapter以避免兩個(gè)目標(biāo)的沖突。?
比如說,實(shí)際使用中,我可能希望Logger接口提供不要求level的println函數(shù),讓它的缺省值取INFO就好了。?
但是,這對(duì)組合子的實(shí)現(xiàn)來說卻是不利的。這時(shí),我們也許就要把這個(gè)實(shí)現(xiàn)要求的Logger接口和組合子的Logger接口分離開來。(比如把組合子單獨(dú)挪到一個(gè)package中)。?
Logger這個(gè)例子是非常簡單的,它雖然來自于實(shí)際項(xiàng)目,但是項(xiàng)目對(duì)logging的需求并不是太多,所以一些朋友提出了一些基于實(shí)際使用的一些問題,我只能給一個(gè)怎么做的大致輪廓,手邊卻沒有可以運(yùn)行的程序。?
那么,下面一個(gè)例子,我們來看看一個(gè)我經(jīng)過了很多思考比較完善了的ioc容器的設(shè)計(jì)。這個(gè)設(shè)計(jì)來源于yan container。?
先說一下ioc容器的背景知識(shí)。?
所謂ioc容器,是一種用來組裝用ioc模式(或者叫依賴注射)設(shè)計(jì)出來的類的工具。?
一個(gè)用ioc設(shè)計(jì)出來的類,本身對(duì)ioc容器是一無所知的。使用它的時(shí)候,可以根據(jù)實(shí)際情況選擇直接new,直接調(diào)用setter等等比較直接的方法,但是,當(dāng)這樣的組件非常非常多的時(shí)候,用一個(gè)ioc容器來統(tǒng)一管理這些對(duì)象的組裝就可以被考慮。?
拿pico作為例子,對(duì)應(yīng)這樣一個(gè)類:?
Java代碼??class?Boy{?? ??private?final?Girl?girl;?? ??public?Boy(Girl?g);{?? ????this.girl?=?g;?? ??}?? ...?? }??
我們自然可以new Boy(new Girl());?
沒什么不好的。?
但是,如果這種需要組裝的類太多,那么這個(gè)組裝就變成一件累人的活了。?
于是,pico container提供了一個(gè)統(tǒng)一管理組建的方法:?
Java代碼??picocontainer?container?=?new?DefaultContainer();;?? container.registerComponentImplementation(Boy.class);;?? container.registerComponentImplementation(Girl.class);;??
這個(gè)代碼,很可能不是直接寫在程序里面,而是先讀取配置文件或者什么東西,然后動(dòng)態(tài)地調(diào)用這段代碼。?
最后,使用下面的方法來取得對(duì)象:?
Java代碼??Object?obj?=?container.getComponentInstance(Boy.class);;??
注意,這個(gè)container.getXXX,本身是違反ioc的設(shè)計(jì)模式的,它 主動(dòng) 地去尋找某個(gè)組件了。所以,組件本身是忌諱調(diào)用這種api的。如果你在組件級(jí)別的代碼直接依賴ioc容器的api,那么,恭喜你,你終于成功地化神奇為腐朽了。 ?
這段代碼,實(shí)際上應(yīng)該出現(xiàn)在系統(tǒng)的最外圍的組裝程序中。?
當(dāng)然,這是題外話。?
那么,我們來評(píng)估一下pico先,?
1。讓容器自動(dòng)尋找符合某個(gè)類型的組件,叫做auto-wiring。這個(gè)功能方便,但是不能scale up。一旦系統(tǒng)復(fù)雜起來,就會(huì)造成一團(tuán)亂麻,尤其是有兩個(gè)組件都符合這個(gè)要求的時(shí)候,就會(huì)出現(xiàn)二義性。所以,必須提供讓配置者或者程序員顯示指定使用哪個(gè)組件的能力。所謂manual-wire。?
當(dāng)然,pico實(shí)際上是提供了這個(gè)能力的,它允許你使用組件key或者組件類型來顯示地給某個(gè)組件的某個(gè)參數(shù)或者某個(gè)property指定它的那個(gè)girl。?
但是,pico的靈活性就到這里了,它要求你的這個(gè)girl必須被直接登記在這個(gè)容器中,占用一個(gè)寶貴的全局key,即使這個(gè)girl只是專門為這個(gè)body臨時(shí)制造的夏娃。?
在java中,遇到這種情況:?
Java代碼??void?A?createA();{?? ??B?b?=?new?B();;?? ??return?new?A(b,b);;?? }??
我們只需要把b作為一個(gè)局部變量,構(gòu)造完A,b就扔掉了。然而,pico里面這不成,b必須被登記在這個(gè)容器中。這就相當(dāng)于你必須要把b定義成一個(gè)全局變量一樣。?
pico的對(duì)應(yīng)代碼:?
Java代碼??container.registerComponent("b"?new?CachingComponentAdapter(new?ConstructorInjectionComponentAdapter(B.class);););;?? container.registerComponent("a",?new?ConstructorInjectionComponentAdapter(A.class););;??
這里,為了對(duì)應(yīng)上面java代碼中的兩個(gè)參數(shù)公用一個(gè)b的實(shí)例的要求,必須把a(bǔ)登記成一個(gè)singleton。CachingComponentAdapter負(fù)責(zé)singleton化某個(gè)組件,而ConstructorInjectionComponentAdapter就是一個(gè)調(diào)用構(gòu)造函數(shù)的組建匹配器。?
當(dāng)然,這樣做其實(shí)還是有麻煩的,當(dāng)container不把a(bǔ)登記成singleton的時(shí)候(pico缺省都登記成singleton,但是你可以換缺省不用singleton的container。),麻煩就來了。?
大家可以看到,上面的createA()函數(shù)如果調(diào)用兩次,會(huì)創(chuàng)建兩個(gè)A對(duì)象,兩個(gè)B對(duì)象,而用這段pico代碼,調(diào)用兩次getComponentInstance("a"),會(huì)生成兩個(gè)A對(duì)象,但是卻只有一個(gè)B對(duì)象!因?yàn)閎被被迫登記為singleton了。?
2。pico除了支持constructor injection,也支持setter injection甚至factory method injection。(對(duì)最后一點(diǎn)我有點(diǎn)含糊,不過就假設(shè)它支持)。所以,跟spring對(duì)比,除了沒有一個(gè)配置文件,life-cycle不太優(yōu)雅之外,什么都有了。?
但是,這就夠了嗎?如果我們把上面的那個(gè)createA函數(shù)稍微變一下:?
Java代碼??A?createA();{?? ??B?b?=?new?B();;?? ??return?new?A(b,?b.createC(x_component););;?? }??
現(xiàn)在,我們要在b組件上面調(diào)用createC()來生成一個(gè)C對(duì)象。完了,我們要的既不是構(gòu)造函數(shù),也不是工廠方法,而是在某個(gè)臨時(shí)組件的基礎(chǔ)上調(diào)用一個(gè)函數(shù)。?
缺省提供的幾個(gè)ComponentAdapter這時(shí)就不夠用了,我們被告知要自己實(shí)現(xiàn)ComponentAdapter。?
實(shí)際上,pico對(duì)很多靈活性的要求的回答都是:自己實(shí)現(xiàn)ComponentAdapter。?
這是可行的。沒什么是ComponentAdapter干不了的,如果不計(jì)工作量的話。?
一個(gè)麻煩是:我們要直接調(diào)用pico的api來自己解析依賴了。我們要自己知道是調(diào)用container.getComponentInstance("x_component")還是container.getComponentInstance(X.class)。?
第二個(gè)麻煩是:降低了代碼重用。自己實(shí)現(xiàn)ComponentAdapter就得自己老老實(shí)實(shí)地寫,如果自己的component adapter也要?jiǎng)討B(tài)設(shè)置java bean setter的話,甭想直接用SetterInjectionComponentAdapter,好好看java bean的api吧。?
其實(shí),我們可以看出,pico的各種ComponentAdapter正是正宗的decorator pattern。什么CachingComponentAdapter,什么SynchronizedComponentAdapter,都是decorator。?
但是,這也就是decorator而已了。因?yàn)闆]有圍繞組合子的思路開展設(shè)計(jì),這些decorator顯得非常隨意,沒有什么章法,沒辦法支撐起整個(gè)的ComponentAdapter的架構(gòu)。?
下一章,我們會(huì)介紹yan container對(duì)上面提出的問題以及很多其他問題的解決方法。?
yan container的口號(hào)是:只要你直接組裝能夠做到的,容器就能做到。?
不管你是不是用構(gòu)造函數(shù),靜態(tài)方法,java bean,構(gòu)造函數(shù)然后再調(diào)用某個(gè)方法,等等等等。?
而且yan container的目標(biāo)是,你幾乎不用自己實(shí)現(xiàn)component adapter,所有的需求,都通過組合各種已經(jīng)存在的組合子來完成。?
對(duì)我們前面那個(gè)很不厚道地用來刁難pico的例子,yan的解決方法是:?
Java代碼??b_component?=?Components.ctor(B.class);.singleton();;?? a_component?=?Components.ctor(A.class);?? ??.withArgument(0,?b_component);?? ??.withArgument(1,?b_component.method("createC"););;??
b_component不需要登記在容器中,它作為局部component存在。?
是不是非常declarative呢??
下一節(jié),你會(huì)發(fā)現(xiàn),用面向組合子的方法,ioc容器這種東西真的不難。我們不需要仔細(xì)分析各種需求,精心分配責(zé)任。讓我們?cè)俅误w驗(yàn)一下吊兒郎當(dāng)不知不覺間就天下大治的感覺吧。?
1。組合子的設(shè)計(jì)要求正交,要求最基本,這是不是太難達(dá)到呢??
2。面對(duì)一些現(xiàn)實(shí)中更復(fù)雜的需求,組合子怎樣scale up呢??
其實(shí),這兩者都指向一個(gè)答案:重構(gòu)。?
要設(shè)計(jì)一個(gè)完全正交,原子到不可再分的組合子,也許不是總是那么容易。但是,我們并不需要一開始就設(shè)計(jì)出來完美的組合子設(shè)計(jì)。?
比如,我前面的logging例子,TimestampLogger負(fù)責(zé)給在一行的開頭打印當(dāng)前時(shí)間。?
然后readonly提出了一個(gè)新的需要:打印調(diào)用這個(gè)logger的那個(gè)java文件的類名字和行號(hào)。?
分析這個(gè)需求,可以發(fā)現(xiàn),兩者都要求在一行的開始打印一些東西。似乎有些共性.?
這個(gè)"在行首打印一些前綴"就成了一個(gè)可以抽象出來的共性.于是重構(gòu):?
Java代碼??
這里,Factory接口用來抽象往行首打印的前綴。這個(gè)地方之所以不是一個(gè)String,是因?yàn)榭紤]到生成這個(gè)前綴可能是比較昂貴的(比如打印行號(hào),這需要?jiǎng)?chuàng)建一個(gè)臨時(shí)異常對(duì)象)?
另外,真正的Logger接口,會(huì)負(fù)責(zé)打印所有的原始類型和Object類型,例子中我們簡化了這個(gè)接口,為了演示方便。?
然后,先重構(gòu)timestamp:?
Java代碼??
這樣,就把timestamp和“行首打印”解耦了出來。?
下面添加TraceBackFactory,負(fù)責(zé)打印當(dāng)前行號(hào)等源代碼相關(guān)信息。?
Java代碼??
具體的SourceLocationFormat的實(shí)現(xiàn)我就不寫了。?
注意,到現(xiàn)在為止,這個(gè)重構(gòu)都是經(jīng)典的oo的思路,劃分責(zé)任,按照責(zé)任定義Factory, SourceLocationFormat等等接口,依賴注入等。完全沒有co的影子。?
這也說明,在co里面,我們不是不能采用oo,就象在oo里面,我們也可以圍繞某個(gè)接口按照co來提供一整套的實(shí)現(xiàn)一樣,就象在oo里面,我們也可以在函數(shù)內(nèi)部用po的方法來實(shí)現(xiàn)某個(gè)具體功能一樣。?
下面開始對(duì)factory做一些co的勾當(dāng):?
先是最簡單的:?
Java代碼??
然后是兩個(gè)factory的串聯(lián),?
Java代碼??
最后,我們把這幾個(gè)零件組合在一起:?
Java代碼??
如此,基本上,在行首添加?xùn)|西的需求就差不多了,我們甚至也可以在行尾添加?xùn)|西,還可以重用這些factory的組合子。?
另一點(diǎn)我想說明的是:這種重構(gòu)是相當(dāng)局部的,僅僅影響幾個(gè)組合子,而并不影響整個(gè)組合子框架。?
真正影響組合子框架的,是Logger接口本身的變化。假設(shè),readonly提出了一個(gè)非常好的意見:printException應(yīng)該也接受level,因?yàn)槲覀儜?yīng)該也可以選擇一個(gè)exception的重要程度。?
那么,如果需要做這個(gè)變化,很不幸的是,所有的實(shí)現(xiàn)這個(gè)接口的類都要改變。?
這是不是co的一個(gè)缺陷呢??
我說不是。?
即使是oo,如果你需要改動(dòng)接口,所有的實(shí)現(xiàn)類也都要改動(dòng)。co對(duì)這種情況,其實(shí)還是做了很大的貢獻(xiàn)來避免的:?
只有原子組合子需要實(shí)現(xiàn)這個(gè)接口,而派生的組合子和客戶代碼,根本就不會(huì)被波及到。?
而co相比于oo,同樣面對(duì)相同復(fù)雜的需求,往往原子組合子的數(shù)目遠(yuǎn)遠(yuǎn)小于實(shí)際上要實(shí)現(xiàn)的語義數(shù),大量的需求要求的語義,被通過組合基本粒子來實(shí)現(xiàn)。也因此會(huì)減少直接實(shí)現(xiàn)這個(gè)接口的類的數(shù)目,降低了接口變化的波及范圍。?
那么,這個(gè)Logger接口是怎么來的呢??
它的形成來自兩方面:?
1。需求。通過oo的手段分配責(zé)任,最后分析出來的一個(gè)接口。這個(gè)接口不一定是最簡化的,因?yàn)樗耆峭獠啃枨篁?qū)動(dòng)的。?
2。組合子自身接口簡單性和完備性的需要。有些時(shí)候,我們發(fā)現(xiàn),一個(gè)組合子里面如果沒有某個(gè)方法,或者某個(gè)方法如果沒有某個(gè)參數(shù),一些組合就無法成立。這很可能說明我們的接口不是完備的。(比如那個(gè)print函數(shù))。?
此時(shí),就需要改動(dòng)接口,并且修改原子組合子的實(shí)現(xiàn)。?
因?yàn)檫@個(gè)變化完全是基于組合需求的完備性的,所以是co方法本身帶來的問題,而不能推諉于oo設(shè)計(jì)出來的接口。?
也因?yàn)槿绱?#xff0c;基本組合子個(gè)數(shù)的盡量精簡就是一個(gè)目標(biāo)。能夠通過基本組合子組合而成的,就可以考慮不要直接實(shí)現(xiàn)這個(gè)接口。?
當(dāng)然,這里面仍然有個(gè)權(quán)衡:?
通過組合出來的不如直接實(shí)現(xiàn)的直接,可理解性,甚至可調(diào)試性,性能都會(huì)有所下降。?
而如果選擇直接實(shí)現(xiàn)接口,那么就要做好接口一旦變化,就多出一個(gè)類要改動(dòng)這個(gè)類的心理準(zhǔn)備。?
如何抉擇,沒有一定之規(guī)。?
而因?yàn)?和2的目標(biāo)并不完全一致,很多時(shí)候,我們還需要在1和2之間架一個(gè)adapter以避免兩個(gè)目標(biāo)的沖突。?
比如說,實(shí)際使用中,我可能希望Logger接口提供不要求level的println函數(shù),讓它的缺省值取INFO就好了。?
但是,這對(duì)組合子的實(shí)現(xiàn)來說卻是不利的。這時(shí),我們也許就要把這個(gè)實(shí)現(xiàn)要求的Logger接口和組合子的Logger接口分離開來。(比如把組合子單獨(dú)挪到一個(gè)package中)。?
Logger這個(gè)例子是非常簡單的,它雖然來自于實(shí)際項(xiàng)目,但是項(xiàng)目對(duì)logging的需求并不是太多,所以一些朋友提出了一些基于實(shí)際使用的一些問題,我只能給一個(gè)怎么做的大致輪廓,手邊卻沒有可以運(yùn)行的程序。?
那么,下面一個(gè)例子,我們來看看一個(gè)我經(jīng)過了很多思考比較完善了的ioc容器的設(shè)計(jì)。這個(gè)設(shè)計(jì)來源于yan container。?
先說一下ioc容器的背景知識(shí)。?
所謂ioc容器,是一種用來組裝用ioc模式(或者叫依賴注射)設(shè)計(jì)出來的類的工具。?
一個(gè)用ioc設(shè)計(jì)出來的類,本身對(duì)ioc容器是一無所知的。使用它的時(shí)候,可以根據(jù)實(shí)際情況選擇直接new,直接調(diào)用setter等等比較直接的方法,但是,當(dāng)這樣的組件非常非常多的時(shí)候,用一個(gè)ioc容器來統(tǒng)一管理這些對(duì)象的組裝就可以被考慮。?
拿pico作為例子,對(duì)應(yīng)這樣一個(gè)類:?
Java代碼??
我們自然可以new Boy(new Girl());?
沒什么不好的。?
但是,如果這種需要組裝的類太多,那么這個(gè)組裝就變成一件累人的活了。?
于是,pico container提供了一個(gè)統(tǒng)一管理組建的方法:?
Java代碼??
這個(gè)代碼,很可能不是直接寫在程序里面,而是先讀取配置文件或者什么東西,然后動(dòng)態(tài)地調(diào)用這段代碼。?
最后,使用下面的方法來取得對(duì)象:?
Java代碼??
注意,這個(gè)container.getXXX,本身是違反ioc的設(shè)計(jì)模式的,它 主動(dòng) 地去尋找某個(gè)組件了。所以,組件本身是忌諱調(diào)用這種api的。如果你在組件級(jí)別的代碼直接依賴ioc容器的api,那么,恭喜你,你終于成功地化神奇為腐朽了。 ?
這段代碼,實(shí)際上應(yīng)該出現(xiàn)在系統(tǒng)的最外圍的組裝程序中。?
當(dāng)然,這是題外話。?
那么,我們來評(píng)估一下pico先,?
1。讓容器自動(dòng)尋找符合某個(gè)類型的組件,叫做auto-wiring。這個(gè)功能方便,但是不能scale up。一旦系統(tǒng)復(fù)雜起來,就會(huì)造成一團(tuán)亂麻,尤其是有兩個(gè)組件都符合這個(gè)要求的時(shí)候,就會(huì)出現(xiàn)二義性。所以,必須提供讓配置者或者程序員顯示指定使用哪個(gè)組件的能力。所謂manual-wire。?
當(dāng)然,pico實(shí)際上是提供了這個(gè)能力的,它允許你使用組件key或者組件類型來顯示地給某個(gè)組件的某個(gè)參數(shù)或者某個(gè)property指定它的那個(gè)girl。?
但是,pico的靈活性就到這里了,它要求你的這個(gè)girl必須被直接登記在這個(gè)容器中,占用一個(gè)寶貴的全局key,即使這個(gè)girl只是專門為這個(gè)body臨時(shí)制造的夏娃。?
在java中,遇到這種情況:?
Java代碼??
我們只需要把b作為一個(gè)局部變量,構(gòu)造完A,b就扔掉了。然而,pico里面這不成,b必須被登記在這個(gè)容器中。這就相當(dāng)于你必須要把b定義成一個(gè)全局變量一樣。?
pico的對(duì)應(yīng)代碼:?
Java代碼??
這里,為了對(duì)應(yīng)上面java代碼中的兩個(gè)參數(shù)公用一個(gè)b的實(shí)例的要求,必須把a(bǔ)登記成一個(gè)singleton。CachingComponentAdapter負(fù)責(zé)singleton化某個(gè)組件,而ConstructorInjectionComponentAdapter就是一個(gè)調(diào)用構(gòu)造函數(shù)的組建匹配器。?
當(dāng)然,這樣做其實(shí)還是有麻煩的,當(dāng)container不把a(bǔ)登記成singleton的時(shí)候(pico缺省都登記成singleton,但是你可以換缺省不用singleton的container。),麻煩就來了。?
大家可以看到,上面的createA()函數(shù)如果調(diào)用兩次,會(huì)創(chuàng)建兩個(gè)A對(duì)象,兩個(gè)B對(duì)象,而用這段pico代碼,調(diào)用兩次getComponentInstance("a"),會(huì)生成兩個(gè)A對(duì)象,但是卻只有一個(gè)B對(duì)象!因?yàn)閎被被迫登記為singleton了。?
2。pico除了支持constructor injection,也支持setter injection甚至factory method injection。(對(duì)最后一點(diǎn)我有點(diǎn)含糊,不過就假設(shè)它支持)。所以,跟spring對(duì)比,除了沒有一個(gè)配置文件,life-cycle不太優(yōu)雅之外,什么都有了。?
但是,這就夠了嗎?如果我們把上面的那個(gè)createA函數(shù)稍微變一下:?
Java代碼??
現(xiàn)在,我們要在b組件上面調(diào)用createC()來生成一個(gè)C對(duì)象。完了,我們要的既不是構(gòu)造函數(shù),也不是工廠方法,而是在某個(gè)臨時(shí)組件的基礎(chǔ)上調(diào)用一個(gè)函數(shù)。?
缺省提供的幾個(gè)ComponentAdapter這時(shí)就不夠用了,我們被告知要自己實(shí)現(xiàn)ComponentAdapter。?
實(shí)際上,pico對(duì)很多靈活性的要求的回答都是:自己實(shí)現(xiàn)ComponentAdapter。?
這是可行的。沒什么是ComponentAdapter干不了的,如果不計(jì)工作量的話。?
一個(gè)麻煩是:我們要直接調(diào)用pico的api來自己解析依賴了。我們要自己知道是調(diào)用container.getComponentInstance("x_component")還是container.getComponentInstance(X.class)。?
第二個(gè)麻煩是:降低了代碼重用。自己實(shí)現(xiàn)ComponentAdapter就得自己老老實(shí)實(shí)地寫,如果自己的component adapter也要?jiǎng)討B(tài)設(shè)置java bean setter的話,甭想直接用SetterInjectionComponentAdapter,好好看java bean的api吧。?
其實(shí),我們可以看出,pico的各種ComponentAdapter正是正宗的decorator pattern。什么CachingComponentAdapter,什么SynchronizedComponentAdapter,都是decorator。?
但是,這也就是decorator而已了。因?yàn)闆]有圍繞組合子的思路開展設(shè)計(jì),這些decorator顯得非常隨意,沒有什么章法,沒辦法支撐起整個(gè)的ComponentAdapter的架構(gòu)。?
下一章,我們會(huì)介紹yan container對(duì)上面提出的問題以及很多其他問題的解決方法。?
yan container的口號(hào)是:只要你直接組裝能夠做到的,容器就能做到。?
不管你是不是用構(gòu)造函數(shù),靜態(tài)方法,java bean,構(gòu)造函數(shù)然后再調(diào)用某個(gè)方法,等等等等。?
而且yan container的目標(biāo)是,你幾乎不用自己實(shí)現(xiàn)component adapter,所有的需求,都通過組合各種已經(jīng)存在的組合子來完成。?
對(duì)我們前面那個(gè)很不厚道地用來刁難pico的例子,yan的解決方法是:?
Java代碼??
b_component不需要登記在容器中,它作為局部component存在。?
是不是非常declarative呢??
下一節(jié),你會(huì)發(fā)現(xiàn),用面向組合子的方法,ioc容器這種東西真的不難。我們不需要仔細(xì)分析各種需求,精心分配責(zé)任。讓我們?cè)俅误w驗(yàn)一下吊兒郎當(dāng)不知不覺間就天下大治的感覺吧。?
待續(xù)。
from:http://ajoo.iteye.com/blog/23314
總結(jié)
以上是生活随笔為你收集整理的论面向组合子程序设计方法 之 重构的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 论面向组合子程序设计方法 之 oracl
- 下一篇: 论面向组合子程序设计方法 之 南无阿弥陀