Управление различными классами с центральным менеджером без RTTI - PullRequest
1 голос
/ 26 февраля 2011

У меня есть вопрос о дизайне, который меня давно беспокоил, но я не могу найти хорошее (в смысле ООП) решение для этого.Язык C ++, и я продолжаю возвращаться к RTTI, который часто называют индикатором плохого дизайна.

Предположим, у нас есть набор модулей разных типов, реализованных как разные классы.Каждый тип модуля характеризуется определенным интерфейсом, однако реализация может отличаться.Таким образом, моей первой идеей было создать интерфейсный (чисто абстрактный) класс для каждого типа модуля (например, IModuleFoo, IModuleBar и т. Д.) И реализации в отдельных классах.Пока все хорошо.

class IModuleFoo {
  public:
    virtual void doFoo() = 0;
};

class IModuleBar {
  public:
    virtual void doBar() = 0;
};

С другой стороны, у нас есть набор (прикладных) классов, и каждый из них использует несколько таких модулей, но только через интерфейсы - даже сами модули могут использовать другиемодули.Однако все классы приложения будут использовать один и тот же пул модулей.Моя идея состояла в том, чтобы создать класс менеджера (ModuleManager) для всех модулей, классы приложений которого могут запрашивать нужные им типы модулей.Доступные модули (и конкретная реализация) устанавливаются во время инициализации менеджера и могут изменяться со временем (но это не является частью моего вопроса).

Поскольку число различных типов модулей наиболее вероятно> 10 и может увеличиваться со временем, мне кажется неуместным хранить ссылки (или указатели) на них отдельно.Кроме того, может быть несколько функций, которые менеджер должен вызывать во всех управляемых модулях.Таким образом, я создал другой интерфейс (IManagedModule) с тем преимуществом, что теперь я могу использовать контейнер (список, набор, что угодно) для IManagedModules, чтобы хранить их в диспетчере.

class IManagedModule {
  public:
    virtual void connect() = 0;
    { ... }
};

В результате модуль, которыйдолжен управляться должен наследоваться как от IManagedModule, так и от соответствующего интерфейса для его типа.

Но все становится ужасно, когда я думаю о ModuleManager.Можно предположить, что в каждый момент времени присутствует не более одного экземпляра каждого типа модуля.Таким образом, если бы можно было сделать что-то подобное (где manager является экземпляром ModuleManager), все было бы хорошо:

IModuleFoo* pFoo = manager.get(IModuleFoo);

Но я почти уверен, что это не так.Я также подумал о решении на основе шаблонов, например:

IModuleFoo* pFoo = manager.get<IModuleFoo>();

Это может сработать, но я понятия не имею, как найти нужный модуль в менеджере, если все, что у меня есть, это набор IManagedModules - то есть безиспользование RTTI, конечно.

Один из подходов состоит в том, чтобы предоставить IManagedModule виртуальный метод getId () , полагаться на реализации, использующие не неоднозначные идентификаторы для каждого типа модуля, и выполнять приведение указателей самостоятельно.,Но это просто изобретение колеса (а именно RTTI) и требует большой дисциплины в реализующих классах (предоставление правильных идентификаторов и т. Д.), Что нежелательно.

Короче говоря, вопрос в том, есть лина самом деле нет никакого способа обойти какой-то RTTI здесь, и в этом случае RTTI может даже быть допустимым решением или если может быть лучший (более чистый, безопасный, ...) дизайн, который демонстрирует такую ​​же гибкость (например, слабая связь между классами приложений)а модульные классы ...)?Я что-то пропустил?

Ответы [ 2 ]

1 голос
/ 26 февраля 2011

Звучит так, будто вы ищете что-то похожее на COM * QueryInterface .Теперь вам не нужно полностью реализовывать COM, но основной принцип стоит: у вас есть базовый класс с виртуальной функцией, которому вы передаете идентификатор, указывающий, какой интерфейс вы хотите.Затем виртуальная функция проверяет, может ли она реализовать этот интерфейс, и, если да, передает указатель на этот интерфейс.

Например:

struct IModuleBase {
    // names changed so as not to confuse later programmers with true COM
    virtual bool LookupInterface(int InterfaceID, void **interfacePtr) = 0;

    // Easy template wrapper
    template<typename Interface>
    Interface *LookupInterface() {
        void *ptr;
        if (!LookupInterface(Interface::INTERFACE_ID, &ptr)) return NULL;
        return (Interface *)ptr;
    }
};

struct IModuleFoo : public IModuleBase {
    enum { INTERFACE_ID = 42 };
    virtual void foo() = 0;
};

struct SomeModule : public IModuleFoo {
    virtual bool LookupInterface(int interface_id, void **pPtr) {
        switch (interface_id) {
            case IModuleFoo::INTERFACE_ID:
                *pPtr = (void*)static_cast<IModuleFoo *>(this);
                return true;
            default:
                return false;
        }
    }

    virtual void foo() { /* ... */ }
};

Это немного громоздко, ноэто не так уж плохо, и без RTTI у вас не будет большого выбора, кроме такого подхода.

0 голосов
/ 26 февраля 2011

Я думаю, предложение bdonlan хорошо, но требование каждого типа модуля для объявления отдельного INTERFACE_ID является головной болью при обслуживании. Отличительность может быть достигнута автоматически, если каждый тип модуля объявляет статический объект и использует его адрес в качестве идентификатора:

struct IModuleFoo : public IModuleBase {
    static char distinct_;        // Exists only to occupy a unique address
    static const void *INTERFACE_ID;
    virtual void foo() = 0;
};

// static members need separate out-of-class definitions
char IModuleFoo::distinct_;
const void *IModuleFoo::INTERFACE_ID = &distinct_;

В этом случае мы используем void * в качестве типа идентификатора интерфейса вместо int или перечислимого типа, поэтому типы в некоторых других объявлениях необходимо будет изменить.

Кроме того, из-за причуд в C ++ значения INTERFACE_ID, несмотря на пометку const, не являются «достаточно постоянными», чтобы их можно было использовать для меток case в операторах switch (или объявлениях размера массива, или горстка других мест), поэтому вам нужно изменить оператор switch на if. Как описано в разделе 5.19 стандарта, для метки case требуется целочисленное константное выражение *1019*, что, грубо говоря, компилятор может определить, просто взглянув на текущую единицу перевода; в то время как INTERFACE_ID является простым константным выражением , значение которого не может быть определено до времени ссылки.

...