Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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, which 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.

like image 518
deovrat singh Avatar asked Sep 07 '11 14:09

deovrat singh


2 Answers

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.

like image 128
bshields Avatar answered Nov 04 '22 08:11

bshields


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.

like image 31
Alexandre C. Avatar answered Nov 04 '22 09:11

Alexandre C.