Управление разнообразными классами с помощью центрального менеджера без RTTI

У меня есть вопрос дизайна, который беспокоил меня некоторое время, но я не могу найти хорошее (в смысле ООП) решение для этого. Язык — 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. Можно предположить, что в каждый момент времени присутствует не более одного экземпляра каждого типа модуля. Таким образом, если бы можно было сделать что-то подобное (где менеджер — это экземпляр ModuleManager), все было бы в порядке:

IModuleFoo* pFoo = manager.get(IModuleFoo);

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

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

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

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

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


person rocktale    schedule 26.02.2011    source источник
comment
Не могли бы вы объяснить, почему RTTI считается плохим дизайном? Использовать его вместо виртуальных функций если конечно плохо, но для этого нужен общий базовый класс, которого у вас нет. Поскольку вы все равно делаете здесь что-то довольно динамичное, так что я не вижу, насколько это может повредить.   -  person Macke    schedule 26.02.2011
comment
Ну, из того, что я читал, RTTI считается индикатором плохого дизайна, что означает, что это не обязательно, но часто так и есть. И может ли это быть одним из случаев, когда RTTI не является признаком дизайна кровати, был частью моего вопроса. При поиске в Интернете я не нашел ни одного источника, утверждающего, что использование RTTI является хорошей идеей (для любых целей).   -  person rocktale    schedule 26.02.2011
comment
Что ж, поскольку ответы пока касаются в основном деталей предложенного мной дизайна и используют по крайней мере какой-то (пользовательский) RTTI - я также открыт для совершенно разных подходов (с точки зрения дизайна).   -  person rocktale    schedule 27.02.2011


Ответы (2)


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

Например:

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 у вас не будет особого выбора, кроме такого подхода.

person bdonlan    schedule 26.02.2011
comment
Мне нравится идея переложить ответственность решать, предоставляется интерфейс или нет, на разные модули. Однако мне было интересно, что может случиться с оболочкой шаблона в IModuleBase, если другой модуль, скажем, IModuleBar, не предоставляет перечисление INTERFACE_ID? - person rocktale; 26.02.2011
comment
Выглядит хорошо, но я думаю, что вы могли бы переместить реализацию SomeModule::LookupInterface() в IModuleFoo(), так как она будет одинаковой для всех классов, реализующих этот интерфейс (если только они не реализуют несколько интерфейсов, в этом случае им нужно будет предоставить свой собственный LookupInterface(), который обрабатывает все). из них, возможно, вызывая каждого родителя в некотором порядке, пока не будет возвращено true). - person j_random_hacker; 26.02.2011
comment
@rocktale: в этом случае ошибка времени компиляции произойдет в момент любого вызова IModuleBase::LookupInterface<IModuleFoo>(). Это хорошая причина для вызова функции-оболочки шаблона вместо простого виртуального метода, поскольку, хотя вызов последнего непосредственно как IModuleBase::LookupInterface(IModuleFoo::INTERFACE_ID, &p);, конечно, не удастся таким же образом, он требует дисциплины написания IModuleFoo::INTERFACE_ID каждый раз. (То есть, если вы только что написали IModuleBase::LookupInterface(SOME_RANDOM_CONSTANT, &p);, компилятор не сможет ничего за вас проверить.) - person j_random_hacker; 26.02.2011
comment
Спасибо за объяснение. Если я правильно понимаю, все становится ужасно, если модули IModuleFoo и IModuleBar указывают один и тот же INTERFACE_ID. Есть ли способ убедиться, что это невозможно? - person rocktale; 26.02.2011
comment
rocktale, есть несколько вариантов. Вы можете использовать указатели на фиктивные статические объекты-члены класса или можете пойти по маршруту COM и использовать идентификаторы GUID. В любом случае switch невозможно — вместо этого вам понадобится дерево if. - person bdonlan; 26.02.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 требуется интегральное константное выражение, которое, грубо говоря, компилятор может определить, просто взглянув на текущую единицу перевода; в то время как INTERFACE_ID — это просто константное выражение, значение которого не может быть определено до времени компоновки.

person j_random_hacker    schedule 26.02.2011
comment
Это интересная идея. Однако в чем преимущество этого подхода перед использованием RTTI? - person rocktale; 26.02.2011
comment
@rocktale: По сути, нет никакого преимущества, за исключением возможной экономии небольшого количества места за счет сохранения метаинформации только о тех классах, которыми мы явно хотим управлять, а не обо всех классах, у которых есть виртуальный метод. RTTI dynamic_cast на самом деле более мощный, чем то, что мы имеем здесь, потому что он позволяет тестировать любой уровень нисходящего преобразования (полезно, если у вас есть многоуровневая иерархия типов модулей), хотя, возможно, он немного медленнее. - person j_random_hacker; 27.02.2011
comment
Я тоже так думал. У меня нет причин не использовать здесь dynamic_cast RTTI. Тем не менее, я многому научился из ваших ответов. - person rocktale; 27.02.2011