螺蛳壳里做道场
“螺蛳壳里做道场”是我们那里的一句俗话,意思是在很受限制、充满约束的情况下,做一些复杂的事情。前段时间我就遇到这么一个问题。
经常开发MIS类的应用,不免需要和数据库打交道。一直使用VC,(唉,反正我们公司已经下了ms这条贼船了),访问数据库无非就是ODBC、DAO、OleDB、ADO什么的。(ADO.net因为无法控制服务端游标,被我一脚踢出候选名单)。
我是个懒人。ODBC和OleDB。虽然功能强大,使用灵活,可一大堆Workaround,让我直起鸡皮疙瘩。所以,我打算选用一个直接可用的类库,帮助我简化开发。MFC的CResoultSet什么的太臃肿了,没入我的法眼。这样,就只能选择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<
classTAccessor=CNoAccessor,
template<typenameT>classTRowset=CRowset,
classTMultiple=CNoMultipleResults
>
classCCommand;
这里,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,实现游标控制。包括default、fast-forward、static、key-set、dynamic五种游标;
<!--[if !supportLists]-->2. <!--[endif]-->ReadWriteT,实现读写控制。包括read-only和read-write;
<!--[if !supportLists]-->3. <!--[endif]-->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:
template<
typenameBinderT,
typenameBufferT,
typenameCursorT,
typenameReadWriteT,
typenameBookMarkT,
typenameConvertorT,
typenameMultiResultT
>
classCommandExt:
publicBinderT,
publicBufferT,
publicCursorT,
publicReadWriteT,
publicBookMarkT,
publicConvertorT
...{
…
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的构造函数时,便迎头撞上了这个问题:
classAutoBinder
{
public:
AutoBinder(???cmd):m_rCommand(cmd){};
…
private:
???m_rCommand;
};
标记为???的地方填什么?也就是这里放什么类型。请注意AutoBinder是policy,定义于CommandExt使用之前,根本无法知道最终m_Command的类型是什么。
一种明显的解决方法是把所有policy做成模板:
template<typenameA,typenameR,typenameM>
classAutoBinder
...{
public:
typedefCommand<A,R,M>CommandT;
public:
AutoBinder(CommandTcmd):m_rCommand(cmd)...{};
…
private:
CommandTm_rCommand;
};
这种解决方法不但累赘,而且使得policy这些本该相互正交的类型耦合在了一起。
另一种可能(只是可能)的方法,就是使用C++0x的auto:
classAutoBinder
...{
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>classRowsetT=CRowset,
template<typenameR>classCursorT=DefaultCursor,
typenameReadWriteT=ReadOnly,
typenameBookMarkT=NoBookMark,
template<typenameA>classConvertorT=AutoConvertor,
typenameMultiResultT=CMultipleResults
>
classCommandExt
:publicCCommand<
typenameAccessorExt<
AccessorT,ReadWriteT,
BookMarkT,ConvertorT>::Type,
typenameRowsetExt<
RowsetT,CursorT,ReadWriteT,
BookMarkT>::Type,
MultiResultT
>
{…};
比较复杂,我一点一点解释。头两个和最后一个模板参数和CCommand的一样。中间的都是新增的。CursorT和ConvertorT都是template-template parameter,而ReadWriteT和BookMarkT都是类型。这种差异是有原因的,(当然是迫不得已),一会儿会看到。
让我们先看看新的policy是如何工作的。以CursorT为例,我给出两个不同的Cursor Policy,一看就明白了:
//FastForwardCursorPolicy
template<typenameRowsetT>
classFastForward:publicRowsetT
{
private:
HRESULTMoveFirst();
HRESULTMoveLast();
HRESULTMovePrev();
HRESULTMoveToRatio();
};
//scrollableCursorPolicy
template<typenameRowsetT>
classStaticCursor:publicRowsetT
{
};
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同它的类型参数之间的关系:
template<…>
classCCommand:
publicCAccessorRowset<
TAccessor,
TRowset
>,
publicCCommandBase,
publicTMultiple
{…}
CCommand继承自三个类:CAccessorRowset< TAccessor, TRowset >、CCommandBase和TMultiple。CCommandBase包含了CCommand的一些基础服务,同我们的主体无关。TMultiple是类型参数,负责控制多结果集,它工作的很好,也就不去理它了。剩下的,就是CAccessorRowset<>模板了。看一下它的定义,我们便明白该从何入手解决问题了:
template<
classTAccessor=CNoAccessor,
template<typenameT>classTRowset=CRowset
>
classCAccessorRowset:
publicTAccessor,
publicTRowset<TAccessor>
...{}
该模板继承自它的两个模板参数TAccessor和TRowset,而且都是public的。(说明一下,TRowset<>并没有继承自它的类型参数,类型参数TAccessor有其他用途)。这表明一点,CCommand本身并不提供数据绑定、缓存处理、数据输出等操作。这些工作都是由TAccessor和TRowset完成的。这就是现代policy模式。
基于这个结构,我们可以很容易地想到,只要把TAccessor和TRowset用我们新增的policy约束起来,便可以达成目的。说做就做,我把CommandExt的定义改成如下结构:
template<
typenameAccessorT,
template<typenameA>classRowsetT=CRowset,
template<typenameR>classCursorT=DefaultCursor,
template<typenameR>classReadWriteT=ReadOnly,
template<typenameR>classBookMarkT=NoBookMark,
template<typenameA>classConvertorT=AutoConvertor,
typenameMultiResultT=CMultipleResults
>
classCommandExt
:publicCCommand<
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,并把它们包装在一个类中:
structReadOnly
{
//用于约束Rowset的部分
template<typenameRowsetT>
classForRowset:publicRowsetT
{
private:
HRESULTDelete();
HRESULTInsert();
HRESULTSetData();
HRESULTUndo();
HRESULTUpdate();
HRESULTUpdateAll();
};
//用于约束Accessor的部分
template<typenameAccessorT>
classForAccessor:publicAccessorT
{
private:
boolSetLength();
boolSetStatus();
boolSetValue();
};
//CManualAccessor有其特殊性,对其特化,不做约束
template<>classForAccessor<CManualAccessor>
:publicCManualAccessor{};
};
这样,可以把CommandExt继承类改成:
:publicCCommand<
typenameBookMarkT::ForAccessor<
typenameReadWriteT::ForAccessor<
ConvertorT<AccessorT>>>,
typenameBookMarkT::ForRowset<
typenameReadWriteT::ForRowset<
CursorT<RowsetT<AccessorT>>>>,
MultiResultT
>
但这依然无法通过编译,因为同样存在第二个问题。为解决这个问题,我做了一个辅助模板:
template<
template<typenameA>classRowsetT,
template<typenameR>classCursorT,
typenameReadWriteT,
typenameBookMarkT
>
structRowsetExt
{
template<typenameA>
classType
:publicReadWriteT::ForRowset<
typenameBookMarkT::ForRowset<
CursorT<RowsetT<A>>>>
{};
};
这里,类模板的基类就是前面CCommand的第二个模板实参。这样,便构造出一个符合Rwoset要求的模板。其实,最完美的方案是使用C++0x的template alias(也就是template typedef):
template<…>
structRowsetExt
{
template<typenameA>
usingType=ReadWriteT::ForRowset<
typenameBookMarkT::ForRowset<
CursorT<RowsetT<A>>>>;
};
当然啦,目前我们也只能将就了。为了保持形式统一,我也为Accessor做了一个辅助模板:
template<
typenameAccessorT,
typenameReadWriteT,
typenameBookMarkT,
template<typenameA>classConvertorT
>
structAccessorExt
{
classType
:publicReadWriteT::ForAccessor<
typenameBookMarkT::ForAccessor<
ConvertorT<AccessorT>>>
{};
分享到:
相关推荐
这两条主线被认为是穿越行业波动,寻找稳定收益的重要途径。 首先,煤价弹性将持续收窄。2020年是供给侧改革的关键阶段,政策重心由“去总量”转向“调结构”。这预示着煤炭行业的产能调整将更加精细,供需关系将...
以广西柳州螺蛳粉为例,通过网络平台的推广,螺蛳粉从地方特色美食逐渐演变成为全国皆知的“网红”食品,并且实现了产业化发展。在农产品区域品牌建设中,互联网发挥了巨大的作用,主要体现在以下几个方面: 首先,...
2021螺蛳粉行业发展白皮书.pdf 本报告是关于2021螺蛳粉行业发展的白皮书,涵盖了螺蛳粉行业的发展趋势、消费趋势、产品深耕、市场分析等多个方面。报告首先对螺蛳粉行业的发展趋势进行了分析,表明了螺蛳粉消费人数...
"2020柳州电商及螺蛳粉行业线上市场研究报告.pdf" 柳州电商市场分析 柳州电商市场的整体规模和增长情况显示,柳州淘宝天猫平台线上销售额呈波动性增长趋势,2019.4-2020.3近1年销售额达22.7亿元,尤其是2020年2、3...
3. “螺蛳壳里做道场”是上海方言,比喻在极其有限的空间里做出一番事业或者布置得很精美。在这篇文章中,它用来形容上海人如何巧妙地利用狭小的亭子间,使其成为精致的生活或工作空间,体现了上海人的智慧和创造力...
项目选择了餐饮业作为切入点,特别是广西柳州的特色小吃——螺蛳粉,旨在打造东莞的螺蛳粉精品。计划书将提供全面的评估和行动指导,以便进行风险投资。 二、产品及服务 项目的核心产品是螺蛳粉,一种结合酸、辣、...
最后,螺蛳粉产业要想持续做为网红产品,实现长远发展,还需要在政策扶持、资本加持、产业升级、渠道助推、消费群体拓展这五个环节上不断努力,使其能够环环相扣、互融共通。面对激烈的市场竞争和不断变化的消费需求...
《螺蛳粉投资商业计划书》是一份详细阐述螺蛳粉产业投资的文档,旨在为潜在投资者提供全面的行业分析、市场前景、经营策略及财务预测。这份商业计划书不仅揭示了螺蛳粉这一美食的独特魅力,同时也展示了其在餐饮市场...
【螺蛳粉投资商业计划书】是一份详细阐述在东莞开设螺蛳粉餐饮店的商业策划,旨在通过投资实现盈利并打造柳州螺蛳粉的品牌。以下是该计划书的关键点: 1. **项目概述**:强调了创业的风险与回报,选择餐饮业作为...
做过测试工作的人或许都知道,...所幸的是,微软的工程师们总能绞尽脑汁,螺蛳壳里做道场,榨干每一行代码、每一个测试用例的价值,最终把“杯具”变成“洗具”。下面就给大家讲一个我参与的项目中利用策略模式(Strat
《螺蛳粉商业计划书》是一份详细的创业指导文档,主要针对想要在餐饮行业中开设螺蛳粉店的企业家。这份计划书涵盖了多个关键领域,包括市场分析、产品定位、运营策略、财务预算以及风险评估,旨在为创业者提供全面的...
【螺蛳粉商业计划书】是一份详细的创业投资项目文档,主要涵盖了从项目概述到财务预算等多个关键环节。项目的核心是开设一家以螺蛳粉为主打产品的餐饮店,旨在利用餐饮行业的消费潜力,尤其是针对广东、广西等地喜好...
《螺蛳粉投资商业计划书》是一份详细阐述如何在螺蛳粉行业中开展创业和投资的文档,通常包括市场分析、产品介绍、营销策略、运营规划、财务预算以及风险评估等多个方面。以下是对这份计划书可能包含的关键知识点的...
螺蛳粉投资商业计划书.pdf
该文主要探讨了基于BP神经网络算法对网红食品——柳州螺蛳粉的销量预测和影响因素分析。文章指出,柳州螺蛳粉作为网红食品,其市场表现受到多种因素影响,包括价格、库存、销售数据、用户评价等。作者通过爬取淘宝网...
- **项目定位**:项目旨在打造一款独具特色的螺蛳粉品牌——“东莞螺蛳粉”,强调其独一无二的口味体验。 ### 二、项目产品及服务 - **主要产品**:螺蛳粉作为主打产品,以其独特的酸、辣、鲜、爽、烫风味著称。...
螺蛳室内养殖技术是一项经济高效的水产养殖方法,尤其适合家庭或小型养殖场操作。养殖螺蛳首先需要准备一个适宜的环境,通常选择鱼缸或水缸作为养殖容器。在底部铺设20cm厚的池塘淤泥是关键,因为螺蛳喜欢在富含...
2023 年中国螺蛳粉行业发展现状及消费行为分析报告 本报告对2023年中国螺蛳粉行业发展现状进行了深入分析,并对消费行为进行了研究。本报告首先对螺蛳粉行业的发展现状进行了概述,包括螺蛳粉行业的发展驱动因素、...