Reputation: 1911
I am working on a system where I have rectangles that can be stacked in a hierarchical order. So the base is something like this:
Rect parent;
Rect child;
parent.addChild(&child);
...
Rect* someChild = parent.getChildAt(1);
Easy to implement so far. But these rectangles should be able to implement different features. This could be a Serializer, Styler, Drawer, etc...
Now I know that multiple inheritance is a sort of "no go" but in this case I'd find a syntax like this desireable:
class StackableStyleableRect: publicRect, public Stackable, Styleable{}
class StackableStyleableDrawableRect: public Rect, public Stackable, Styleable, Drawable{}
I stumbled upon the curiously recurring template pattern (crtp) which would make the above possible if I understand it correctly. Something like this:
class Rect{
public:
float width;
float height;
}
template <class RectType>
class Stackable{
public:
void addChild(RectType* c){
children.push_back(c);
}
std::vector<RectType*> children;
}
template <class RectType>
class Drawable{
public:
virtual void draw(){
RectType* r static_cast<RectType>(this);
drawRect(r->width, r->height);
}
}
template <class RectType>
class Styleable{
public:
int r, g, b;
}
class CustomRect: public Rect, public Stackable<CustomRect>, public Drawable<CustomRect>{
}
class CustomRectWithStyle: public Rect, public Stackable<CustomRect>, public Drawable<CustomRect>, public Styleable<CustomRect>{
public:
}
The reason for this is that I would like to reuse the code with different kind of Rect types. In one project I don't have to style them while in another case I'd need all the functionality provided. With this way to select the features required, it would keep things clean and also separates funcitonality.
I did some basic tests with this and it works as expected but I feel like the syntax might grow overly complex over time.
Also at some point it would be useful to make components dependent from one another or make them behave differently if a component is around. (For example the draw function of Drawable could automatically use the colors from Styleable if present)
Now am I doomed to run into troubles sooner or later or might it work? Is there a different pattern that would fit better? Or is it simply not possible to do something like this in "proper" c++?
Upvotes: 3
Views: 963
Reputation: 927
Let me point out some of the shortcomings of your approach to this problem before I suggest different solutions, each with its own advantages and disadvantages.
Stackable
class/interface builds knowledge of an aggregate data structure into a class that represents data (Rect
). This might prove limiting in situations where Rect
s need to go into more than one data structure (A tree for lookups, for instance) or when a data structure needs to have Circle
s in it too. This solution also makes it difficult to swap out the data structure later.StackableStyleableDrawableRect
. He or she then goes on to define their own DrawableStackableStyleableRect
, that provides the same functionality but isn't the same as your class. In the best case scenario, you now have redundant code in your project. Worse off, you'll run into problems and confusion when you need to mix use of both classes because portions of the codebase already exist that use either.Rect
s, do we change all of our existing classes or create further new ones? Do we change StackableStyleableDrawableRect
into StackableStyleableDrawableResizeableRect
, prompting changes to the existing codebase, or do we create it as an entirely new class?Rect
is both a GDIDrawable
and a DirectXDrawable
and needs to call something like Drawable::logDrawingOperation()
.So while it may seem trivial that a Rect
is-a Drawable, Stackable, etc, this approach is cumbersome and has many drawbacks. I believe that in this case, Rect
has no business being anything other than a plain rectangle and shouldn't know about any other subsystem of the project.
Two alternate solutions exist that I can think of, but each of those makes the usual tradeoffs of readability vs. flexibility and compile-time complexity vs. run-time complexity.
As illustrated here, using mix-ins through template cleverness can avoid some of the problems of the MI approach, though not all. On top of that, it creates a strange inheritance hierarchy and adds compile-time complexity. It also breaks down once we add more classes to the Shape
hierarchy, since Drawable
only knows how to draw a Rect
.
The visitor pattern allows us to separate walking the object hierarchy from the algorithms that operate on it. Since each object knows its own type, it can dispatch the proper algorithm even without knowing what that algorithm is. To illustrate using Shape
, Circle
and Rect
:
class Shape
{
public:
virtual void accept(class Visitor &v) = 0;
};
class Rect : public Shape
{
public:
float width;
float height;
void accept(class Visitor &v)
{
v.visit(this);
}
};
class Circle : public Shape
{
public:
float radius;
void accept(class Visitor &v)
{
v.visit(this);
}
};
class Visitor
{
public:
virtual void visit(Rect *e) = 0;
virtual void visit(Circle *e) = 0;
};
class ShapePainter : public Visitor
{
// Provide graphics-related implementations for the two methods.
};
class ShapeSerializer : public Visitor
{
// Provide methods to serialize each shape.
};
By doing this we have sacrificed some run-time complexity, but decoupled our various concerns from our data. Adding a new concern into the program is now easy. All we need to do is add another Visitor
class that does what we want and use Shape::accept()
in conjunction with an object of this new class, like so:
class ShapeResizer : public Visitor
{
// Resize Rect.
// Resize Circle.
};
Shape *shapey = new Circle();
ShapeResizer sr;
shapey->accept(sr);
This design pattern also has the advantage that if you forget to implement some data/algorithm combination but use it in the program, the compiler will complain. We may want to override Shape::accept()
later to define, say, aggregate shape types like ShapeStack
. This way we can traverse and draw the whole stack.
I think that if performance isn't critically important in your project, the Visitor solution is superior. It might also be worth considering if you need to meet real-time constraints but it doesn't slow down the program enough to jeopardize meeting the deadlines.
Upvotes: 3
Reputation: 185
Don't know if I understand your question correctly, but maybe policy based class design might be worth a look: http://en.wikipedia.org/wiki/Policy-based_design. It was first introduced in Alexandrescu's book Modern C++ Design. It allows to extend classes by deriving them from so called policies.
The mechanics looks like:
template<class DrawingPolicy>
class Rectangle : public DrawingPolicy {
...
};
where DrawingPolicy provides a draw(void) method for example, which is then available in class Rectangle.
Hope I could help
Upvotes: 2
Reputation: 12547
I do not pretend to be extra C++ expert.
(otherwise it would be excluded from the language). Multiplle inheritance is the thing, that should be used carefully. And you should understand, what are you doing, when using it.
In your case it looks improbable that you meet diamond problem, that is evil of multiple inheritance.
Recursive template pattern allows you to check enabled features in compile time, like this
#define FEATURED(FEATURE, VALUE) \
template <template<class>class Feature = FEATURE> \
typename std::enable_if<std::is_base_of<Feature<RectType>, RectType>::value == VALUE>::type
template <class RectType>
class Styleable;
template <class RectType>
class Drawable{
public:
FEATURED(Styleable, true)
drawImpl()
{
std::cout << "styleable impl\n";
}
FEATURED(Styleable, false)
drawImpl()
{
std::cout << "not styleable impl\n";
}
virtual void draw(){
drawImpl();
}
};
You can implement something likewise in functionality with normal inheritance, but It seems impossible to make compile time feature checks.
On the other hand, you'll get your code more complicated with crtp, and you'll have to implement in all in header files.
I think it is complicated, and you should be sure you actually need it. It will work for some time, until it will be redesigned, as any other code. Its lifetime depends mostly on your task understanding.
Upvotes: 3