Managing diverse classes with a central manager wi

2019-07-27 03:13发布

I have a design question that has been bugging me for a while but I cannot find a good (in a OOP sense) solution for this. The language is C++ and I keep coming back to RTTI - which is often referred to as an indicator for bad design.

Suppose we have a set of different kinds of modules implemented as different classes. Each kind of module is characterized by a defined interface, however the implementation may vary. Thus my first idea was to create an interface (pure abstract) class for each kind of module (e.g. IModuleFoo, IModuleBar etc.) and the implementations in seperate classes. So far so good.

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

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

On the other hand we have a set of (application) classes and each of them uses a couple of those modules but only through the interfaces - even the modules themselves might use other modules. However, all of the application classes will share the same pool of modules. My idea was to create a manager class (ModuleManager) for all modules which application classes can query for the module types they need. The available modules (and the concrete implementation) are set up during initialization of the manager and may vary over time (but that is not really part of my question).

Since the number of different module kinds is most probably >10 and may increase over time it does not appear suitable to me to store references (or pointers) to them separately. In addition there might be a couple of functions the manager needs to invoke on all managed modules. Thus I created another interface (IManagedModule) with the benefit that I can now use a container (list, set, whatsoever) of IManagedModules to store them in the manager.

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

The consequence is that a module that shall be managed needs to inherit both from the IManagedModule and from the appropriate interface for its type.

But things turn ugly when I think about the ModuleManager. It can be assumed that there is at most one instance of each module type present at each time. Thus if it was possible to do something like this (where manager is the instance of the ModuleManager), everything would be fine:

IModuleFoo* pFoo = manager.get(IModuleFoo);

But I'm pretty sure that it's not. I also thought about a template based solution like:

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

That could work but I have no idea how to find the right module within the manager if all I have is a set of IManagedModules - that is without the use of RTTI, of course.

One approach would be to provide IManagedModule with a virtual getId() method, rely on the implementations to use non-ambigous ids for each kind of module and do the pointer casting on your own. But that's just reinventing the wheel (namely RTTI) and requires a lot of discipline within the implementing classes (providing the right ids etc...) which is not desirable.

Long story short - the question is if there is really no way around some kind of RTTI here and in this case RTTI might even be a valid solution or if there might be a better (cleaner, safer, ...) design which exhibits the same flexibility (e.g. loose coupling between application classes and module classes...)? Did I miss anything?

标签: c++ oop rtti
2条回答
ゆ 、 Hurt°
2楼-- · 2019-07-27 03:55

I think bdonlan's suggestion is good, but requiring each module type to declare a distinct INTERFACE_ID is a maintenance headache. The distinctness can be accomplished automatically by having each module type declare a static object and using its address as the 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_;

In this case we are using void * as the interface ID type, instead of int or an enumerated type, so the types in some other declarations will need to change.

Also, due to quirks in C++, the INTERFACE_ID values, despite being labelled const, are not "constant enough" to be used for case labels in switch statements (or array size declarations, or a handful of other places), so you would need to change the switch statement to an if. As described in section 5.19 of the standard, a case label requires an integral constant-expression, which roughly speaking is something the compiler can determine just from looking at the current translation unit; while INTERFACE_ID is a mere constant-expression, whose value cannot be determined until link time.

查看更多
Emotional °昔
3楼-- · 2019-07-27 04:20

It sounds like you're looking for something similar to COM's QueryInterface. Now, you don't need to implement COM entirely, but the basic principle stands: You have a base class, with a virtual function, to which you pass an identifier specifying which interface you want. The virtual function then looks to see if it can implement that interface, and if so, passes back a pointer to that interface.

For example:

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() { /* ... */ }
};

It's a bit unwieldy, but it's not too bad, and without RTTI you don't have much of a choice besides an approach like this.

查看更多
登录 后发表回答