Search code examples
c++polymorphismdecoupling

C++ - Identifying a family of polymorphic classes without introducing tight coupling


Suppose I have an abstract base class called Component, which is the root of a hierarchy of GUI components. In such a case, we might have two subclasses, Button and Label, both of which are also abstract classes and exist as the root of their own respective hierarchies of concrete classes.

Concrete classes inheriting from Button might include RoundButton and SquareButton.

Concrete classes inheriting from Label might include TextLabel and PictureLabel.

Finally, let's say there's an aggregate Container class that holds a collection of Component objects.

The problem is that I have pointers to Component objects, but I need to identify them as being either Buttons or Labels. For example, if I want to specify that all Buttons should have a larger font for their interior text, I could iterate over all of the Component objects in the Container and somehow determine which ones are buttons, and call some method specific to buttons.

One way for these Component "families" to identify themselves is with strings.

class Component {
public:
    virtual char const * const getFamilyID() const = 0;
};

// In Button.h
char const * const COMPONENT_BUTTON = "button";

class Button : public Component {
public:
    virtual char const * const getFamilyID() const { return COMPONENT_BUTTON; };
};

// Code sample
if (strcmp(component->getFamilyID(),COMPONENT_BUTTON) == 0)
    // It's a button!

This is loosly coupled in that Component leaves the task of defining these families to its children; it does not require knowledge of what families exist. The client needs to be aware of the different Component families, but if it's trying to target a specific one for some operation, then that can't be avoided.

However, suppose we have really high performance requirements and we want to avoid comparing strings. It would also be nice to avoid making this function virtual so we can inline it. Also, if every subclass of Component is going to need to declare a global constant, it might be nice to somehow modify the Component class to either make this a requirement or make it unnecessary.

One solution to this problem is to define an enumerator in Component.h

enum COMPONENT_FAMILY {
    COMPONENT_BUTTON = 0,
    COMPONENT_LABEL,
    // etc...
};

In this case getFamilyID() can just return a COMPONENT_FAMILY enum and we can basically just compare ints. Unfortunately, this means that any new component families will have to be "registered" in this enum, which is easy but isn't entirely intuitive for other programmers. Also, the method still has to be virtual unless we make a nonstatic COMPONENT_FAMILY member that we know will have extremely low cardinality (not ideal).

What would be a good way to solve this problem? In my case, performance is key, and while something similar to the enum solution seems easy enough, I'm left wondering if I'm overlooking a better way.

--- EDIT ---
I realize that I should probably point out that in the actual system, the Container equivalent can only store 1 Component from each family. Therefore, the Components are actually stored in a map such as:

std:map<COMPONENT_FAMILY, Component*>

Applied to my simple example here, this would mean that a Container could only contain 1 Button, 1 Label, etc.

This makes it really easy to check for the existence of a particular type of Component (log time). Therefore, this question is more about how to represent COMPONENT_FAMILY, and how to determine the Component's type when I'm adding it to the map.

In other words, a component's sole purpose is to be identified as a specific piece of functionality added to the Container, and together all of the components define a specific behavior for the container.

So, I don't need to know the type of a Component, that's already implied. I am specifically asking the Container for a specific type of Component. What I need is a way for the Component to communicate its type so it can be mapped in the first place.


Solution

  • I need to identify them as being either Buttons or Labels.

    That's your problem. That assumption, as common as it might be, is usually wrong.

    • Why do you believe that you need to identify them?
    • At what stage in your code is that knowledge important?

    You can probably get around the "requirement" to know their concrete type by assigning a UI strategy for the control at construction time. Retrieve the font property of that UI strategy at paint time (or whenever) in the base class:

    class IVisualStrategy
    {
        ...
        virtual const Font& GetFont() const = 0;
        ...
    };
    
    class HeavyVisuals : public IVisualStrategy
    {
        Font font_;
        ...
        HeavyVisuals() : font_(15, Bold) {}
    
        virtual const Font& GetFont() const { return font_; }
        ...
    };
    
    class LightVisuals : public IVisualStrategy
    {
        Font font_;
        ...
        LightVisuals() : font_(12, Regular) {}
    
        virtual const Font& GetFont() const { return font_; }
        ...
    };
    

    Retrieve from base:

    class Control
    {
        ...
    private:
        void OnPaintOrSomething()
        {
            DrawTextWithFont(GetVisualStrategy().GetFont());
        }
    
        virtual const IVisualStrategy& GetVisualStrategy() const = 0;
    };
    
    class Button : public Control
    {
        HeavyVisualStrategy visualStrategy_;
        ...
    private:
        virtual const IVisualStrategy& GetVisualStrategy() const
        {
            return visualStrategy_;
        }
    }
    
    class Label : public Control
    {
        LightVisualStrategy visualStrategy_;
        ...
    private:
        virtual const IVisualStrategy& GetVisualStrategy() const
        {
            return visualStrategy_;
        }
    }
    

    A more flexible design is to keep shared pointers to IVisualStrategy in the concrete control classes and constructor-inject it, instead of hard-setting them to Heavy or Light.

    Also, to share font between objects in this design, the Flyweight pattern can be applied.