Как реализовать класс интерфейса, используя идиому невиртуального интерфейса в С++?

В 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).


person andreas buykx    schedule 29.04.2010    source источник


Ответы (4)


Я думаю, что у вас неправильный шаблон 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, а другой из БД. Обратите внимание, что общая структура перемещена в readFromSource NVI, а нестандартное поведение перемещено в частный виртуальный 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() { ... }
};
person Michael Anderson    schedule 29.04.2010
comment
Я хотел бы работать с интерфейсным классом, который только предписывает методы, которые должна предлагать любая реализация этого интерфейса, и базовым классом, который гарантирует некоторую общность в поведении своих производных классов, используя идиому NVI ( он же шаблонный метод). У меня могло бы быть два совершенно разных базовых класса, один, который взаимодействует с файлом XML, а другой, например, взаимодействует с СУБД. Они реализуют IFoo и должны гарантировать некоторую общность в поведении производных классов. - person andreas buykx; 29.04.2010
comment
@andreas: В C ++ интерфейсы описываются с использованием (возможно, абстрактных) базовых классов, которые объявляют (возможно, чистые) виртуальные функции. Затем производные классы реализуют эти виртуальные функции, переопределяя их. Любая виртуальная функция-член базового класса может быть переопределена в любых производных классах. Поэтому NVI может быть реализован только в базовом классе путем реализации невиртуальных методов. Если вам нужен двухуровневый подход, простой NVI не подойдет, поскольку классы, производные от общих базовых классов (которые являются производными от интерфейса), могут переопределить все виртуальные функции интерфейса. - person sbi; 29.04.2010
comment
@andreas Я добавил пример, который делает то, что вы описываете в своем комментарии. Надеюсь, это имеет смысл. Я думаю, что вы хотите большего, чем это... но я не уверен, что именно. Возможно, вам следует расширить свой вопрос, приведя полный пример: 1) что вы хотели бы уметь делать и/или 2) что не так с обычным NVI? - person Michael Anderson; 30.04.2010

Обычно причиной использования 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(), не нарушая его основного алгоритма.

person sbi    schedule 29.04.2010
comment
Совершенно верно, но как я могу заставить базовый класс реализовать интерфейс, не теряя при этом гарантий (о порядке выполнения vf, vg и vh, например), которые предлагает мой шаблонный метод? - person andreas buykx; 29.04.2010
comment
@andreas: я ответил на ваш комментарий к ответу Майкла. Я предлагаю вам расширить свой вопрос, чтобы он немного лучше описывал то, что вы хотите сделать. Насколько я понимаю, возможно, вы ищете смешанные варианты или политики. - person sbi; 29.04.2010
comment
NVI и метод шаблона — совершенно разные понятия; хотя NVI - это то, как обычно реализуется метод шаблона. Вы могли бы так же просто иметь метод шаблона, который использует объект стратегии для своих виртуальных машин, и вы могли бы просто иметь шаблон NVI, который не предназначен для шаблона метода шаблона. - person Billy ONeal; 30.04.2010
comment
+1 за простой и небольшой код, который прекрасно иллюстрирует, почему можно использовать шаблон NVI. - person WebF0x; 15.02.2014

Может сбивать с толку тот факт, что как только метод объявлен виртуальным в базовом классе, он автоматически становится виртуальным во всех производных классах, даже если там не используются ключевые слова virtual. Итак, в вашем примере оба метода FooBase являются виртуальными.

... чтобы запретить производным классам возможность переопределения общего поведения, реализованного в FooBase::method()...

Если вы можете избавиться от IFoo и просто начать иерархию с FooBase с невиртуальным method, это подойдет. Но похоже, что вы хотите разрешить прямым дочерним элементам IFoo переопределять method(), но запретить дочерним элементам FooBase переопределять его. Я не думаю, что это возможно.

person DS.    schedule 29.04.2010

Для этого вы можете использовать идиому pimpl:

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< /a> для правильной инициализации IFoo. Это только суть метода. Есть много способов изменить этот подход. Например, вы можете использовать фабричный шаблон, чтобы убедиться, что IFoo созданы правильно.

Надеюсь, это поможет.

person Björn Pollex    schedule 29.04.2010
comment
Хм... это похоже на много накладных расходов и механизмов, чтобы избежать частной виртуальной функции.... - person Billy ONeal; 30.04.2010
comment
Проблема в том, что любая виртуальная функция, даже приватная, может быть переопределена подклассами. В этом варианте действительно много шаблонов, но он пуленепробиваемый. Так что это компромисс. - person Björn Pollex; 30.04.2010