Simple OOP, polymorphism design question

J

Jacek Dziedzic

Hello!

First of all please forgive me for not posting a compilable snippet,
but rather a simplified piece of code with the unimportant
details left out.

Let's say I have two classes 'box_shape' and 'cylinder_shape' that
derive from a common base class 'shape', more or less like this:

namespace Shapes {

int SHA_UNDEFINED=-1;
int SHA_BOX=0;
int SHA_CYLINDER=1;

class shape {
public:
shape();
virtual ~shape();

int virtual what_shape() const {
return SHA_UNDEFINED;
}

void virtual move_shape(/*...*/) {
/*...*/
}

/* some more methods common to all shapes*/
};

class box_shape : public shape {
public:
box_shape();

int what_shape() const {
return SHA_BOX;
}

void move_shape(/*...*/) {
shape::move_shape(/*...*/);
/* do some stuff specific to box_shape */
}

vector<double> box_corners() const { // returns box corners
/*...*/
}
};

class cylinder_shape : public shape {
public:
cylinder_shape();

int what_shape() const {
return SHA_CYLINDER;
}

void move_shape(/*...*/) {
shape::move_shape(/*...*/);
/* do some stuff specific to cylinder_shape */
}

double cylinder_height() const { // returns cylinder height
/*...*/
}
};

} // of namespace Shapes

Later on I have a class called 'system_geometry' which
contains a member 'region', which is a shape, either a box-like or
a cylindrical shape. Since I don't know in advance which
particular descendant of 'shape' this region is, I decided
to use a pointer to shape, like this:

class system_geometry {
public:
/* ... lots of stuff here ... */
Shapes::shape *region;
}

and I initialize it like that:

if(user_wants_a_box) region=new Shapes::box_shape;
else if(user_wants_a_cylinder) region=new Shapes::cylinder_shape;

Thanks to polymorphism I can now have calls like
region->what_shape() or region->move_shape() which
dynamically resolve to methods of appropriate descendant classes.

That's all right up to this point. There are some places in
my code, however, when I need to call some methods that are
specific to the derived classes, something akin to:

switch(region.what_shape()) {
case SHA_BOX:
// this branch now needs to call region->box_corners();
break;
case SHA_CYLINDER:
// this branch now needs to call region->cylinder_height();
break;
default:
throw EInternalError("Wrong shape");
}

The problem is, of course, that neither box_corners() nor
cylinder_height() are defined for the base class 'shape'.
What should I do in this situation? I've thought up two
solutions. One, possibly ugly, is to add all the names of
all methods of the derived classes to the base class as
pure virtual methods. That way 'shape' will formally have
all the required methods. I don't like that solution.

The other solution I've been pondering is via dynamic_cast.
Will it be all right to do something like:

switch(region.what_shape()) {
case SHA_BOX:
box_shape *region_as_box=dynamic_cast<box_shape*>(region);
// now this branch can use region_as_box->box_corners()
break;
case SHA_CYLINDER:
cylinder_shape *region_as_cylinder=
dynamic_cast<cylinder_shape*>(region);
// now this branch can use region_as_cylinder->cylinder_height();
break;
default:
throw EInternalError("Wrong shape");
}

I have doubts because I recall that casting base --> derived
is not a good idea. On the other hand I can be sure that 'region'
definitely points to an instance of a derived class, not the base.
And I guess I'm trying to cast base* --> derived*, but I'm a little
confused here.

Is this approach OK? If not, how can I solve this problem?

TIA,
- J.
 
D

Derrick Coetzee

Jacek said:
switch(region.what_shape()) {
case SHA_BOX: [ . . . ]
case SHA_CYLINDER: [ . . . ]
}

Whenever you see a switch on the type of an object, this is a good sign
that you are failing to exploit polymorphism properly. For example,
consider this code:

class Animal { ... };
class Dog : Animal { ... };
class Cat : Animal { ... };
....
void makeSound(Animal* animal) {
switch(animal->getType()) {
case TYPE_DOG:
Dog* dog = static_cast<Dog*>(animal);
cout << "Woof!";
dog->wagTail();
break;
case TYPE_CAT:
Cat* cat = static_cast<Cat*>(animal);
cout << "Meow!";
cat->purr();
break;
}
}

As you've surely guessed, this is the wrong way to implement
makeSound(), and the above casts, while safe and acceptable, are a
symptom of bad design. There should instead be a virtual member function
on Animal which is overridden accordingly in Dog and Cat:

