codefans

导航

<2024年5月>
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678

统计

常用链接

留言簿(2)

随笔分类

随笔档案

文章分类

文章档案

程序设计链接

搜索

最新评论

阅读排行榜

评论排行榜

OO随笔(关于connection pool系列的补充,兼答bonmot) 选择自 ajoo 的 Blog

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的实现者的事。

 


相关文章
对该文的评论
ajoo ( 2002-08-07)
myan, 我想我可能是没有明白你的意思。那个关于hdc的东西,就当我没说吧。

至于pattern与否。我总觉得pattern看看可以。用来和别人交流也不错。
但真正做东西时,往往是做完了之后才reverse-engineer, 发现:“啊,我这里用的是bridge嘛,那里用的是state..."
对于你的gui框架。我想,我会本着“分析需求;用接口定义需求;解耦”的原则来设计。并且象我上一贴提到的剥皮般地
进行逐层细分。这样,即使你最后发现某一部分不合适,修改也会被局限在最小的范围内。
举个例子,它就象通讯协议里的层:
定义了网络层,下面又可以定义链路层,物理层。但对于网络层的用户,其它两层都是透明的,并不是必然的,或强迫的。如果实现者觉得好,完全可以绕过链路层,或再加几层。无论如何更改,不影响网络层的用户。而递归地,对于链路层的用户,物理层的实现又是透明的。

当然,面向接口的OO, 并不适合对performance敏感到一两个函数调用开销都要计较的应用,aggregate, interface的使用,总会引入一定的开销。

bonmot ( 2002-08-05)
myan,
个人觉得,设计时结构性质的pattern或不是pattern的框架已经成形,refactor时某些实现的pattern会自然而然逐步显现。

ajoo,
如果factory维持对产品的引用,肯定是可以回收的,只是这样会增加factory的复杂度,是否好可以商榷。
ar7_top ( 2002-08-04)
学习
waveless ( 2002-08-03)
ajoo关于DC的那段我没看懂,myan的意思应该是在用到jpeg2dib转换
的某个函数里面无法得到和窗口相关的HDC了。这里的hdc应该是个参
数。HdbJpeg2Dib里的HDC是那儿来的?

当然象jpeg2dib这样的过程中理论上是不应该需要HDC做参数的,但
是Windows中和DIB有关的很多函数都需要HDC。其实只是为了取得一
些设置象调色板什么的,用GetDC(NULL)取个设备DC给它应该就可以。
不一定非要是某一个窗口的DC。但是这需要先看懂它的代码,并对
Windows中有关DIB的函数很熟悉。这样就失去用现成库的意义了。
ajoo ( 2002-08-03)
myan:
还不是很明白。如果Jpeg2Dib是一个接口:
interface Jpeg2Dib{
   virtual Dib* convert(Jpeg* jpeg)=0;
};
难道你不能这样实现?
class HdbJpeg2Dib:public Jpeg2Dib{
   public:
   Dib* convert(Jpeg* jpeg){
      return ::convert_func(hdc, jpeg);
   }
   private:
   const HDC hdc;
};

bonmot:
我觉得你没有很明白面向接口的意思。为什么不要求保证ConnectionPool实现中一定引用ConnectionPooling接口呢?因为这样做是不好的。
面向接口的原则是:谁需要,就有谁定义。提供功能的类可以不定义接口。但需要功能的模块一定要定义所需要的接口。
我们的ConnectionPool是pool的用户需要的。对于一个ConnectionPool的用户来说,他并不关心你是用什么巧妙的方法实现的这个ConnectionPool, 只要它能完成所定义的操作,对用户来说就可以。谁管你是直接实现的还是用的什么pattern? ConnectionPooling相对于ConnectionPool的用户来说,只是实现细节。属于解耦要隐藏的无关信息。
所以没有理由要把ConnectionPooling的细节公之于众。

