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