class Animal { ... virtual void makeSound() = 0; };
class Dog : Animal { ... virtual void makeSound(); };
class Cat : Animal { ... virtual void makeSound(); };
....
void Dog::makeSound() {
cout << "Woof!";
wagTail();
}
void Cat::makeSound() {
cout << "Meow!";
cat->purr();
}

The main advantage of this design is modularity: if you add a new
animal, you don't have to go hunting down switches and adding branches
to them in various places. Instead, you just add a new class defining
each of the virtual methods. This also ensures no cases are missed, and
avoids ugly code.

However, if this was, for some reason, impossible (maybe Animal is from
a library header that can't be changed) then the switch-based solution
would work and may be the best possible solution, at least in the
short-term. If you're still worried about the generally unsafe
static_casts, you may want to use dynamic_casts instead. In particular,
when you use dynamic_casts with references they conveniently throw an
exception when the run-time type is wrong.

I hope this helps, although I'm not sure how much of it applies to your
specific case.
 
D

David Hilsee

Jacek Dziedzic said:
Hello!

First of all please forgive me for not posting a compilable snippet,
but rather a simplified piece of code with the unimportant
details left out.

Let's say I have two classes 'box_shape' and 'cylinder_shape' that
derive from a common base class 'shape', more or less like this:

namespace Shapes {

int SHA_UNDEFINED=-1;
int SHA_BOX=0;
int SHA_CYLINDER=1;

class shape {
public:
shape();
virtual ~shape();

int virtual what_shape() const {
return SHA_UNDEFINED;
}

void virtual move_shape(/*...*/) {
/*...*/
}

/* some more methods common to all shapes*/
};

If you use constants and a what_shape() function, then you are essentially
using RTTI. From a design standpoint, it's about the same as using
dynamic_cast to figure out the type of the object because you still end up
using switch/case-like code.

That's all right up to this point. There are some places in
my code, however, when I need to call some methods that are
specific to the derived classes, something akin to:

switch(region.what_shape()) {
case SHA_BOX:
// this branch now needs to call region->box_corners();
break;
case SHA_CYLINDER:
// this branch now needs to call region->cylinder_height();
break;
default:
throw EInternalError("Wrong shape");
}

The problem is, of course, that neither box_corners() nor
cylinder_height() are defined for the base class 'shape'.
What should I do in this situation? I've thought up two
solutions. One, possibly ugly, is to add all the names of
all methods of the derived classes to the base class as
pure virtual methods. That way 'shape' will formally have
all the required methods. I don't like that solution.

The other solution I've been pondering is via dynamic_cast.
Will it be all right to do something like:

switch(region.what_shape()) {
case SHA_BOX:
box_shape *region_as_box=dynamic_cast<box_shape*>(region);
// now this branch can use region_as_box->box_corners()
break;
case SHA_CYLINDER:
cylinder_shape *region_as_cylinder=
dynamic_cast<cylinder_shape*>(region);
// now this branch can use region_as_cylinder->cylinder_height();
break;
default:
throw EInternalError("Wrong shape");
}

I have doubts because I recall that casting base --> derived
is not a good idea. On the other hand I can be sure that 'region'
definitely points to an instance of a derived class, not the base.
And I guess I'm trying to cast base* --> derived*, but I'm a little
confused here.

Many people don't like constructs like this because they can cause
maintenance problems. Adding a new shape becomes more complicated because
it requires you to dig through the codebase, updating various switch/case or
if/else type-specific operations. If you have many of these, it can be a
real nightmare.

Usually, if you want to avoid these switch/case tests, you add an additional
virtual function that contains the type-specific behavior. For example, if
it's drawing on the screen, then you could add an additional virtual member
function to Shape called, say, draw(). Could you do that for this
switch/case using function, or is that approach unreasonable?
 
P

Phlip

Jacek said:
First of all please forgive me for not posting a compilable snippet,

The "compilable snippet" rule is for syntax questions.
Let's say I have two classes 'box_shape' and 'cylinder_shape' that
derive from a common base class 'shape', more or less like this:
class shape {
int virtual what_shape() const {

class box_shape : public shape {
int what_shape() const {
vector<double> box_corners() const { // returns box corners

class cylinder_shape : public shape {
int what_shape() const {
double cylinder_height() const { // returns cylinder height

Later on I have a class called 'system_geometry' which
contains a member 'region', which is a shape, either a box-like or
a cylindrical shape. Since I don't know in advance which
particular descendant of 'shape' this region is, I decided
to use a pointer to shape, like this:

class system_geometry {
public:
/* ... lots of stuff here ... */

Uh, why "lots" of stuff? Could it move elsewhere?
Shapes::shape *region;
}

and I initialize it like that:

if(user_wants_a_box) region=new Shapes::box_shape;
else if(user_wants_a_cylinder) region=new Shapes::cylinder_shape;

This is correct, so far. The situation "user_wants_a_foo" is a _boundary_
situation. It might represent types as typecodes. The first layer, going in,
should convert type codes to types. Code distant from such a boundary should
use as few 'if' and 'switch' statements as possible.
Thanks to polymorphism I can now have calls like
region->what_shape() or region->move_shape() which
dynamically resolve to methods of appropriate descendant classes.

And you obey the Liskov Substitution Principle, such that clients never
accidentally become aware what target type they use, right?
That's all right up to this point. There are some places in
my code, however, when I need to call some methods that are
specific to the derived classes, something akin to:

switch(region.what_shape()) {
case SHA_BOX:
// this branch now needs to call region->box_corners();
break;
case SHA_CYLINDER:
// this branch now needs to call region->cylinder_height();
break;
default:
throw EInternalError("Wrong shape");
}

The problem is, of course, that neither box_corners() nor
cylinder_height() are defined for the base class 'shape'.
What should I do in this situation?

You seek to avoid a down-cast. However, your style got poor when you called
region->what_shape() itself. Clients should tell, not ask, servant objects
what to do.

What is the switch, semantically, for? Suppose that box_corners() return a
vector of 4 points, and cylinder_height() returned 2. Then something used
them to decorate 4 or 2 times.

Whatever it does, suppose system_geometry inherited an abstract base class,
System, with a pure virtual method called 'emblandish()'. Then you call:

region->emblandishYourself(*this);

The base class provides a pure virtual emblandishYourself(System & aSystem)
method. Then the box provides a concrete implementation, and calls
aSystem.emblandish(point) four times, and cylinder calls it two times.

This idea is not "better". The point is to decouple interfaces at the same
time as we merge duplicated definitions of behavior. Read the book /Design
Patterns/.
I've thought up two
solutions. One, possibly ugly, is to add all the names of
all methods of the derived classes to the base class as
pure virtual methods. That way 'shape' will formally have
all the required methods. I don't like that solution.

Sometimes that's just "right". You won't find it in /Design Patterns/,
though... ;-)
The other solution I've been pondering is via dynamic_cast.
Will it be all right to do something like:

switch(region.what_shape()) {
case SHA_BOX:
box_shape *region_as_box=dynamic_cast<box_shape*>(region);
// now this branch can use region_as_box->box_corners()
break;
case SHA_CYLINDER:
cylinder_shape *region_as_cylinder=
dynamic_cast<cylinder_shape*>(region);
// now this branch can use region_as_cylinder->cylinder_height();
break;
default:
throw EInternalError("Wrong shape");
}

Sometimes that is "right" too. The proof comes when this system must change,
somehow. How easily could you add another shape? How easily could you change
cylinder_height() without changing box_corners()?

(And, sometimes, deferring that proof until you find a real need to change
will teach what kind of decoupling you actually need... Read the book
/Refactoring/ to learn how to re-design existing code. Then read
/Refactoring to Patterns/ to put them together!)
 
J

Jacek Dziedzic

Thanks a lot for all the answers. Yes, it's late at night here
in Europe, so I missed the (now) obvious solution which was to
move the code to the descendants of 'shape' and make them
perform tasks themselves instead of asking what type they were
and performing tasks for them.

The method in question was 'make_periodic_with_respect_to_shape(...)'
which now just looks like
{
region->make_periodic_with_respect_to_shape(...);
}

letting the shape-classes do their tasks by themselves.

Owing to your hints I've managed to remove all but one of my
'switch(type_of)' constructs and the code looks much cleaner now!
Adding more shapes will definitely be easier now, as those
switch statements reminded me of Fortran-77 :).

Thanks a lot!

- J.
 
P

Phlip

Jacek said:
The method in question was 'make_periodic_with_respect_to_shape(...)'
which now just looks like
{
region->make_periodic_with_respect_to_shape(...);
}

letting the shape-classes do their tasks by themselves.

Forgot to mention one more goal: Dependency Inversion Principle. Look that
up, ensure you have it, and _then_ you are done.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,744
Messages
2,569,484
Members
44,904
Latest member
HealthyVisionsCBDPrice

Latest Threads

Top