C_and_CPP 版 (精华区)

发信人: seaboy (浪小), 信区: C_and_CPP
标  题: 与大虾对话: 领悟设计模式 
发信站: 哈工大紫丁香 (2003年06月25日09:44:49 星期三), 站内信件

与大虾对话: 领悟设计模式 
myan(孟岩) 翻译 

-------------------------------------------------------------------------------
-

[译者按] 
本文根据发表在CUJ Expert Forum上的两篇文章编译而成。C/C++ User's Journal是目前
最出色的C/C++语言专业杂志,特别是在C++ Report闭刊之后,CUJ的地位更加突出。CUJ 
Expert Forum是CUJ主办的网上技术专栏,汇集2000年10月以来C++社群中顶尖专家的技术
短文,并免费公开发布,精彩纷呈,是每一个C/C++学习者不可错过的资料。由Jim 
Hyslop和Herb Sutter主持的Conversation系列,是CUJ Expert Forum每期必备的精品专栏
,以风趣幽默的对话形式讲解C++高级技术,在C++社群内得到广泛赞誉。译者特别挑选两
篇设计模式方面的文章,介绍给大家。设计模式方面的经典著作是GoF的Design Patterns
。但是那本书有一个缺点,不好懂。从风格上讲,该书与其说是为学习者而写作的教程范
本,还不如说是给学术界人士看的学术报告,严谨有余,生动不足。这一点包括该书作者
和象Bjarne Stroustrup这样的大师都从不讳言。实际上Design Pattern并非一定是晦涩难
懂的,通过生动的例子,一个中等水平的C++学习者完全可以掌握基本用法,在自己的编程
实践中使用,得到立竿见影的功效。这两篇文章就是很好的例证。本文翻译在保证技术完
整性的前提下作了不少删节和修改,以便使文章显得更紧凑。 


-------------------------------------------------------------------------------
-

人物介绍: 
我 --- 一个追求上进的C++程序员,尚在试用期,聪明但是经验不足。 

Wendy --- 公司里的技术大拿,就坐在我旁边的隔间里,C++大虾,最了不起的是,她是个
女的!她什么都好,就是有点刻薄, 

我对她真是又崇拜又嫉妒。 


-------------------------------------------------------------------------------
-

I. Virtually Yours -- Template Method模式 
我在研究Wendy写的一个类。那是她为这个项目写的一个抽象基类,而我的工作就是从中派
生出一个具象类(concrete class)。这个类的public部分是这样的: 

class Mountie {
public:
    void read( std::istream & );
    void write( std::ostream & ) const;
    virtual ~Mountie();

很正常,virtual destructor表明这个类打算被继承。那么再看看其protected部分: 

protected:
    virtual void do_read( std::istream & );
    virtual void do_write( std::ostream & ) const;

也不过就是一会儿的功夫,我识破了Wendy的把戏:她在使用template method模式。publi
c成员函数read和write是非虚拟的,它们肯定是调用protected部分do_read/do_write虚拟
成员函数来完成实际的工作。啊,我简直为自己的进步而飘飘然了!哈,Wendy,这回你可
难不住我,还有什么招数?尽管放马过来... 突然,笑容在我脸上凝固,因为我看到了其p
rivate部分: 

private:
    virtual std::string classID() const = 0;

这是什么?一个private纯虚函数,能工作么?我站了起来, 

“Wendy,你的Mountie类好像不能工作耶,它有一个private virtual function。” 

“你试过了?”她连头都不抬。 

“嗯,那倒是没有啦,可是想想也不行啊?我的派生类怎么能override你的private函数呢
?” 我嘟囔着。 

“嗬,你倒是很确定啊!”Wendy的声音很轻柔,“你怎么老是这也不行,那也不行的,这
几个月跟着我你就没学到什么东西吗?小菜鸟。” 

真是可恶啊... 

“小菜鸟,你全都忘了,访问控制级别跟一个函数是不是虚拟的根本没关系。判断一个函
数是动态绑定还是静态绑定是函数调用解析的最后一个步骤。好好读读标准的3.4和5.2.2
节吧。” 

我完全处于下风,只好采取干扰战术。“好吧,就算你说的不错,我也还是不明白,何必
把它设为private?” 

“我且问你,倘若你不想让一个类中的成员函数被其他的类调用,应当如何处理?” 

“当然是把它设置为private的,” 我回答道。 

“那么你去看看我的Mountie类实现,特别是write()函数的实现。” 

我正巴不得逃开Wendy那刺人的目光,便转过头去在我的屏幕上搜索,很快,我找到了: 

void Mountie::write(std::ostream &Dudley) const
{
    Dudley << classID() << std::endl;
    do_write(Dudley);
}

嗨,最近卡通片真是看得太多了,居然犯这样的低级失误。还是老是承认吧:“好了,我
明白了。classID()是一个实现细节,用来在保存对象时指示具象类的类型,派生类必须覆
盖它,所以必须是纯虚的。但是既然是实现细节,就应该设为private的。” 

“这还差不多,小菜鸟。”大虾点了点头,“现在给我解释一下为什么do_read()和do_wri
te()是protected的?” 

这个问题并不难,我组织了一下就回答:“因为派生类对象需要调用这两个函数的实现来
读写其中的基类对象。” 

“很好很好,”大虾差不多满意了,“不过,你再解释解释为什么我不把它们设为public
的?” 

现在我感觉好多了:“因为调用它们的时候必须以一种特定的方式进行。比如do_write()
函数,必须先把类型信息写入,再把对象信息写入,这样读取的时候,负责生成对象的模
块首先能够知道要读出来的对象是什么类型的,然后才能正确地从流中读取对象信息。” 


“聪明啊,我的小菜鸟!”Wendy停顿了一下,“就跟学习外国口语一样,学习C++也不光
是掌握语法而已,还必须要掌握大量的惯用法。” 

“是啊是啊,我正打算读Coplien的书...” 

[译者注:就是James Coplien 1992年的经典著作Advanced C++ Programming Style and 
Idioms] 

大虾挥了挥她的手,“冷静,小菜鸟,我不是指先知Coplien的那本书,我是指某种结构背
后隐含的惯用法。比如一个类有virtual destructor,相当于告诉你说:‘嗨,我是一个
多态基类,来继承我吧!’ 而如果一个类的destructor不是虚拟的,则相当于是在说:‘
我不能作为多态基类,看在老天的份上,别继承我。’” 

“同样的,virtual函数的访问控制级别也具有隐含的意义。一个protected virtual 
function告诉你:‘你写的派生类应该,哦,可是说是必须调用我的实现。’而一个priva
te virtual function是在说:‘派生类可以覆盖,也可以不覆盖我,随你的便。但是你不
可以调用我的实现。’” 

我点点头,告诉她我懂了,然后追问道:“那么public virtual function呢?” 

“尽可能不要使用public virtual function。”她拿起一支笔写下了以下代码: 

class HardToExtend 
{
public:
     virtual void f();
};
 void HardToExtend::f() 

    // Perform a specific action 
}

“假设你发布了这个类。在写第二版时,需求有所变化,你必须改用Template Method。可
是这根本不可能,你知道为什么?” 

“呃,这个...,不知道。” 

“由两种可能的办法。其一,将f()的实现代码转移到一个新的函数中,然后将f()本身设
为non-virtual的: 

class HardToExtend
{
// possibly protected
    virtual void do_f();
public:
    void f();
};
void HardToExtend::f()
{
    // pre-processing
    do_f();
    // post-processing
}
void HardToExtend::do_f()
{
    // Perform a specific action
}

然而你原来写的派生类都是企图override函数f()而不是do_f()的,你必须改变所有的派生
类实现,只要你错过了一个类,你的类层次就会染上先知Meyers所说的‘精神分裂的行径
’。” [译者注:参见Scott Meyers,Effective C++, Item 37,绝对不要重新定义继承
而来的非虚拟函数] 

“另一种办法是将f()移到private区域,引入一个新的non-virtual函数:” 

class HardToExtend
{
// possibly protected
    virtual void f();
public:
    void call_f();
};

“这会导致无数令人头痛的问题。首先,所有的客户都企图调用f()而不是call_f(),现在
它们的代码都不能编译了。更有甚者,大部分派生类都回把f()放在public区域中,这样直
接使用派生类的用户可以访问到你本来想保护的细节。” 

“对待虚函数要象对待数据成员一样,把它们设为private的,直到设计上要求使用更宽松
的访问控制再来调整。要知道由private入public易,由public入private难啊!” 

[译者注:这篇文章所表达的思想具有一定的颠覆性,因为我们太容易在基类中设置public
 virtual function了,Java中甚至专门为这种做法建立了interface机制,现在竟然说这
不好!一时间真是接受不了。但是仔细体会作者的意思,他并不是一般地反对public 
virtual function,只是在template method大背景下给出上述原则。虽然这个原则在一般
的设计中也是值得考虑的,但是主要的应用领域还是在template method模式中。当然,te
mplate method是一种非常有用和常用的模式,因此也决定了本文提出的原则具有广泛的意
义。] 


-------------------------------------------------------------------------------
-

II. Visitor模式 
我正在为一个设计问题苦恼。试用期快结束了,我希望自己解决这个问题,来证明自己的
进步。每个人都记得自己的第一份工作吧,也都应该知道在这个时候把活儿做好是多么的
重要!我亲眼看到其他的新雇员没有过完试用期就被炒了鱿鱼,就是因为他们不懂得如何
对付那个大虾...,别误会,我不是说她不好,她是我见过最棒的程序员,可就是有点刻薄
古怪...。现在我拜她为师,不为别的,就是因为我十分希望能达到她那个高度。 

我想在一个类层次(class hierarchy)中增加一个新的虚函数,但是这个类层次是由另外一
帮人维护的,其他人碰都不能碰: 

class Personnel
{
public:
  virtual void Pay ( /*...*/ ) = 0;
  virtual void Promote( /*...*/ ) = 0;
  virtual void Accept ( PersonnelV& ) = 0;
  // ... other functions ...
};

class Officer : public Personnel { /* override virtuals */ };
class Captain : public Officer { /* override virtuals */ };
class First : public Officer { /* override virtuals */ };

我想要一个函数,如果对象是船长(Captain)就这么做,如果是大副(First Officer)就那
么做。Virtual function正是解决之道,在Personnel或者Officer中声明它,而在Captain
和First覆盖(override)它。 

糟糕的是,我不能增加这么一个虚函数。我知道可以用RTTI给出一个解决方案: 

void f( Officer &o )
{
  if( dynamic_cast<Captain*>(&o) )
    /* do one thing */
  else if( dynamic_cast<First*>(&o) )
    /* do another thing */
}

int main()
{
  Captain k;
  First s;
  f( k );
  f( s );
}

但是我知道使用RTTI是公司编码标准所排斥的行为,我对自己说:“是的,虽然我以前不
喜欢RTTI,但是这回我得改变对它的看法了。很显然,除了使用RTTI,别无它法。” 

“任何问题都可以通过增加间接层次的方法解决。” 

我噌地一下跳起来,那是大虾的声音,她不知道什么时候跑到我背后,“啊哟,您吓了我
一跳...您刚才说什么?” 

“任何问...” 

“是的,我听清楚了,”我也不知道哪来的勇气,居然敢打断她,“我只是不知道您从哪
冒出来的。”其实这话只不过是掩饰我内心的慌张。 

“哈,算了吧,小菜鸟,”大虾斜着眼看着我,“你以为我不知道你心里想什么!”她把
声音提高了八度,直盯着我,“那些可怜的C语言门徒才会使用switch语句处理不同的对象
类型。你看:” 

/* A not-atypical C program */
void f(struct someStruct *s)
{
  switch(s->type) {
  case APPLE:
    /* do one thing */
    break;
  case ORANGE:
    /* do another thing */
    break;
  /* ... etc. ... */
  }
}

“这些人学习Stroustrup教主的C++语言时,最重要的事情就是学习如何设计好的类层次。
” 

“没错,”我又一次打断她,迫不及待地想让Wendy明白,我还是有两下子的,“他们应该
设计一个Fruit基类,派生出Apple和Orange,用virtual function来作具体的事情。 

“很好,小菜鸟。C语言门徒通常老习惯改不掉。但是,你应该知道,通过使用virtual 
function,你增加了一个间接层次。”她放下笔,“你所需要的不就是一个新的虚函数吗
?” 

“是的。可是我没有权力这么干。” 

“因为你无权修改类层次,对吧!” 

“您终于了解了情况,我们没法动它。也不知道这个该死的类层次是哪个家伙设计的...”
 我嘀嘀咕咕着。 

“是我设计的。” 

“啊...,真的?!这个,嘿嘿...”,我极为尴尬。 

“这个类层次必须非常稳定,因为有跨平台的问题。但是它的设计允许你增加新的virtual
 function,而不必烦劳RTTI。你可以通过增加一个间接层次的办法解决这个问题。请问,
Personnel::Accept是什么?” 

”嗯,这个...” 

“这个类实现了一个模式,可惜这个模式的名字起得不太好,是个PNP,叫Visitor模式。
” 

[译者注:PNP,Poor-Named Pattern, 没起好名字的模式] 

“啊,我刚刚读过Visitor模式。但是那只不过是允许若干对象之间相互迭代访问的模式,
不是吗?” 

她叹了一口气,“这是流行的错误理解。那个V,我觉得毋宁说是Visitor,还不如说是Vir
tual更好。这个PNP最重要的用途是允许在不改变类层次的前提下,向已经存在的类层次中
增加新的虚函数。首先来看看Personnel及其派生类的Accept实现细节。”她拿起笔写下:
 

void Personnel::Accept( PersonnelV& v )
  { v.Visit( *this ); }

void Officer::Accept ( PersonnelV& v )
  { v.Visit( *this ); }

void Captain::Accept ( PersonnelV& v )
  { v.Visit( *this ); }

void First::Accept ( PersonnelV& v )
  { v.Visit( *this ); }

“Visitor的基类如下:” 

class PersonnelV/*isitor*/
{
public:
  virtual void Visit( Personnel& ) = 0;
  virtual void Visit( Officer& ) = 0;
  virtual void Visit( Captain& ) = 0;
  virtual void Visit( First& ) = 0;
};

“啊,我记起来了。当我要利用Personnel类层次的多态性时,我只要调用Personnel::Acc
ept(myVisitorObject)。由于Accept是虚函数,我的myVisitorObject.Visit()会针对正确
的对象类型调用,根据重载法则,编译器会挑选最贴切的那个Visit来调用。这不相当于增
加了一个新的虚拟函数了吗?” 

“没错,小菜鸟。只要类层次支持Accept,我们就可以在不改动类层次的情况下增加新的
虚函数了。” 

“好了,我现在知道该怎么办了”,我写道: 

class DoSomething : public PersonnelV
{
public:
  virtual void Visit( Personnel& );
  virtual void Visit( Officer& );
  virtual void Visit( Captain& );
  virtual void Visit( First& );
};

void DoSomething::Visit( Captain& c )
{
  if( femaleGuestStarIsPresent )
    c.TurnOnCharm();
  else
    c.StartFight();
}

void DoSomething::Visit( First& f )
{
  f.RaiseEyebrowAtCaptainsBehavior();
}
void f( Personnel& p )
{
  p.Accept( DoSomething() ); // 相当于 p.DoSomething()
}

int main()
{
  Captain k;
  First s;

  f( k );
  f( s );
}

大虾满意地笑了,“也许这个模式换一个名字会更好理解,可惜世事往往不遂人意...”。
 

[译者注:这篇文章我作了一定的删节,原文中有稍微多一些的论述,而且提供了两篇技术
文章的link。] 
 
--
欢迎到C_and_CPP版讨论相关问题。

※ 来源:·哈工大紫丁香 bbs.hit.edu.cn·[FROM: 202.118.239.80]
[百宝箱] [返回首页] [上级目录] [根目录] [返回顶部] [刷新] [返回]
Powered by KBS BBS 2.0 (http://dev.kcn.cn)
页面执行时间:202.794毫秒