Re: Solving the data inheritance problem
Kaba wrote:
Here it is the intent that the common behaviour is not
dependent on
subclasses, that is, does not use virtual functions.
That seems rather obvious. The behavior which is common is in
the base class; that which isn't is relegated to the derived
class. (And if there is no behavior which is not common, of
course, you don't need to derive.) That's the principle behind
the template method pattern.
Well, why not. I have always conceived that the most important thing of
the template method pattern is to form a skeleton that can be customized
by calling the subclass functions. Clearly zero subclass-calls can be
thought as a special case of it. Then again, I wonder if considering
singularities is useful: I suspect that many design patterns have the
property of converging to a plain old class in the limit.
Now that I have (hopefully) unambiguously stated the problem, I am ready
to hear for more ideas. The ideas thus far are summarized as:
1) The obvious approach (already called an "industry standard")
This works by placing the data and the concrete functionality right in
the superclass. This works fine, but has the effect of forcing the
subclass implementers to remember to call the superclass swap in their
swap etc. I'd like to remove this need as a possible source of (human)
error.
2) Deferring data to the subclass
This works by writing a pure virtual function to the superclass which
can be used to retrieve the common data which is stored in the subclass.
This way the superclass becomes a pure interface again and the subclass
does not have to call it back to swap. The data is a member of the
subclass and thus it probably remembers to swap that.
3) Deferring data to the subclass and automating the boiler-plate code
generation
This works by using a template class to make a boiler-plate code class a
leaf of the inheritance hierarchy. This class contains the required data
and the implementation of the data access function (which is virtual in
superclass). This way the subclass is not cluttered with this boiler-
plate: the superclass and the boiler-plate class together form the
functionality. Note that the boiler-plate class has to remember to call
swap for the subclass in its swap, but the difference now is that the
boiler-plate class has to be coded only once for all different
subclasses.
In my opinion, option 3 is simply improvement over option 2. But both
options 2 and 3 suffer from a performance problem: every data access
from superclass to the boiler-plate goes through a virtual function.
This is enough for me to consider again the simple option 1. Maybe
that's just how it must go.. Any more ideas?
4.) Use instance behaviour.
There is another, slightly more radical solution. I've been spending
the day writing an article on instance behaviour for my web site and I
think the shape issue can be easily re-formulated in these terms.
Starting at the beginning we have a point (I'm going to keep all of
this as simple as possible. C++ to be treated as pseudo-code for
pedagogical purposes only - don't try to compile this).
typedef std::pair< int, int > point_t;
Although we don't think we can have just two points for all the sorts
of primitives we need I expect that we can use something like this:
typedef std::list< point_t > control_points_t;
For a circle 2 is fine. 2 for a square also gives us orientation
(opposite corners) and I *think* this also works for all other proper
polygons. A (non equiliateral) triangle requires three points, an
ellipse three (each end of one diagonal and then a distance from that,
expressed as another point). A proper rectangle is also three points
(with orientation) and then other n-polygons are just n points.
This means that we must constrain the number of points we allow for a
given shape. Maybe something like this will work:
int min_points() const;
int max_points() const; // use numeric limits on int, or pick a high
number for n-polys
We also want a method something like this to draw the shape:
void draw( screen & ) const;
I think we'd also want something that allows the next point to be
chosen by the user. Because of the way we interpret the points we
probably want something like this (which lets us control how we change
the mouse position to the next control point):
point_t rubber_band( screen &, point_t mousepos ) const;
And of course to add the point when the user clicks:
void add_point( point_t );
The obvious way of using all of this is something like:
class Shape {
public:
virtual void draw( const control_points_t &, screen & ) const = 0;
virtual point_t rubber_band( const screen &, point_t mousepos )
const = 0;
virtual int min_points() const = 0;
virtual int max_points() const = 0;
void add_point( point_t );
private:
control_points_t points;
};
This is a partial solution as it now at least lets us keep all of the
data manipulation in the super class. We have delegated the constraints
to the various sub-classes (by using the rubber_band() member to
constrain where points can be, and by allowing the super-class to ask
for the min and max number of points the shape supports).
It doesn't really solve the swap() problem though. As James Kanze
points out, we still can't really swap different primitives even if
this does at least allow us to move the control points into a new
primitive more easily.
From an OO perspective we have defined all of our shapes with a common
data structure and we are using inclusional polymorphism to deliver the
message (i.e. rubber_band()) to the bit of code that we want to execute
(the method). We don't have to use this delivery mechanism though (or
at least, not directly).
We can add an extra level of indirection to the messages and handle the
final message delivery not within a hierarchy based from Shape, but
from a completely seperate hierarchy. We then store a pointer to an
instance of these other objects within Shape thus allowing us to use
per-instance rather than per-type message delivery as seen from Shape.
It will be clearer with an example:
class Primitive {
public:
virtual void draw( const control_points_t &, screen & ) const = 0;
virtual point_t rubber_band( const screen &, point_t mousepos )
const = 0;
const int min_points;
const int max_points;
protected:
Primitive( int max, int min );
};
Note that we don't bring add_point() to here as Shape will still hold
the control points.
We now sub-class this in order to fill in the actual code that needs to
be executed just as we did before. We might end up with something like
this:
const class Circle {
public:
Circle() : Primitive( 2, 2 ) {}
void draw( const control_points_t &, screen & ) const;
point_t rubber_band( const screen &, point_t mousepos ) const;
} circle_primitive; // We use a single global instance of the primitive
for convenience
We can now change shape to be this:
class Shape {
public:
Shape( const Primitive & );
void draw( const control_points_t &, screen & ) const;
point_t rubber_band( const screen &, point_t mousepos ) const;
int min_points() const = 0;
int max_points() const = 0;
void add_point( point_t );
private:
control_points_t m_points;
const Primitive *m_primitive;
};
All of the Shape members now just forward to the relevant Primitive
members. If I have my head around this properly then I think we should
now be able to implement swap() fairly easily for Shape.
The really neat thing here though is that we can add a member to change
the primitive:
void primitive( const Primitive &p ) {
m_primitive = &p;
}
We can now imagine the user interface allowing the user to switch
between primitives for the control points by (maybe) using the page
up/down keys. What to do about extra/missing control points depends on
the user experience you want, and I expect it could be solved in a
fairly straight forward way (maybe you'd only allow to flip through
primitives that are valid with that number of control points).
This works pretty well here because we can seperate the data from the
bahaviour very cleanly and get all of our bahaviour from a common data
definition. In other situations this won't work so well.
K
--
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]