【问题标题】:How to implement an interface class using the non-virtual interface idiom in C++?如何使用 C++ 中的非虚拟接口习语实现接口类?
【发布时间】:2011-02-13 16:49:39
【问题描述】:

在 C++ 中,接口可以由其方法是纯虚拟的类来实现。

这样的类可以是库的一部分,用于描述对象应该实现哪些方法才能与库中的其他类一起工作:

class Lib::IFoo
{
    public:
        virtual void method() = 0;
};

class Lib::Bar
{
    public:
        void stuff( Lib::IFoo & );
};

现在我想使用Lib::Bar类,所以我必须实现IFoo接口。

出于我的目的,我需要一整套相关的类,所以我想使用一个基类来保证使用 NVI 惯用语的常见行为:

class FooBase : public IFoo // implement interface IFoo
{
    public:
        void method(); // calls methodImpl;

    private:
        virtual void methodImpl();
};

非虚拟接口 (NVI) 习惯用法应该拒绝派生类覆盖在 FooBase::method() 中实现的常见行为的可能性,但由于 IFoo 使其成为虚拟,似乎所有派生类都有机会覆盖FooBase::method()

如果我想使用 NVI 成语,除了已经建议的 pImpl 成语之外,我还有哪些选择(感谢 space-c0wb0y)。

【问题讨论】:

    标签: c++ interface idioms non-virtual-interface


    【解决方案1】:

    我认为你的 NVI 模式是错误的: http://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Non-Virtual_Interface

    不确定这是否能解决您的问题。

    class IFoo
    {
        public:
           void method() { methodImpl(); }
        private:
           virtual void methodImpl()=0;
    };
    
    class FooBase : public IFoo // implement interface IFoo
    {
        private:
            virtual void methodImpl();
    };
    

    下面是一个示例,说明为什么您可以使用一个读取 XML 和另一个读取 DB 的读取器来执行此操作。请注意,通用结构被移动到 NVI readFromSource 中,而非通用行为被移动到私有虚拟 getRawDatum 中。这种方式只需要在一个函数中进行日志记录和错误检查。

    class IReader
    {
      public:
        // NVI
        Datum readFromSource()
        {
           Datum datum = getRawDatum();
           if( ! datum.isValid() ) throw ReaderError("Unable to get valid datum");
           logger::log("Datum Read");
           return datum;
        }
      private:
        // Virtual Bits
        Datum getRawDatum()=0;
    };
    
    class DBReader : public IReader
    {
      private:
        Datum getRawDatum() { ... }
    };
    
    class XmlReader : public IReader
    {
       private:
         Datum getRawDatum() { ... }
    };
    

    【讨论】:

    • 我想使用一个接口类规定该接口的任何实现都应提供的方法,以及一个保证其行为的一些共性的基类通过使用 NVI 成语(又名模板方法)派生类。例如,我可以有两个完全不同的基类,一个与 XML 文件接口,一个与 RDBMS 接口。它们实现了 IFoo,并且必须保证派生类的行为有一些共性。
    • @andreas:在 C++ 中,接口是通过使用声明(可能是纯)虚函数的(可能是抽象的)基类来描述的。派生类然后通过覆盖它们来实现这些虚函数。基类的任何虚成员函数都可以在任何派生类中被覆盖。因此,NVI 只能在 base 类中实现,通过实现 non-virtual 方法。如果你想要一个两层的方法,简单的 NVI 是行不通的,因为从公共基类(从接口派生)派生的类可以覆盖所有接口的虚函数。
    • @andreas 我添加了一个示例,该示例执行您在评论中描述的内容。希望这是有道理的。我认为您想要的不止于此..但我不确定到底是什么。也许您应该通过以下完整示例进一步扩展您的问题:1) 您希望能够做什么,和/或 2) 正常 NVI 出了什么问题?
    【解决方案2】:

    通常,使用 NVI(有时也称为“模板方法”)的原因是派生类应该只更改基类行为的一部分。所以你要做的是:

    class base {
      public:
        void f()
        {
          // do something derived classes shouldn't interfere with          
          vf();
          // do something derived classes shouldn't interfere with          
          vg();
          // do something derived classes shouldn't interfere with          
          vh();
          // do something derived classes shouldn't interfere with          
        }
      private:
        virtual void vf(); // might be pure virtual, too
        virtual void vg(); // might be pure virtual, too
        virtual void vh(); // might be pure virtual, too
    };
    

    然后,派生类可以插入到f() 的应有位置并更改f() 的行为的方面,而不会弄乱其基本算法。

    【讨论】:

    • 完全正确,但是我怎样才能让基类实现接口而不失去我的模板方法提供的保证(例如关于 vf、vg 和 vh 的执行顺序)?跨度>
    • @andreas:我已经回复了您对迈克尔回答的评论。我建议您扩展您的问题,以便更好地描述您想要做什么。据我了解,也许混合或策略可能是您正在寻找的。​​span>
    • NVI和模板方法是完全不同的概念;尽管 NVI 是模板方法的典型实现方式。您可以简单地拥有一个使用策略对象作为虚拟对象的模板方法,也可以简单地拥有一个不用于模板方法模式的 NVI 模式。
    • +1 简单而小巧的代码完美地说明了为什么要使用 NVI 模式。
    【解决方案3】:

    如果一个方法在基类中被声明为 virtual,它会在所有派生类中自动变为 virtual,即使在那里没有使用 virtual 关键字,这可能会让人感到困惑。所以在你的例子中,FooBase 的两种方法都是虚拟的。

    ... 拒绝派生类 覆盖共同点的可能性 实现的行为 FooBase::method()...

    如果您可以摆脱IFoo,只需使用非虚拟methodFooBase 开始层次结构,那就可以了。但看起来您希望允许IFoo 的直接子代覆盖method(),但要防止FooBase 的子代覆盖它。我认为这是不可能的。

    【讨论】:

      【解决方案4】:

      您可以使用 pimpl-idiom 来实现:

      class IFoo
      {
          public:
              IFoo( boost::shared_ptr< IFooImpl > pImpl )
                  : m_pImpl( pImpl )
              {}
      
              void method() { m_pImpl->method(); }
              void otherMethod() { m_pImpl->otherMethod(); }
          private:
              boost::shared_ptr< IFooImpl > m_pImpl;
      };
      
      class IFooImpl
      {
          public:
              void method();
              virtual void otherMethod();
      };
      

      现在其他人仍然可以继承IFooImpl 并将其传递给IFoo,但他们不能覆盖method 的行为(他们可以覆盖otherMethod)。您甚至可以将IFooImpl 设为IFoo 的直接子类,并使用enable_shared_from_this 正确初始化IFoo。这只是该方法的要点。有很多方法可以调整这种方法。例如,您可以使用factory-pattern 来确保正确创建IFoos。

      希望对您有所帮助。

      【讨论】:

      • 嗯.. 这似乎需要很多开销和机制来避免拥有私有虚拟功能......
      • 问题是任何虚函数,即使是私有的,都可以被子类覆盖。这个选项确实有很多样板,没错,但它是防弹的。所以这是一个权衡。
      猜你喜欢
      • 2010-11-19
      • 1970-01-01
      • 2014-03-14
      • 2011-01-21
      • 1970-01-01
      • 2019-02-11
      • 1970-01-01
      • 1970-01-01
      • 2011-09-22
      相关资源
      最近更新 更多