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

类别:编程语言 点击:0 评论:0 推荐:

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)。

(完)

本文地址:http://com.8s8s.com/it/it29862.htm