分享
 
 
 

Guru of the Week 条款15:类之间的关系(下篇)

王朝other·作者佚名  2006-01-08
窄屏简体版  字體: |||超大  

GotW #15 Class Relationships Part II

著者:Herb Sutter

翻译:kingofark

[声明]:本文内容取自www.gotw.ca网站上的Guru of the Week栏目,其著作权归原著者本人所有。译者kingofark在未经原著者本人同意的情况下翻译本文。本翻译内容仅供自学和参考用,请所有阅读过本文的人不要擅自转载、传播本翻译内容;下载本翻译内容的人请在阅读浏览后,立即删除其备份。译者kingofark对违反上述两条原则的人不负任何责任。特此声明。

Revision 1.0

Guru of the Week 条款15:类之间的关系(下篇)

难度:6 / 10

(设计模式是编写可复用代码的一个重要工具。你能辨认出本条款中的代码所用到的模式吗?你能改进它吗?)

[问题]

一个操纵数据库的程序经常需要在一个给定的表(table)中对一条或多条纪录(record)施以一定的操作。这一般涉及到两个连续的过程:首先以只读方式游访(pass through)整个表以搜集信息,确定哪些纪录需要被操纵;然后再对表进行第二次游访,实施真正的操作。

为了避免每次重复的编写那些惯常使用的操作代码,一个程序员试图通过下面的抽象类来提供一个通用的可复用框架(framework)。他希望抽象类能通过如下方式来封装那些重复的代码:首先,生成一个清单(list),用来记录表中需要被处理的那些记录行(record row);其次,对清单中的每个表项进行相应的处理。各种特定的处理代码细节由各个派生类自己实现。

//---------------------------------------------------

// gta.h 文件

//---------------------------------------------------

class GenericTableAlgorithm {

public:

GenericTableAlgorithm( const string& table );

virtual ~GenericTableAlgorithm();

// Process() 如果执行成功就返回true.

// 它做了所有的工作,包括: a) physically reads

// a)从物理设备上读取表中的记录,然后对每一条记录调用Filter()

// 来检查其是否就是需要被处理的记录;

// b)当创建好需要被处理的记录的清单后,对每一条需要被处理的记录

// 调用ProcessRow()。

bool Process();

private:

// 如果当前记录就是需要被处理的记录,Filter() 就返回true。

// 缺省的行为是将表中的所有记录都包括进去。

virtual bool Filter( const Record& ) {

return true;

}

// 对每一条需要被处理的记录,ProcessRow()被调用一次。

// 这正是实际使用的特定的类中进行其特定的操作的地方。

// (注意:可以看出,每一条记录前前后后被读取了两次。

// 这里我们假设出现这种情况是必要的,而不是一个效率上的问题。)

virtual bool ProcessRow( const PrimaryKey& ) =0;

class GenericTableAlgorithmImpl* pimpl_; // MYOB

};

这个类的使用者从其派生出一个类,可能会像下面这样编写使用代码:

class MyAlgorithm : public GenericTableAlgorithm {

// ... 在这里覆写Filter()和ProcessRow(),进行一些

// 特定的具体操作...

};

int main( int, char*[] ) {

MyAlgorithm a( "Customer" );

a.Process();

}

现在有3个问题:

1. 这是一个不错的设计,它实现了一种常用的设计模式(design patterns)。请问,这里使用的是什么模式?为什么这种模式可以用在这里?

2. 在不改变其本设计的情况下,评估这种设计被实际执行的方式。你能采用一些与其不一样的方式吗?pimpl_成员是为什么而设计的?

3. 实际上,这个设计可以进行较大的改进。GenericTableAlgorithm所担负的责任是什么?如果其担负的责任多于一个,那么这些责任所包含的操作应该如何被更好的封装起来?说明你采用的方法是如何影响类的可复用性的,特别是类的可扩展性这方面。

[解答]

1. 这是一个不错的设计,它实现了一种常用的设计模式(design patterns)。请问,这里使用的是什么模式?为什么这种模式可以用在这里?

这种模式叫做Template Method(可别跟C++中的template模板搞混淆了)。[注1] 这种设计模式非常有用,因为我们由此可以从算法中提取出那些每次都要进行的步骤,将其抽象出来,只把一些因地制宜的细节留给派生类来实现。

(注意:pimpl_惯用法与

Bridge方法非常相似[注1],但在这里,它只是作为一种对抗编译依赖性的防火墙而存在;它将各个特定类的具体实现细节隐藏起来,其在运作的时候与真正的具有可扩展性的

bridge还不太一样。)

2. 在不改变其本设计的情况下,评估这种设计被实际执行的方式。你能采用一些与其不一样的方式吗?pimpl_成员是为什么而设计的?

这个设计里面使用bool变量作为返回值,同时也丧失了使用其它方法——例如状态码(status code)或者异常处理——来进行错误报告(error reporting)的能力。也许根据依照某些特定的需求来考虑的时候,这样做是不错的,但一般我们还是应该认识并注意到这一点。

那个(不太容易发音的)pimpl_成员很好的将实现细节隐藏在了一个神秘的指针后面。pimpl_所指向的结构包含了私有成员函数和成员变量。这样一来,对他们进行任何改变,都不用重新编译用户代码(client code)。这正是Lakos等人[注2]所描述的一种很重要的技术。之所以说很重要,是因为这种技术在不给代码带来过多的复杂性和干扰的情况下,从一定程度上弥补了C++缺少模块系统(module system)的不足。

3. 实际上,这个设计可以进行较大的改进。GenericTableAlgorithm所担负的责任是什么?如果其担负的责任多于一个,那么这些责任所包含的操作应该如何被更好的封装起来?

GenericTableAlgorithm还可以进行较大的改进,因为他现在还是身兼二职。这就跟普通人在身兼二职时需要承受额外的负担一样,压力会很大。所以我们可以想见,缓解和改变GenericTableAlgorithm这种身兼二职、一心两用的状况,一定会对类自身大有好处。

在原始代码中,GenericTableAlgorithm担负着两个完全不同且毫不相关的责任。这两个责任完全可以被有效的分离开来,这是因为它们面向着不同的作用对象。简单的说,这两种责任是:

(1) 用户代码(client code)使用特定的通用算法(generic algorithm);

(2) 针对特定的实际情况,GenericTableAlgorithm会使用具有特定实现细节的类来使其操作特殊化(specialize)。

好,该说的说完了,现在我们来看看改进之后的代码:

//---------------------------------------------------

// gta.h文件

//---------------------------------------------------

// 责任#1: 提供一个公共接口,使其能够将常用的功能作为

// template method进行封装。这与继承关系无关,并可以

// 在一个实现特定功能的类中被很好的孤立起来。这是一个面向

// GenericTableAlgorithm的外部用户(external users)

// 的接口。

class GTAClient;

class GenericTableAlgorithm {

public:

// 构造函数现在获取了一个有具体实现的对象。

GenericTableAlgorithm( const string& table,

GTAClient& worker );

// 由于我们把继承关系隔离了起来,因此析构函数不必是virtual的。

// 事实上,我们也许压根儿就不需要它。

~GenericTableAlgorithm();

bool Process(); // 这一行不变

private:

class GenericTableAlgorithmImpl* pimpl_; // MYOB

};

//---------------------------------------------------

// gtaclient.h文件

//---------------------------------------------------

// 责任 #2: 为可扩展性提供了一个抽象接口。在这里,

// GenericTableAlgorithm的实现细节与外部用户代码无关,

// 并且可以被隔离到一个作用更明确的抽象协议类中去。

// 这里的接口是面向那些利用GenericTableAlgorithm 来编写

//可被实际使用的类的代码编写者。

class GTAClient {

public:

virtual ~GTAClient() =0;

virtual bool Filter( const Record& ) {

return true;

}

virtual bool ProcessRow( const PrimaryKey& ) =0;

};