也许你要说:应该强迫程序员在实现ConnectionPool的时候使用ConnectionPooling. 我觉得这种强迫是不应该的。无论你的实现方法多么巧妙,你永远不应该把它强加于人。
而且,现实世界的复杂性永远是超过我们的想象的。无论你多么激动于自己天才的实现方法,都不要以为它可以解决所有问题。就象你提出的监测已分配的连接的使用,它就完全可能和ConnectionPool的实现紧耦合,而使得我们的bridge pattern (仅仅实现ConnectionPooling)无法有效实现需求。
另外,即使我们的ConnectionPooling的方法真能解决所有ConnectionPool实现的需求,我们就可以强迫所有实现都用它吗?如果忽然有人给了我们一个已经实现好的connection pool, 它是五年前设计的,完全没有用我们的ConnectionPooling的方法,难道我们就不能使用它了吗?为什么不能简单地用一个adapter来用它呢?

其实,我顶烦某些巨大的framework, 你要不就用它,享受它的好处,也忍受它的局限。要不就只能干脆抛开它。象MFC就是这样。
一个真正的面向接口的设计应该开放的,我的想象应该是这样:
首先,假设需要实现的系统的功能被定义在一系列接口中。ok, 这些接口就是需求,无论你怎样实现。
现在,我们要实现这些接口。在实现过程中,我们发现系统可以被拆成几个独立的模块,(这里的“可以”,可以是我们对需求的分析,认为这几个模块可以自然被解耦,也可能是我们发现,虽然该系统理论上是个紧耦合,但对一些特定的场合,还是可以做一些分解以简化实现)。这样,几套子需求被定义。系统被拆成几个小系统,几套子接口,以及组合小系统为大系统的逻辑。
这里,对顶层接口来说,这些子系统,子接口,都不是必须的。它们只是“限于我们有限的知识和经验所使用的一种我们觉得好的实现方法”。所以,如果以后有我们的设计所没有预料到的情况,至少我们还可以直接绕过这层设计,不会影响顶层的接口。
好,同样的分析,分解,递归地在实现每个子系统时使用。直到设计者认为已分解的足够细,或留到以后refactor时再说。
在这一层一层的分解过程中,每一层对其上一层来说都不是必须的,并且是透明的。这样,如果后来的实现者发现某一个子模块设计得不能完全符合需要,都完全可以推倒重来。
在这样的一个开放式的设计中,没有什么是强迫的。从最顶层的实现,到底层的某个小子模块的实现,都不是神圣不可侵犯的。
当然,系统设计的目标是要尽量避免将来对相对顶层的子系统设计更换的可能性。因为,越更换顶层,代价越大,除非,那只是一个用于demo或测试的dummy实现, 或一个其它legacy system的adapter。

这里,设计者应是谦虚的,不是说:“我就是这样设计的,想要灵活性?呵呵,我都预想到了,这里,这里,这里,都是我预留的供你cusomize的地方。什么?不够?不可能!”
而是说:“我的设计不一定就是最好的,如果你认为有更好的设计实现方法,或它不能满足你的需要,你可以轻易地使用你自己的方法。我的任何一层你都可以替换成你的实现。”

另外,用factory来释放对象确实是不可行的。用template?template依赖于静态类型。而factory方法的返回类型只能是一个。

elm:
我觉得你所描述的接口系统就象我说的mfc式的封闭式的系统。繁文缛节都规定好了,只能使用,或在实现预留好的地方做些修改。
比如说:总经理告诉项目经理要上一个项目。项目经理必须先开动员会,再做项目报告,再做需求分析。。。。。。一系列的过程都定死了。
而一个开放式的系统是:总经理告诉项目经理要上一个项目,项目经理只负责最终实现这个项目,给出所有需要的deliverables.
对项目经理来说,它可以是用上面描述的方法来组织项目。但那并不是公司要求的。如果需要,完全可以换个项目经理,引入一个新的方法。

myan:
我倒没感觉你的那个gui系统就一定是composition或其它的任何一种pattern. :)
我觉得,用上面说的那种拨皮式的方法,即使你最后要修改实现,应该也不是个很大的工程。为什么上来就直奔pattern而去呢?
 

posted on 2005-11-22 11:52 春雷的博客 阅读(271) 评论(0)  编辑  收藏


只有注册用户登录后才能发表评论。


网站导航: