随笔-57  评论-12  文章-0  trackbacks-0
[译著]在模板方法中的一些"反常"用法

作者: Jerry Cat
时间: 2006/05/19
链接: 
http://www.cppblog.com/jerysun0818/archive/2006/05/19/7393.html

-------------------------------------
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模式。public成员函数read和write是非虚拟的,它们肯定是调用protected部分do_read/do_write虚拟成 员函数来完成实际的工作。啊,我简直为自己的进步而飘飘然了!哈,Wendy,这回你可难不住我,还有什么招数?尽管放马过来... 突然,笑容在我脸上凝固,因为我看到了其private部分:

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_write()是protected的?”

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

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

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

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

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

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

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

“同 样的,virtual函数的访问控制级别也具有隐含的意义。一个protected virtual function告诉你:‘你写的派生类应该,哦,可是说是必须调用我的实现。’而一个private 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模式中。当然,template method是一种非常有用和常用的模式,因此也决定了本文提出的原则具有广泛的意义。]

posted on 2006-05-19 12:18 Martin 阅读(491) 评论(0)  编辑 收藏 引用 所属分类: C & C++
只有注册用户登录后才能发表评论。