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

螺蛳壳里做道场

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

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

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

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

1.       policy划分不合理,缺少游标控制、读写控制和书签控制的policy

2.       policy实现也未能充分发挥静态类型约束的优点;

3.       只能输出原始类型的数据,不能转换输出。

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

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

template  <
   
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,实现游标控制。包括defaultfast-forwardstatickey-setdynamic五种游标;

2.       ReadWriteT,实现读写控制。包括read-onlyread-write

3.       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 <
  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处理。

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

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<typename A, typename R, typename M>
class  AutoBinder
{
public:
  typedef Command
<A, R, M> CommandT;
public:
  AutoBinder(CommandT cmd):m_rCommand(cmd)
{};
  …
private:
  CommandT m_rCommand;
}
;

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

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

class  AutoBinder
{
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模板是这样定义的:

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
  
>
{…};

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

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

// FastForward 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没有声明任何成员函数。这些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 
< typename T >   class  TRowset  =  CRowset 
>  
class  CAccessorRowset :
   
public  TAccessor, 
   
public  TRowset < TAccessor >
{}

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

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

template <
  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这个“螺蛳壳”有多小)。第一,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 < 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继承类改成:

: public  CCommand <
  typename BookMarkT::ForAccessor
<
      typename ReadWriteT::ForAccessor
<
         ConvertorT
< AccessorT >   >   > ,
  typename BookMarkT::ForRowset
<
      typename ReadWriteT::ForRowset
<
         CursorT
< RowsetT < AccessorT >   >   >   > ,
  MultiResultT
>  

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

template <
  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++0xtemplate alias(也就是template typedef):

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

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

template <
  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扩展:

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,则用自己的GetValueSetValue覆盖AccessorT的对应函数:

template < typename A >
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使用起来差不多是这样的:

// 动态绑定,普通结果集,fast-forward游标,只读,有书签,无多结果集
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 。更主要的,我得到了这样一个教训:不要只把眼光放在非常高级、尖端的技术运用上,而应该综合考虑各种技术,合理的应用。有时,一个很古老的、不起眼的,甚至卑微的特性,可以很好地达到其他方法无法实现的目标
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值