被误解的C++——螺蛳壳里做道场

螺蛳壳里做道场

“螺蛳壳里做道场”是我们那里的一句俗话,意思是在很受限制、充满约束的情况下,做一些复杂的事情。前段时间我就遇到这么一个问题。

经常开发MIS类的应用,不免需要和数据库打交道。一直使用VC,(唉,反正我们公司已经下了ms这条贼船了),访问数据库无非就是ODBCDAOOleDBADO什么的。(ADO.net因为无法控制服务端游标,被我一脚踢出候选名单)。

我是个懒人。ODBCOleDB。虽然功能强大,使用灵活,可一大堆Workaround,让我直起鸡皮疙瘩。所以,我打算选用一个直接可用的类库,帮助我简化开发。MFCCResoultSet什么的太臃肿了,没入我的法眼。这样,就只能选择OleDB Template了。OleDB Template倒是不错,policy化,使用起来既方便,又高效。其方便程度和ADO相差无几。

唉,我也是一个爱多事的人。有这么个不错的库,用便是了,可我却觉得不满意。问题主要集中在三个方面:

<!--[if !supportLists]-->1. <!--[endif]-->policy划分不合理,缺少游标控制、读写控制和书签控制的policy

<!--[if !supportLists]-->2. <!--[endif]-->policy实现也未能充分发挥静态类型约束的优点;

<!--[if !supportLists]-->3. <!--[endif]-->只能输出原始类型的数据,不能转换输出。

针对这三个方面,我打算改造OleDB Template。当然我也可以重做一个库,但是这不符合我懒人的风格。于是,我决定通过扩展OleDB模板和类达到这个目的。

我最主要的改造对象是CCommand模板。这个模板的声明如下:

template <
class TAccessor = CNoAccessor,
template
< typenameT > 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

<!--[if !supportLists]--> 1. <!--[endif]-->CursorT,实现游标控制。包括defaultfast-forwardstatickey-setdynamic五种游标;

<!--[if !supportLists]-->2. <!--[endif]-->ReadWriteT,实现读写控制。包括read-onlyread-write

<!--[if !supportLists]-->3. <!--[endif]-->BookMarkT,实现书签管理。包括has-bookmarkno-bookmark

同时,还需要为新的Command模板加上静态约束。也就是,如果CursorTdefaultfast-forward两种只进游标,那么Command模板的实例(类)只提供MoveNext()成员函数;如果是其他服务端游标,则提供MovePre()MoveFirst()MoveLast()等等成员函数。如果ReadWriteTread-only,那么只提供GetValue()成员,否则,必须提供SetValue()UpdateData()等成员。BookMarkT也有类似的情况。

这么做,可以使得一些对游标、读写等错误的使用在编译期就得到拦截,而无需象OleDB Template那样等到运行期检验返回结果码才能知道。(其实,我也可以通过改造所有的PolicyCCommand模板,使用异常代替结果码。但是工程量太大,暂时不做考虑)。

另外,我还需要增加一个policyConvertorT,负责类型转换,确保数据输出的类型安全。同时也简化操作。(不再需要读取数据类型,再将数据读出到相应类型的数据缓冲区中)。包括AutoConvertorT,执行自动类型转换。和NoConvertorT,不执行转换,用于性能要求很高的情况。将来,如果需要,还可以开发PartialConvertorT,只执行与字段类型相对应的C++类型的转换。

所有这些扩展,都无法在原始的OleDB Template中实现,因为我无权修改OleDB Template的源代码。(这就叫螺蛳壳)。于是,我作了一个扩展库,用新的模板实现我需要的功能,但其实现,还是使用老的OleDB Template