可以看到,上面的两个类需要放在不同的头文件里面。那么在经过了这些改变之后,用户代码(client code)又可能会是什么样子的呢?答案是,用户代码(client code)基本没有变化,与原来的几乎一样:

class MyWorker : public GTAClient {

// ... 在这里覆写Filter()和ProcessRow(),进行一些

// 特定的具体操作...

};

int main( int, char*[] ) {

GenericTableAlgorithm a( "Customer", MyWorker() );

a.Process();

}

尽管代码样子没怎么变,但是必须考虑改进之后产生的如下三个效果:

1. 如果GenericTableAlgorithm的公共接口改变了会怎么样?结果是:在原始的版本中,所有具体的用户端的类都需要被重新编译,这是因为它们都派生自GenericTableAlgorithm;而在改进的版本中,对GenericTableAlgorithm公共接口的任何改变都被很好的孤立起来了,并不会影响用户端所使用的具体的类。

2. 如果GenericTableAlgorithm的可扩展协议被改变了会怎么样(比如Filter()或Processrow()里增加了新的缺省参数)?结果是:在原始的版本中,即使GenericTableAlgorithm公共接口没有任何改变,所有使用GenericTableAlgorithm的外部代码都必须被重新编译。这是因为,一个派生接口(derivation interface)在类定义中是可见的。而在改进的版本中,对GenericTableAlgorithm扩展协议接口的任何改变都被很好的孤立起来了,并不影响外部的用户代码。

3. 在改进的版本中,任何具体被使用的类可以在任何以Filter()或Processrow()为接口的算法中被使用,而不仅仅限于GenericTableAlgorithm中。

其实,我们在改进的代码中使用了与Strategy Pattern[注1]极为相似的模式(pattern)。

要记住计算机科学领域中的一句格言:Most any problem can be solved by adding a level of indirection(大部分问题可以通过增加间接层次即间接性来解决)。当然,同时考虑“奥卡的剃刀(Occam's Razor)” 原则也是很明智的。“奥卡的剃刀(Occam's Razor)”原则说道:Don't multiply entities more than necessary(不要做超出需求的额外举动)。把握好这两者之间的平衡关系,可以使你在花费很少甚至免费的情况下,增强代码的可复用性和可维护性——这无论如何都是一笔划算的买卖。

你也许注意到了,GenericTableAlgorithm其实完全可以是一个函数(实际上,有些人会把Process()改称为operator()(),这是由于此时的类很明显的只是一个functor(函算符)而已)。这里的类之所以可以替换成函数,是因为这里并没有说明在调用Process()的前后需要保存状态。例如我们可以把代码替换成这样:

bool GenericTableAlgorithm(

const string& table,

GTAClient& method ) {

// ... 原来的Process() 放在在这里...

}

int main( int, char*[] ) {

GenericTableAlgorithm( "Customer", MyWorker() );

}

这里的代码实际上就是一个通用函数(generic function),可以根据实际需要将其特殊化(specialized)。如果你发现“method”对象并不需要保存状态信息(),你就可以使“method”对象成为一个non-class template parameter(非class的模板参数):

template<typename GTACworker>

bool GenericTableAlgorithm( const string& table ) {

// ... 原来的Process() 放在在这里...

}

int main( int, char*[] ) {

GenericTableAlgorithm<MyWorker>( "Customer" );

}

这一个函数版本只比上面那个少了一个逗号。当然,在本条款所讨论的问题里面,少这一个逗号并不会给你带来多大的好处,因此第一个函数或许更好些。毕竟,能够抵挡住诱惑,不去编写这样一些以炫耀为目的的蹊跷的代码,总是一件好事。

无论如何,选择使用函数实现还是使用类实现完全取决于你要达到的目的。在本条款的这个问题中,使用函数实现比较好。

[注1]:E. Gamma et al., Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1995)。(中文版:《设计模式:可复用面向对象软件的基础》)

[注2]:J. Lakos. Large-Scale C++ Software Design (Addison-Wesley, 1996)。

(完)

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有