OO隨筆(關(guān)于connection pool系列的補充,兼答bonmot)
說起OO, 每個人都有每個人自己的見解。粗淺者如“obj.method的語法就是OO”;高深的則必侃“design pattern”.
今天我也來說說我的一孔之見。
什么是OO?
就是面向接口編程。無論你是用vtable, 或gp的function object, 或就是C的函數(shù)指針,正交分解也好,各種pattern也罷,都是面向接口編程思想的一種實現(xiàn)。
為什么要面向接口編程?
為了解耦。
什么是解耦?
就是把程序中互相不相關(guān)或有限相關(guān)的模塊分割開來。就象收拾屋子,你希望把不同的東西放到不同的地方。把醬油和醋倒進不同的瓶子里去。
這里,對完全不相關(guān)的功能,可以簡單地分開實現(xiàn)。
但事實上,很多情況下,不同模塊之間是有互相之間的關(guān)系的。這時,就需要接口。用接口準確定義模塊之間的關(guān)系。解耦前,兩個模塊之間共享所有信息(這個信息包括數(shù)據(jù),也包括各自的實現(xiàn)細節(jié))。解耦后,需要共享的信息被準確地定義在接口中。同時,信息的流向也被確定。
解耦的好處是什么呢?
首先,程序變得清晰了。
其次,不該暴露的實現(xiàn)細節(jié)被隱藏了。代碼的修改變?nèi)菀琢恕?BR>再次,結(jié)構(gòu)靈活了,通過靜態(tài)多態(tài)(function object)或動態(tài)多態(tài)(vtable), 一個模塊可以和任意實現(xiàn)接口的模塊協(xié)作。原來類A只能與類B協(xié)作,解耦后可以和所有實現(xiàn)接口IB的類如B1, B2, ... 協(xié)作了。擴展性大大增強。自然而然就代碼重用了。
編譯依賴也沒有了。你可以專心寫和編譯一個模塊,不用等待其它模塊的完成。
調(diào)試容易了。只要模塊對一個接口調(diào)試成功,其它的接口也沒有問題。于是,甚至可以用一個simple naive的實現(xiàn)該接口的dummy類來調(diào)試。(這點,使用template的gp不適用)
那么解耦的壞處是什么呢?
接口的定義變得很關(guān)鍵。解耦就是隱藏一些信息,定義一些需要共享的信息。如果接口定義的不好,隱藏了不該隱藏的信息,那么對某些需要這些信息的復(fù)雜情況來說,這個解耦就失敗了。
而如果沒有隱藏一些應(yīng)該隱藏的信息,那么不該有的耦合仍然存在。
那么怎樣解耦,又怎樣定義接口呢?
這是一個純粹業(yè)務(wù)邏輯的思考過程。這里,對編程語言的知識變得無關(guān)緊要。事實上,只要精確掌握需求,嚴密地分析需求和模塊內(nèi)部子模塊之間的需求,任何一個會邏輯思考的人都可勝任這個工作。就象歌星鄭智化一樣,雖然不識譜,但一樣寫歌,只不過最后要懂譜的人把歌紀錄下來。
解耦的原則很簡單:精確定義需求,仔細分析需求。不要隱藏任何“需求”也許會需要的信息。不要放過任何“需求”明顯不需要的信息。
而對需求不清楚的情況,寧可錯放一千,不能錯殺一個。總而言之,決不能隱藏可能需要的信息。
不考慮重用,重用是解耦后的自然結(jié)果。不能倒因為果!
至于對這些原則的具體的運用在前面幾篇的connection pool的文章里已經(jīng)有所體現(xiàn)了。
下面,我先針對bonmot對我的connection pool的例子的疑問進行回答。最后,再對bonmot的一個問題給出我的解決的思路。
無關(guān)緊要的問題:
1.ConnectionFactoryImpl也可以繼承方式實現(xiàn)ConnectionFactory
其實,我最初的實現(xiàn),的確是ConnectoinFactoryImpl implements ConnectionFactory的,
但后來,當我overload了instance()函數(shù)之后,我發(fā)現(xiàn),這兩個函數(shù)返回的ConnectionFactory的實現(xiàn)類的代碼是不同的。于是,匿名類就誕生了。
這里,有一點值得吹噓的是,對構(gòu)造函數(shù)的隱藏,使得使用ConnectionFactoryImpl的客戶代碼對我的改動完全不敏感。這也就是我為什么一直鼓吹要隱藏構(gòu)造函數(shù)的原因。
以下是這個類的實現(xiàn):
public class ConnectionFactoryImpl
{
private ConnectionFactoryImpl(){}
static public ConnectionFactory instance(final String driver, final String url,
final String user, final String pwd)
throws SQLException, ClassNotFoundException{
final Class driverClass = Class.forName(driver);
return new ConnectionFactory(){
private final Class keeper = driverClass;
public final Connection createConnection()
throws SQLException{
return DriverManager.getConnection(url,user,pwd);
}
};
}
static public ConnectionFactory instance(final String driver, final String url)
throws SQLException, ClassNotFoundException{
final Class driverClass = Class.forName(driver);
return new ConnectionFactory(){
private final Class keeper = driverClass;
public final Connection createConnection()
throws SQLException{
return DriverManager.getConnection(url);
}
};
}
}
2.ConnectionFactoryImpl中
private final Class keeper = driverClass;//似乎多余
是啊,很多代碼里都是禿禿的Class.forName(classname)。也工作的很好。不過,記得在哪篇文章里看到過,在新的java language specification里,動態(tài)加載的類有可能被垃圾回收。如果是這樣,那不麻煩啦?我好容易Class.forName()加載了driver類,好嘛!哪天jvm一高興給我回收啦!所以咱還是以防萬一的好!
功能的問題
1.ConnectionPooling是實現(xiàn)pooling的算法,其最基本的就是getConnection(),releaseConnection(Conn)
為什么不直接在ConnectionPool定義releaseConnection()方法,而要多一個interface ConnectionHome
首先,我的ConnectionPool接口是直接給用戶使用的。我在該文的第一章就提出,向用戶暴露releaseConnection(Connection)是不好的。你怎樣保證用戶沒有向oracle連接池中返回sql server連接?怎樣保證用戶不會把同一個連接向連接池返回兩次?已經(jīng)有Connection.close(), 用戶為什么要調(diào)用releaseConnection?
ConnectionHome接口是PooledConnection類定義的。PooledConnection作為一個封裝在物理Connection外的與pool協(xié)同工作的類,它需要知道怎樣返還一個物理Connection. ConnectionHome接口只定義了一個方法:void releaseConnection(Connection), 就是描述這一需求的產(chǎn)物。
2.事物總是對等的,F(xiàn)actory用于實現(xiàn)物理連接,同樣應(yīng)該負責關(guān)閉物理連接,而不應(yīng)該讓pooling算法關(guān)閉物理連接。另外,獲取與關(guān)閉connection應(yīng)該在一個接口中實現(xiàn),如果分成2個接口,就不能保證連接的實現(xiàn)一定對應(yīng)于關(guān)閉的實現(xiàn)。
即Factory是物理層,pool是cache層,client是應(yīng)用層。
首先,ConnectionPooling作為一個描述pooling算法的接口,它需要代表所有可能的pooling算法,所以,我們不能排除在某種pooling算法中,它會以一定的邏輯關(guān)閉物理數(shù)據(jù)庫連接。因此,pooling算法一定要可以在任何時候關(guān)閉這個連接。
至于是調(diào)用Connection.close(), 還是放一個closeConnection方法在ConnectionFactory中,讓我們先看看一些其它的factory的實現(xiàn)。
在COM中,IFactory的接口負責生產(chǎn)對象。但釋放對象是由IUnknown::Release()負責的。
在Java中,很多Factory接口負責生產(chǎn)對象,但垃圾收集負責回收對象。
為什么這些factory的機制不要求生產(chǎn)者來銷毀對象呢?
其原因在于類型安全!
舉個例子:
class Factory{
public Object getObject(){
if(...)return new ClassA();
else return new ClassB();
}
public void release(Object obj){
if(obj 是ClassA){
((ClassA)obj).closeA();
}
else{
((ClassB)obj).closeB();
}
/*丑啊!*/
}
}
在這樣一個工廠里,getObject方法知道生產(chǎn)的對象的真正類型。但在返回之后,該對象的真正類型就被丟失了。
這樣,如果你再把對象送還給工廠,說:“嘿!這是從你們廠出的,現(xiàn)在我不用了,還給你。”對工廠來說,它需要:
1, 確認這個對象真是出自本廠。(這可不那么容易)
2, 確認這個對象是怎么造出來的。以便找出相應(yīng)的銷毀機制(也不容易)
我們?yōu)槭裁床话裷eleaseConnection對用戶公開?也是因為考慮到用戶可能會錯誤返還非本廠生產(chǎn)的東西。
其實,當對象出廠之后,只有對象自己才知道怎樣銷毀自己。其它任何對象,包括生產(chǎn)者,都無能為力。
4.可靠性不夠。表現(xiàn)在:
a.pool的可靠性應(yīng)該與server的可靠性無關(guān),即database server或socket server可能由于某些原因重新啟動,但pool不應(yīng)該也要重新啟動(比如一個pool存有不同server的connection),否則就跑出錯誤。所以,pool因該檢查物理connection的連接狀態(tài)
怎么說呢?這屬于ConnectionPool這個接口的語義。我們是否想讓我們的pool即使數(shù)據(jù)庫server崩潰了也能工作呢?
首先,這樣做是否有意義呢?如果數(shù)據(jù)庫server崩潰了,我們的Connection pool怎么補救呢?
其次,就算這樣是有意義的,它也是ConnectionPooling的邏輯。完全可以交給一個對此負責的ConnectionPooling處理。
b.pooled Connection可能由于一個client忘記關(guān)閉,而導(dǎo)致整個pool阻塞。所以,應(yīng)該對pooled Connection進行監(jiān)控,對于超時的或其他invaild狀態(tài)的pooled connection強制回收。
這個問題提的好!起初,我覺得這也只是另一個ConnectionPooling的邏輯。可以交給一個監(jiān)測已分配的連接使用情況的ConnectionPooling實現(xiàn)來處理。但仔細一想。這樣做是不好的。
首先,監(jiān)視連接的使用一定會需要在連接對象上記錄一些狀態(tài),象連接分配的時間,最近一次客戶使用該連接的時間等等。而ConnectionPooling的語義是返回pool里的物理連接,而由ConnectionPooling2Pool類來做封裝。這樣,ConnectionPooling的實現(xiàn)就很難紀錄必要的信息。當然,ConnectionPooling也可以在返回物理連接前先做一個wrapper, 把信息紀錄在這個wrapper里。可是,這樣一來,類型安全就得不到保障。在使用該wrapper時,就要進行downcast.
其次,監(jiān)視已分配連接和管理空閑連接之間到底有多大耦合呢?能否對它們解耦呢?經(jīng)過分析,我感覺,答案是:不能。監(jiān)視已分配連接的算法理論上有可能需要知道空閑連接的一些信息,而反之也是一樣。而且,更討厭的是,它們之間所需要的信息量無法估計,也就是說,對一些特定的算法,它們可能是完全的緊耦合。如果按這樣分析,這種ConnectionPool可能還得要求實現(xiàn)者直接實現(xiàn)ConnectionPool, 就象我們第三章里使用的方法,只能偶爾使用一些utility類,象PooledConnection之類。
不過,雖然我們不能完全把監(jiān)視算法和分配算法分開。但事實上很多監(jiān)視算法,分配算法確實是互不相關(guān)的。我們也許可以寫一個框架,簡化對這些互不相關(guān)的算法的實現(xiàn)。雖然對完全緊耦合的情況我們無能為力,但對多數(shù)普通的情況,我們還是可以有些作為的。而且,這樣一個框架并不影響對復(fù)雜的緊耦合情況的特殊實現(xiàn)。
這個框架,當然應(yīng)該和我們現(xiàn)有的框架協(xié)同工作。具體的實現(xiàn)思路,我將在后面給出。
c.ConnectionPoolingImpl
public final synchronized void clear(){
closeAll();
freeConn.removeAllElements();
}//沒有transaction保證,有可能引起數(shù)據(jù)不一致,資源(connection)泄漏(connection沒關(guān)閉,pool卻拿掉了)
可以關(guān)閉一個connection,去掉一個pool對象
這里不需要transaction保證的。我們先關(guān)閉所有連接,然后再清連接池,怎么可能“connection沒關(guān)閉,pool卻拿掉了”呢?
擴展的問題
1.ConnectionPool是否定義成一個結(jié)構(gòu)interface更好,而讓pooling實現(xiàn)pooling算法。
pool可定義成Vector,Tree,...,負責存儲遍歷,而pooling負責check in,check out.
數(shù)據(jù)結(jié)構(gòu)和算法永遠是緊耦合的。實際上,算法決定數(shù)據(jù)結(jié)構(gòu),不可能實現(xiàn)定義一個數(shù)據(jù)結(jié)構(gòu),然后強迫所有算法使用。即使是Collection, Iterator之類較抽象的結(jié)構(gòu)也不行。
2.可能有大型的pool,比如字庫,因此有檢索問題
這就是ConnectionPooling的實現(xiàn)者要動的腦筋了。我們的框架只定義語義和責任分工,并不牽扯這樣的實現(xiàn)細節(jié)。
2.更復(fù)雜的是可能每個connection上有多個引用,pooling要負責給client引用最少的那個connection.
這還是一個實現(xiàn)的細節(jié)。不過我想不出有什么理由我們會要不同客戶共享同一個連接。這是不安全的,不是嗎?
3.可能同一個pool存儲不同類型的對象,對不同對象的處理是否可用visitor模式。
還是ConnectionPool的實現(xiàn)者的事。