template <
typenameBinderT,
typenameBufferT,
typenameCursorT,
typenameReadWriteT,
typenameBookMarkT,
typenameConvertorT,
typenameMultiResultT
>
class CommandExt:
public BinderT,
public BufferT,
public CursorT,
public ReadWriteT,
public BookMarkT,
public ConvertorT
... {

private:
typedefBinderT::AccessorTAccessorT;
typedefBufferT::RowsetTRowsetT;
typedefCCommand
<AccessorT,RowsetT,MultiResultT>CommandT;

CommandTm_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处理。

结果我发现,两者都有问题。当我草草写下这样一个构造函数时,并未意识到事态的严重:

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的构造函数时,便迎头撞上了这个问题:

class AutoBinder
{
public :
AutoBinder(
??? cmd):m_rCommand(cmd){};

private :
??? m_rCommand;
};

标记为???的地方填什么?也就是这里放什么类型。请注意AutoBinderpolicy定义于CommandExt使用之前,根本无法知道最终m_Command的类型是什么。

一种明显的解决方法是把所有policy做成模板:

template<typenameA,typenameR,typenameM>
class AutoBinder
... {
public:
typedefCommand
<A,R,M>CommandT;
public:
AutoBinder(CommandTcmd):m_rCommand(cmd)
...{};

private:
CommandTm_rCommand;
}
;

这种解决方法不但累赘,而且使得policy这些本该相互正交的类型耦合在了一起。

另一种可能(只是可能)的方法,就是使用C++0xauto

class AutoBinder
... {
public:
AutoBinder(autocmd):m_rCommand(cmd)
...{};

private:
autom_rCommand;
}
;

限于对auto提案的了解,我并不知道这个方案是否真的可行。(哪位知晓,请告知,先谢了J)。况且远水解不了近渴。

所以,我转而采用回调方案。但是,很快我遇到了新的问题:在CommandExt中定义的事件是“死的”,而policy上的功能是独立的、自由的。在CommandExt中很难预测到policy对回调的需求。

任何方案都有问题,我陷入了进退两难的境地。整整两天,我都陷在这个问题中,不能自拔。我并没有放弃,因为我有一种感觉,一个非常明显的解决方案唾手可得,只是没有发现而已。

经过长期的工作,我养成了一个习惯:如果解决不了一个问题,就反过来想想。所以,我就尝试着从另一个方向分析这个问题:我希望增加一些policy,并且通过改变policy的实现强化静态约束。静态约束也就是限制CommandExt的行为。。嗯?限制行为?一个模板/类的行为是什么?。没错,是它的成员函数。限制成员函数?不就是去掉几个成员函数么。去掉成员函数?我知道C++0x里有=delete,可以去掉某个特殊成员函数。可是,不能去掉普通成员函数。(唉,即便行,远水也解不了近渴)。

突然,我想起了Bjarne的话:如果你不想要什么,就把它声明成private!(D&E

我有办法了!但是吃不准是否能行。于是,等到儿子跑开,去粘着他妈妈的时候,我立刻打开计算机,做了个试验程序,看看继承类的private成员函数是否能够屏蔽掉基类中同名的成员函数。我想很多人都知道结果吧:可行!

所以,第二天,我便设计了一个新方案,只用了半天的时间,便大致构造完成了这个扩展库。新的CommandExt模板是这样定义的:

template <
typenameAccessorT,
template
< typenameA > class RowsetT = CRowset,
template
< typenameR > class CursorT = DefaultCursor,
typenameReadWriteT
= ReadOnly,
typenameBookMarkT
= NoBookMark,
template
< typenameA > class ConvertorT = AutoConvertor,
typenameMultiResultT
= CMultipleResults
>
class CommandExt
:
public CCommand <
typenameAccessorExt
<
AccessorT,ReadWriteT,
BookMarkT,ConvertorT>::Type,
typenameRowsetExt<
RowsetT,CursorT,ReadWriteT,
BookMarkT>
::Type,
MultiResultT
>
{…};

比较复杂,我一点一点解释。头两个和最后一个模板参数和CCommand的一样。中间的都是新增的。CursorTConvertorT都是template-template parameter,而ReadWriteTBookMarkT都是类型。这种差异是有原因的,(当然是迫不得已),一会儿会看到。

让我们先看看新的policy是如何工作的。以CursorT为例,我给出两个不同的Cursor Policy,一看就明白了:

// FastForwardCursorPolicy
template < typenameRowsetT >
class FastForward: public RowsetT
{
private
HRESULTMoveFirst();
HRESULTMoveLast();
HRESULTMovePrev();
HRESULTMoveToRatio();
};
// scrollableCursorPolicy
template < typenameRowsetT >
class StaticCursor: public RowsetT
{
};

FastForward声明了MoveFirst()等四个成员函数,并将其置为private。而StaticCursor没有声明任何成员函数。这些Policypublic继承自模板参数,于是,private成员函数将会屏蔽所有基类的同名成员函数。由于类型参数RowsetT必然是一个CRowset类或其继承类。所以,FastForward将会屏蔽RowsetT上相应的成员函数,而只剩下MoveNext()。这样,便使CommandExt具备了与游标类型相一致的行为(只进不退)。相反,StaticCursor没有定义任何成员函数,便不会覆盖RowsetT上的任何成员函数。于是,CommandExt则具备了static游标相一致的行为(可进可退,随机访问)。

其他的各个policy都采用这种形式来“修饰”相应的OleDB Template类。实际上,这种方式就是Matthew Wilson的“饰面”(Imperfect C++, 21章)的一种特殊应用。

接下来,再看看CommandExt的基类。CommandExt直接继承自CCommand,只是类型实参用了更加炫目的形式给出。在解释这一大堆杂碎之前,先了解一下CCommand同它的类型参数之间的关系:

template < >
class CCommand:
public CAccessorRowset <
TAccessor,
TRowset
> ,
public CCommandBase,
public TMultiple
{…}

CCommand继承自三个类:CAccessorRowset< TAccessor, TRowset >CCommandBaseTMultipleCCommandBase包含了CCommand的一些基础服务,同我们的主体无关。TMultiple是类型参数,负责控制多结果集,它工作的很好,也就不去理它了。剩下的,就是CAccessorRowset<>模板了。看一下它的定义,我们便明白该从何入手解决问题了:

template <
class TAccessor = CNoAccessor,
template
< typenameT > class TRowset = CRowset
>
class CAccessorRowset:
public TAccessor,
public TRowset < TAccessor >
... {}

该模板继承自它的两个模板参数TAccessorTRowset,而且都是public的。(说明一下,TRowset<>并没有继承自它的类型参数,类型参数TAccessor有其他用途)。这表明一点,CCommand本身并不提供数据绑定、缓存处理、数据输出等操作。这些工作都是由TAccessorTRowset完成的。这就是现代policy模式。

基于这个结构,我们可以很容易地想到,只要把TAccessorTRowset用我们新增的policy约束起来,便可以达成目的。说做就做,我把CommandExt的定义改成如下结构:

template <
typenameAccessorT,
template
< typenameA > class RowsetT = CRowset,
template
< typenameR > class CursorT = DefaultCursor,
template
< typenameR > class ReadWriteT = ReadOnly,
template
< typenameR > class BookMarkT = NoBookMark,
template
< typenameA > class ConvertorT = AutoConvertor,
typenameMultiResultT
= CMultipleResults
>
class CommandExt
:
public CCommand <
BookMarkT
<ReadWriteT<ConvertorT<AccessorT>>>,
BookMarkT<ReadWriteT<CursorT<RowsetT<AccessorT>>>>
,
MultiResultT
>
{…};

看起来很合理,但是这里存在两个问题。(此时我才意识到,OleDB Template这个“螺蛳壳”有多小)。第一,BookMarkTReadWriteT在两个模板参数中都出现。而这两个模板参数最终都为CommandExt所继承(并且都是非virtual继承,我们也改不了),这将会引爆多继承上的核心问题(两组嵌套的policy分别操纵不同的子对象)。尽管目前BookMarkTReadWriteT对应的类都是空的,但是我们不能确定未来扩展都能提供这种保证。

造成这个问题的原因,主要是AccessorTRowsetT,也就是OleDB TemplateAccessor类和Rowset类,都拥有与读写和书签相关的成员函数。比如Get/SetValueAccessor上,而UpdateDataRowset上。这样,我们被迫对这两组类都必须同时施加读写和书签policy

第二个问题,CCommand的第二个模板参数应当是一个模板,但BookMarkT<ReadWriteT<CursorT<RowsetT<AccessorT> > > >是一个类。无法通过编译。

对于第一个问题,经过一番考虑,我想出了这样一个办法:分别针对AccessorRowset制作读写和书签policy,并把它们包装在一个类中:

struct ReadOnly
{
// 用于约束Rowset的部分
template < typenameRowsetT >
class ForRowset: public RowsetT
{
private :
HRESULTDelete();
HRESULTInsert();
HRESULTSetData();
HRESULTUndo();
HRESULTUpdate();
HRESULTUpdateAll();
};
// 用于约束Accessor的部分
template < typenameAccessorT >
class ForAccessor: public AccessorT
{
private :
bool SetLength();
bool SetStatus();
bool SetValue();
};
// CManualAccessor有其特殊性,对其特化,不做约束
template <> class ForAccessor < CManualAccessor >
:
public CManualAccessor{};
};

这样,可以把CommandExt继承类改成:

: public CCommand <
typenameBookMarkT::ForAccessor
<
typenameReadWriteT::ForAccessor
<
ConvertorT
< AccessorT > > > ,
typenameBookMarkT::ForRowset
<
typenameReadWriteT::ForRowset
<
CursorT
< RowsetT < AccessorT > > > > ,
MultiResultT
>

但这依然无法通过编译,因为同样存在第二个问题。为解决这个问题,我做了一个辅助模板:

template <
template
< typenameA > class RowsetT,
template
< typenameR > class CursorT,
typenameReadWriteT,
typenameBookMarkT
>
struct RowsetExt
{
template
<typenameA>
class Type
:
public ReadWriteT::ForRowset<
typenameBookMarkT::ForRowset<
CursorT<RowsetT<A>>>>

{};
};

这里,类模板的基类就是前面CCommand的第二个模板实参。这样,便构造出一个符合Rwoset要求的模板。其实,最完美的方案是使用C++0xtemplate alias(也就是template typedef):

template < >
struct RowsetExt
{
template
<typenameA>
using Type=ReadWriteT::ForRowset<
typenameBookMarkT::ForRowset<
CursorT<RowsetT<A>>>>
;
};

当然啦,目前我们也只能将就了。为了保持形式统一,我也为Accessor做了一个辅助模板:

template <
typenameAccessorT,
typenameReadWriteT,
typenameBookMarkT,
template
< typenameA > class ConvertorT
>
struct AccessorExt
{
class Type
:
public ReadWriteT::ForAccessor <
typenameBookMarkT::ForAccessor
<
ConvertorT
< AccessorT > > >
{};
分享到:
评论

你可能感兴趣的:(数据结构,C++,c,C#,ext)