C#/C++中的非虚拟接口设计模式

34

在设计接口时,有人建议使用非虚拟接口模式。请简要概述一下这种模式的好处是什么?


哪种非虚拟接口模式? - Nicol Bolas
2个回答

63

非虚拟接口模式的本质是有私有虚拟函数,这些函数由公共非虚拟函数(即非虚拟接口)调用。

优点是基类对其行为有更多的控制,而不是派生类能够覆盖其界面的任何部分。换句话说,基类(接口)可以提供更多关于其功能的保证。

以一个简单的例子来说,考虑一个具有几个典型派生类的经典动物类:

class Animal
{
public:
    virtual void speak() const = 0;
};

class Dog : public Animal
{
public:
    void speak() const { std::cout << "Woof!" << std::endl; }
};

class Cat : public Animal
{
public:
    void speak() const { std::cout << "Meow!" << std::endl; }
};

这个代码使用了我们习惯的公共虚拟接口,但有一些问题:

  1. 每个派生动物都在重复相同的代码 -- 唯一变化的只是字符串,然而每个派生类都需要整个的 std::cout << ... << std::endl; 代码。
  2. 基类无法保证 speak() 函数的行为。一个派生类可能会忘记换行符,或者将其写到 cerr 或任何其他地方。

要解决这个问题,可以使用非虚拟接口,并补充一个私有虚拟函数以实现多态行为:

class Animal
{
public:
   void speak() const { std::cout << getSound() << std::endl; }
private:
   virtual std::string getSound() const = 0;
};

class Dog : public Animal
{
private:
   std::string getSound() const { return "Woof!"; }
};

class Cat : public Animal
{
private:
   std::string getSound() const { return "Meow!"; }
};

现在,基类可以保证将输出到std::cout并以新行结束。这也使得维护更容易,因为派生类不需要重复该代码。

Herb Sutter撰写了一篇有关非虚接口的好文章,我推荐阅读。


1
你是否忘记了虚析构函数或者为了简洁而省略了它们,或者在这种情况下(层次结构中没有数据)可以忽略它们? - Alexander Malakhov
谢谢您的解释。我明白了。 - Eddy Ekofo
非常好的解释。谢谢。 - Baterka
我还没有阅读过所链接的文章,但我很快就会阅读。不过,我认为这个答案需要解决以下反对意见(这是我自己的真实想法):问题2是,在代码的第一个版本中,派生类可以做“任何他们想做的事情”;在第二个版本中,他们在return语句之前仍然可以做“任何他们想做的事情”,这是否仍然成立?我猜测,这个问题的答案很简单,即NVI模式并不旨在保护免受故意的糟糕设计选择的影响,但如果您能发表一下您的看法,那将是很好的。 - Enlico

4
这里有一篇维基百科文章,其中详细介绍了一些例子。其核心是,您可以在基类的中心位置确保重要条件(如获取和释放锁),同时仍允许通过使用私有或受保护的虚函数派生出不同的实现。
类层次结构的任何类的用户始终会调用公共接口,该接口将调用不可外部看到的实现。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接