螺蛳壳里做道场
“螺蛳壳里做道场”是我们那里的一句俗话,意思是在很受限制、充满约束的情况下,做一些复杂的事情。前段时间我就遇到这么一个问题。
经常开发MIS类的应用,不免需要和数据库打交道。一直使用VC,(唉,反正我们公司已经下了ms这条贼船了),访问数据库无非就是ODBC、DAO、OleDB、ADO什么的。(ADO.net因为无法控制服务端游标,被我一脚踢出候选名单)。
我是个懒人。ODBC和OleDB。虽然功能强大,使用灵活,可一大堆Workaround,让我直起鸡皮疙瘩。所以,我打算选用一个直接可用的类库,帮助我简化开发。MFC的CResoultSet什么的太臃肿了,没入我的法眼。这样,就只能选择OleDB Template了。OleDB Template倒是不错,policy化,使用起来既方便,又高效。其方便程度和ADO相差无几。
唉,我也是一个爱多事的人。有这么个不错的库,用便是了,可我却觉得不满意。问题主要集中在三个方面:
1. policy划分不合理,缺少游标控制、读写控制和书签控制的policy;
2. policy实现也未能充分发挥静态类型约束的优点;
3. 只能输出原始类型的数据,不能转换输出。
针对这三个方面,我打算改造OleDB Template。当然我也可以重做一个库,但是这不符合我懒人的风格。于是,我决定通过扩展OleDB模板和类达到这个目的。
我最主要的改造对象是CCommand模板。这个模板的声明如下:
class TAccessor = CNoAccessor,
template < typename T > class TRowset = CRowset,
class TMultiple = CNoMultipleResults
>
class CCommand;
这里, TAccessor 是用于访问控制的 policy ,负责数据绑定和输入输出,包括 CManulAccessor (手工绑定数据,一般很少使用)、 CDynamicAccessor (“全自动”数据绑定,使用最方便的 Accessor )、 CDynamicParameterAccessor (在 CDynamicAccessor 基础上,增加参数功能)、 CStringDynamicAccessor (以 string 绑定所有类型,通常用于数据显示)、 CXMLAccessor (输出 XML 格式数据的 Accessor )等等。
TRowset是用于管理数据缓存和游标运动的policy,包括CRowset(普通rowset)、CBulkRowset(块操作rowset)、CArrayRowset(提供[]操作符,可以像操作数组一样操作结果集)。
TMultiple是负责控制多结果集返回的policy。
作为改造,我首先需要增加三个新的policy:
1. CursorT,实现游标控制。包括default、fast-forward、static、key-set、dynamic五种游标;
2. ReadWriteT,实现读写控制。包括read-only和read-write;
3. BookMarkT,实现书签管理。包括has-bookmark和no-bookmark;
同时,还需要为新的Command模板加上静态约束。也就是,如果CursorT是default和fast-forward两种只进游标,那么Command模板的实例(类)只提供MoveNext()成员函数;如果是其他服务端游标,则提供MovePre()、MoveFirst()、MoveLast()等等成员函数。如果ReadWriteT是read-only,那么只提供GetValue()成员,否则,必须提供SetValue()、UpdateData()等成员。BookMarkT也有类似的情况。
这么做,可以使得一些对游标、读写等错误的使用在编译期就得到拦截,而无需象OleDB Template那样等到运行期检验返回结果码才能知道。(其实,我也可以通过改造所有的Policy和CCommand模板,使用异常代替结果码。但是工程量太大,暂时不做考虑)。
另外,我还需要增加一个policy:ConvertorT,负责类型转换,确保数据输出的类型安全。同时也简化操作。(不再需要读取数据类型,再将数据读出到相应类型的数据缓冲区中)。包括AutoConvertorT,执行自动类型转换。和NoConvertorT,不执行转换,用于性能要求很高的情况。将来,如果需要,还可以开发PartialConvertorT,只执行与字段类型相对应的C++类型的转换。
所有这些扩展,都无法在原始的OleDB Template中实现,因为我无权修改OleDB Template的源代码。(这就叫螺蛳壳)。于是,我作了一个扩展库,用新的模板实现我需要的功能,但其实现,还是使用老的OleDB Template:
typename BinderT,
typename BufferT,
typename CursorT,
typename ReadWriteT,
typename BookMarkT,
typename ConvertorT,
typename MultiResultT
>
class CommandExt :
public BinderT,
public BufferT,
public CursorT,
public ReadWriteT,
public BookMarkT,
public ConvertorT
{
…
private:
typedef BinderT::AccessorT AccessorT;
typedef BufferT::RowsetT RowsetT;
typedef CCommand<AccessorT, RowsetT, MultiResultT> CommandT;
CommandT m_Command;
} ;
(Ext或者Ex可是个很有用的词根,凡是需要扩展的地方,都可以用。有时甚至可以几个连在一起用。呵呵,都是从ms那里学来的。当然啦,如果原来设计得好,扩展性强,那么也就用不着Ex或者Ext了,对吧?)
这个模板的实现,原先我是打算让CommandExt继承自这些Policy,这是标准的现代Policy模式。这里,BinderT policy提供了类型AccessorT,在不同的Binder里,定义了不同的AccessorT别名。比如AutoBinderT里,就可以用CDynamicAccessor定义AccessorT。同样,不同的BufferT里,RowsetT也定义了相应的Rowset模板。此后,CCommand<>模板在CommandExt<>里实例化,并且创建一个成员对象。这是Composite模式标准应用。
但是,我立刻遇到了麻烦。因为所有的policy都需要操作m_Command对象。对此,我有两个办法:其一,让每一个policy持有m_Command的引用或指针。其二,通过call-back,让policy回调CommandExt的事件,将所需的操作交由CommandExt处理。
结果我发现,两者都有问题。当我草草写下这样一个构造函数时,并未意识到事态的严重:
BinderT(m_Command),
BufferT(m_Command),
CursorT(m_Command),
ReadWriteT(m_Command),
BookMarkT(m_Command)
{}
这里有两个问题。第一,我打算让policy持有m_Command的引用,所以只能通过其构造函数对其初始化。(我不会用指针的,因为实在不想每次操作前都对指针做有效性检验,太费事,也太低效了)。但是m_Command,是CommandExt的成员变量,是在所作为基类的policy初始化完成之后再初始化。这里的初始化方式是“违反规定”的。但是,在这里的特殊情况下,这不会成为问题。因为在policy的构造函数中,我并未“使用”m_Command,仅仅用它初始化了一个引用。m_Command的内存是在CommandExt()之前就已经分配完成的(同CommandExt本身一起),所以仅仅取得m_Command地址是不会有问题的。事实也证明,我的这个“冒险”举动并未产生任何问题,除了编译器的几声微弱的抱怨(warning)。
第二个问题就严重了,是致命的。当我试图写policy的构造函数时,便迎头撞上了这个问题:
{
public :
AutoBinder( ??? cmd):m_rCommand(cmd){};
…
private :
??? m_rCommand;
};
标记为???的地方填什么?也就是这里放什么类型。请注意AutoBinder是policy,定义于CommandExt使用之前,根本无法知道最终m_Command的类型是什么。
一种明显的解决方法是把所有policy做成模板:
class AutoBinder
{
public:
typedef Command<A, R, M> CommandT;
public:
AutoBinder(CommandT cmd):m_rCommand(cmd){};
…
private:
CommandT m_rCommand;
} ;
这种解决方法不但累赘,而且使得policy这些本该相互正交的类型耦合在了一起。
另一种可能(只是可能)的方法,就是使用C++0x的auto:
{
public:
AutoBinder(auto cmd):m_rCommand(cmd){};
…
private:
auto m_rCommand;
} ;
限于对auto提案的了解,我并不知道这个方案是否真的可行。(哪位知晓,请告知,先谢了J)。况且远水解不了近渴。
所以,我转而采用回调方案。但是,很快我遇到了新的问题:在CommandExt中定义的事件是“死的”,而policy上的功能是独立的、自由的。在CommandExt中很难预测到policy对回调的需求。
任何方案都有问题,我陷入了进退两难的境地。整整两天,我都陷在这个问题中,不能自拔。我并没有放弃,因为我有一种感觉,一个非常明显的解决方案唾手可得,只是没有发现而已。
经过长期的工作,我养成了一个习惯:如果解决不了一个问题,就反过来想想。所以,我就尝试着从另一个方向分析这个问题:我希望增加一些policy,并且通过改变policy的实现强化静态约束。…静态约束也就是限制CommandExt的行为。…。嗯?限制行为?一个模板/类的行为是什么?…。没错,是它的成员函数。限制成员函数?不就是去掉几个成员函数么。去掉成员函数?我知道C++0x里有=delete,可以去掉某个特殊成员函数。可是,不能去掉普通成员函数。(唉,即便行,远水也解不了近渴)。
突然,我想起了Bjarne的话:如果你不想要什么,就把它声明成private!(D&E)
我有办法了!但是吃不准是否能行。于是,等到儿子跑开,去粘着他妈妈的时候,我立刻打开计算机,做了个试验程序,看看继承类的private成员函数是否能够屏蔽掉基类中同名的成员函数。我想很多人都知道结果吧:可行!
所以,第二天,我便设计了一个新方案,只用了半天的时间,便大致构造完成了这个扩展库。新的CommandExt模板是这样定义的:
typename AccessorT,
template < typename A > class RowsetT = CRowset,
template < typename R > class CursorT = DefaultCursor,
typename ReadWriteT = ReadOnly,
typename BookMarkT = NoBookMark,
template < typename A > class ConvertorT = AutoConvertor,
typename MultiResultT = CMultipleResults
>
class CommandExt
: public CCommand <
typename AccessorExt <
AccessorT, ReadWriteT,
BookMarkT, ConvertorT>::Type,
typename RowsetExt<
RowsetT, CursorT, ReadWriteT,
BookMarkT> ::Type,
MultiResultT
>
{…};
比较复杂,我一点一点解释。头两个和最后一个模板参数和CCommand的一样。中间的都是新增的。CursorT和ConvertorT都是template-template parameter,而ReadWriteT和BookMarkT都是类型。这种差异是有原因的,(当然是迫不得已),一会儿会看到。
让我们先看看新的policy是如何工作的。以CursorT为例,我给出两个不同的Cursor Policy,一看就明白了:
template < typename RowsetT >
class FastForward : public RowsetT
{
private :
HRESULT MoveFirst( );
HRESULT MoveLast( );
HRESULT MovePrev( );
HRESULT MoveToRatio();
};
// scrollable Cursor Policy
template < typename RowsetT >
class StaticCursor : public RowsetT
{
};
FastForward声明了MoveFirst()等四个成员函数,并将其置为private。而StaticCursor没有声明任何成员函数。这些Policy都public继承自模板参数,于是,private成员函数将会屏蔽所有基类的同名成员函数。由于类型参数RowsetT必然是一个CRowset类或其继承类。所以,FastForward将会屏蔽RowsetT上相应的成员函数,而只剩下MoveNext()。这样,便使CommandExt具备了与游标类型相一致的行为(只进不退)。相反,StaticCursor没有定义任何成员函数,便不会覆盖RowsetT上的任何成员函数。于是,CommandExt则具备了static游标相一致的行为(可进可退,随机访问)。
其他的各个policy都采用这种形式来“修饰”相应的OleDB Template类。实际上,这种方式就是Matthew Wilson的“饰面”(Imperfect C++, 第21章)的一种特殊应用。
接下来,再看看CommandExt的基类。CommandExt直接继承自CCommand,只是类型实参用了更加炫目的形式给出。在解释这一大堆杂碎之前,先了解一下CCommand同它的类型参数之间的关系:
class CCommand :
public CAccessorRowset <
TAccessor,
TRowset
> ,
public CCommandBase,
public TMultiple
{…}
CCommand继承自三个类:CAccessorRowset< TAccessor, TRowset >、CCommandBase和TMultiple。CCommandBase包含了CCommand的一些基础服务,同我们的主体无关。TMultiple是类型参数,负责控制多结果集,它工作的很好,也就不去理它了。剩下的,就是CAccessorRowset<>模板了。看一下它的定义,我们便明白该从何入手解决问题了:
class TAccessor = CNoAccessor,
template < typename T > class TRowset = CRowset
>
class CAccessorRowset :
public TAccessor,
public TRowset < TAccessor >
{}
该模板继承自它的两个模板参数TAccessor和TRowset,而且都是public的。(说明一下,TRowset<>并没有继承自它的类型参数,类型参数TAccessor有其他用途)。这表明一点,CCommand本身并不提供数据绑定、缓存处理、数据输出等操作。这些工作都是由TAccessor和TRowset完成的。这就是现代policy模式。
基于这个结构,我们可以很容易地想到,只要把TAccessor和TRowset用我们新增的policy约束起来,便可以达成目的。说做就做,我把CommandExt的定义改成如下结构:
typename AccessorT,
template < typename A > class RowsetT = CRowset,
template < typename R > class CursorT = DefaultCursor,
template < typename R > class ReadWriteT = ReadOnly,
template < typename R > class BookMarkT = NoBookMark,
template < typename A > class ConvertorT = AutoConvertor,
typename MultiResultT = CMultipleResults
>
class CommandExt
: public CCommand <
BookMarkT <ReadWriteT<ConvertorT<AccessorT> > >,
BookMarkT<ReadWriteT<CursorT<RowsetT<AccessorT> > > > ,
MultiResultT
>
{…};
看起来很合理,但是这里存在两个问题。(此时我才意识到,OleDB Template这个“螺蛳壳”有多小)。第一,BookMarkT、ReadWriteT在两个模板参数中都出现。而这两个模板参数最终都为CommandExt所继承(并且都是非virtual继承,我们也改不了),这将会引爆多继承上的核心问题(两组嵌套的policy分别操纵不同的子对象)。尽管目前BookMarkT和ReadWriteT对应的类都是空的,但是我们不能确定未来扩展都能提供这种保证。
造成这个问题的原因,主要是AccessorT和RowsetT,也就是OleDB Template的Accessor类和Rowset类,都拥有与读写和书签相关的成员函数。比如Get/SetValue在Accessor上,而UpdateData在Rowset上。这样,我们被迫对这两组类都必须同时施加读写和书签policy。
第二个问题,CCommand的第二个模板参数应当是一个模板,但BookMarkT<ReadWriteT<CursorT<RowsetT<AccessorT> > > >是一个类。无法通过编译。
对于第一个问题,经过一番考虑,我想出了这样一个办法:分别针对Accessor和Rowset制作读写和书签policy,并把它们包装在一个类中:
{
// 用于约束Rowset的部分
template < typename RowsetT >
class ForRowset : public RowsetT
{
private :
HRESULT Delete();
HRESULT Insert();
HRESULT SetData( );
HRESULT Undo();
HRESULT Update();
HRESULT UpdateAll();
};
// 用于约束Accessor的部分
template < typename AccessorT >
class ForAccessor : public AccessorT
{
private :
bool SetLength();
bool SetStatus();
bool SetValue();
};
// CManualAccessor有其特殊性,对其特化,不做约束
template <> class ForAccessor < CManualAccessor >
: public CManualAccessor{};
};
这样,可以把CommandExt继承类改成:
typename BookMarkT::ForAccessor <
typename ReadWriteT::ForAccessor <
ConvertorT < AccessorT > > > ,
typename BookMarkT::ForRowset <
typename ReadWriteT::ForRowset <
CursorT < RowsetT < AccessorT > > > > ,
MultiResultT
>
但这依然无法通过编译,因为同样存在第二个问题。为解决这个问题,我做了一个辅助模板:
template < typename A > class RowsetT,
template < typename R > class CursorT,
typename ReadWriteT,
typename BookMarkT
>
struct RowsetExt
{
template <typename A>
class Type
: public ReadWriteT::ForRowset<
typename BookMarkT::ForRowset<
CursorT<RowsetT<A> > > >
{};
};
这里,类模板的基类就是前面CCommand的第二个模板实参。这样,便构造出一个符合Rwoset要求的模板。其实,最完美的方案是使用C++0x的template alias(也就是template typedef):
struct RowsetExt
{
template <typename A>
using Type=ReadWriteT::ForRowset<
typename BookMarkT::ForRowset<
CursorT<RowsetT<A> > > > ;
};
当然啦,目前我们也只能将就了。为了保持形式统一,我也为Accessor做了一个辅助模板:
typename AccessorT,
typename ReadWriteT,
typename BookMarkT,
template < typename A > class ConvertorT
>
struct AccessorExt
{
class Type
: public ReadWriteT::ForAccessor <
typename BookMarkT::ForAccessor <
ConvertorT < AccessorT > > >
{};
};
于是,便得到一个可用的OleDB Template扩展:
typename AccessorT,
template < typename A > class RowsetT = CRowset,
template < typename R > class CursorT = DefaultCursor,
typename ReadWriteT = ReadOnly,
typename BookMarkT = NoBookMark,
template < typename A > class ConvertorT = AutoConvertor,
typename MultiResultT = CMultipleResults
>
class CommandExt
: public CCommand <
typename AccessorExt <AccessorT, ReadWriteT,
BookMarkT, ConvertorT>::Type,
typename RowsetExt<RowsetT, CursorT, ReadWriteT,
BookMarkT> ::Type,
MultiResultT
>
{…};
此后,还有一些外围工作,包括重写Open成员函数(覆盖掉CCommand::Open),以使同policy相关的DB Properties能够自动设置。等等。相比之下,这些工作就不那么富于探索性了。
最后,还有一个很重要的内容没有涉及,就是类型转换。在AccessorExt中,我们可以看到,ConvertorT直接在AccessorT外面包了一层,对于NoConvertor policy,就是一个空类,完全使用OleDB Template提供的原始数据输出和输入功能。而AutoConvertor,则用自己的GetValue和SetValue覆盖AccessorT的对应函数:
class AutoConvertor : public A
{
public :
template <typename T, typename I>
T GetValue(I& index) {
…
}
template<typename T, typename I>
void SetValue(I& index, T& val) {
…
}
};
和先前所定义的policy不同,这两个成员函数模板都是public,因为它们的任务不是屏蔽老的函数,而是提供新的功能。当然,SetValue最终可能会被ReadOnly的同名成员函数所屏蔽,以实现只读行为。
下一个要点,就是两个函数模板中“…”所代表的内容,也就是具体的类型转换代码。不过,抱歉,这保密:)。
呵呵,开个玩笑。限于篇幅,外加这些内容与本文的主题无关,所以类型转换部分留在将来的文章中再写。(不过可以先预告一下,所采用的方法同我在《被误解的C++——优化Variant实现》一文中使用的类似,有共同的技术基础)。
最后,这个CommandExt使用起来差不多是这样的:
CommandExt < CDynamicAccessor, CRowset, FastForward, ReadOnly, HasBookMark, NoMultiple > cmd;
cmd.Open( " select ... " );
while (cmd.MoveNext() == S_OK)
{
std:: string r = cmd.GetValue < std:: string > ( 5 );
...
int data = ...;
cmd.SetValue( 5 , data); // 编译错误,只读结果集,不能写
}
cmd.MoveFirst(); // 编译错误,只进游标,无法返回第一行结果
...
总结一下吧。为了能够充分发挥C++的静态约束机制,以及获得最大的自由度,我尝试着改造(或者说扩展)OleDB Template。开始,我准备采用标准的policy模式。但是由于OleDB Template不完善的policy结构,使得我很难用composite CCommand加以实现。随后,我从相反的方向考虑问题,并运用C++的一些传统机制,终于达到了最初设想的目标。
这里所采用的这种“饰面”,以及相关的“螺栓”(Imperfect C++,22章)方法,可以运用于大多数扩展既有类的应用。既可以增加新的功能,也可以限制原有的功能。非常灵活,方便。
在这个案例中,我得到的不仅仅是一个扩展的 OleDB Template 。更主要的,我得到了这样一个教训:不要只把眼光放在非常高级、尖端的技术运用上,而应该综合考虑各种技术,合理的应用。有时,一个很古老的、不起眼的,甚至卑微的特性,可以很好地达到其他方法无法实现的目标。