开发者

How should I design a set of related classed where only some of them support a certain operation?

开发者 https://www.devze.com 2023-04-03 00:21 出处:网络
I am working on a slide-based application in C++. Each slide has a slide-items collection which can include items like caption, button, rectangle, etc.

I am working on a slide-based application in C++. Each slide has a slide-items collection which can include items like caption, button, rectangle, etc.

Only some of these items support fill, while others don't.

What is the best way to implement the fill for the slide items in this case? Here are two ways that I thought of:

  1. Create an interface Fillable and implement this interface for slide items which support fill, keeping all the properties related to fill in the interface. When iterating over the list of slide items, dynamic_cast them into Fillable, and if successful, do the operation related to fill.

  2. Make a fill class. Make a fill pointer a part of slide item class, assign the fill object to the fill pointer for those objects which support fill, and for rest of them keep it null. Give a function GetFill, wh开发者_JAVA百科ich will return the fill for the items if it exists otherwise returns NULL.

What's the best approach for this? I'm interested in performance and maintainability.


I would do a combination of the two. Make your Fillable interface and have it be the return type for your GetFill method. This is better than the dynamic cast approach. Using dynamic cast to query for the interface requires that the actual slide item object implement the interface if it is to support it. With an accessor method like GetFill however, you have the option of providing a reference/pointer to some other object that implements the interface. You can also just return this if the interface is in fact implemented by this object. This flexibility can help avoid class bloat and promote the creation of re-usable component objects that can be shared by multiple classes.

Edit: This approach also works nicely with the null object pattern. Instead of returning a null pointer for the objects that don't support Fillable, you can return a simple no-op object that implements the interface. Then you don't have to worry about always checking for null pointers in the client code.


The answer is it depends.

I don't see the point in having to clutter your base interface with fill/get_fillable_instance/... if not every object is supposed to handle fill. You can however get away with just

struct slide_object
{
    virtual void fill() {} // default is to do nothing
};

but it depends on whether you think fill should appear in the slide object abstract class. It rarely should however, unless being non fillable is exceptional.

Dynamic casting can be correct in the case you need to provide two distinct classes of objects (and no more than two), some of them being fillable, and the other having nothing to do with fillability. In this case, it makes sense to have two sub-hierarchies and use dynamic casting where you need.

I have used this approach successfully in some cases and it is simple and maintainable, provided the dispatch logic is not scattered (ie. there is only one or two places where you dynamic cast).

If you are expected to have more fill-like behavior, then dynamic_cast is a wrong choice since it will lead to

if (auto* p = dynamic_cast<fillable*>(x))
   ...
else if (auto* p = dynamic_cast<quxable*>(x))
   ...

which is bad. If you are going to need this, then implement a Visitor pattern.


Create a base class SlideItem:

class SlideItem {
    public:
        virtual ~SlideItem();
        virtual void fill() = 0;
};

Then do an empty implementation for those you can't fill:

class Button : public SlideItem {
    public:
        void fill() { }
};

And a proper fill implementation for the others:

class Rectangle : public SlideItem {
    public:
        void fill() { /* ... fill stuff ... */ }
};

and put all of them inside a container.. if you want to fill them just call everybody... easy to maintain.. and who cares about performance :)


If you really need fast code your first solution is certainly good. But if you do it like that, make sure you don't have to cast it every time you want to fill. Cast them one time and put the pointers in a fillable-container. Then iterate over this fillable-container if you have to fill.

Then again, IMHO you put too much effort into this, without a reasonable performance gain. (of course I don't know your application, it might be justified.. but usually not)


It seems like what you're looking for is close to the Capability Pattern. Your #2 is close to this pattern. Here's what I would do:

Make a fill class. Make fill pointer a part of slide item class, assign the fill object to fill pointer for only those objects which support fill, for rest of them keep it null. Create a function GetCapability(Capability.Fill), which will return the fill for the items if it exists otherwise returns NULL. If some of your objects already implement a Fillable interface, then you can return the object cast to a Fillable pointer instead.


Consider storing Variant items, such as boost::variant.

You can define a boost::variant<Fillable*,Item*> (you should use smart pointers if you have ownership), and then have a list of those variants on which to iterate.


I suggest using an interface for the shapes, with a method that returns a filler. For example:

class IFiller {
public:
    virtual void Fill() = 0;

protected:
    IFiller() {}
    virtual ~IFiller() {}
};

class IShape {
public:
    virtual IFiller* GetFiller() = 0;

protected:
    IShape() {}
    virtual ~IShape() {}
};

class NullFiller : public IFiller {
public:
    void Fill() { /* Do nothing */ }
};

class Text : public IShape {
public:
    IFiller* GetFiller() { return new NullFiller(); }
};

class Rectangle;
class RectangleFiller : public IFiller {
public:
    RectangleFiller(Rectangle* rectangle) { _rectangle = rectangle; }
    ~RectangleFiller() {}

    void Fill() { /* Fill rectangle space */ }

private:
    Rectangle* _rectangle;
};

class Rectangle : IShape {
public:
    IFiller* GetFiller() { return new RectangleFiller(this); }
};

I find this method easier to maintain and to extend, while it does not introduce major performance issues.

0

精彩评论

暂无评论...
验证码 换一张
取 消