views:

300

answers:

9

I have the following C++ code (simplified version):

class Shape
{
    bool isCircle = false;
    bool isSquare = false;
}

class Circle : public Shape
{
    // some special members/methods
}

class Square : public Shape
{
    // some special members/methods
}

class CAD
{
    virtual DrawCircle(Circle * circle) = 0;
}

class SWX : public CAD
{
    virtual DrawCircle(Circle * circle){// do some stuff that draws circle on SWX system}
}

class PRO : public CAD
{
    virtual DrawCircle(Circle * circle){// do some stuff that draws circle on PRO system}
}

int main()
{
    Circle * circle = new Circle();
    circle->isCircle = true;

    Square * sq = new Square;
    sq->isSquare = true;

    vector<Shape*> shapes;
    shapes.push_back(circle);
    shapes.push_back(sq);

    SWX * swx = new SWX();

    for( int i = 0 ; i < shapes.size() ; ++i )
    {
        if( shapes[i]->isCircle )
    {
        SWX->DrawCircle((Circle*)(shapes[i]));
    }
    else if( shapes[i]->isSquare )
    {
        SWX->DrawSquare((Square*)(shapes[i]));
    }
}

I wish to remove the need for if...else (if at all possible within the constraints stated below).

My constraints right now are:

  • The CAD and derived classes are ginormous classes with various external dependencies.
  • The CAD classes cannot be merged with the Shape and derived classes (that would have been ideal, since then I can use polymorphism to solve my problem), since other projects/classes depend on the Shape classes and cannot depend on the CAD classes.
  • There are more than a dozen Shape-derived classes with a half dozen CAD-derived classes and this if...else is happening in numerous locations - so it would help if any solution is simple to understand (easier to convince my teammates to change legacy code).

Any suggestions/comments/solution you have would be most welcome.

+8  A: 

The standard solution for this problem, especially given your constraints regarding dependencies, is to use the Visitor Pattern.

Here's how Visitor Pattern would work in your case:

  • You need an abstract ShapeVisitor class. It has an abstract Visit method for each concrete subclass of Shape. eg: Visit(Circle*), Visit(Square*), etc.
  • Shape has an abstract AcceptVisitor(ShapeVisitor*) method.
  • Each Shape subclass implements AcceptVisitor as just calling visitor->Visit(this)
  • Each CAD class is a (or has-a, up to you) a ShapeVisitor. The Visit methods do the appropriate drawing for the specific type of Shape. No conditional or casting required.

Here's a modified version of your code that uses Visitor Pattern in a pretty low-impact way:

class Circle;
class Square;
class ShapeVisitor
{
    virtual void Visit(Circle *circle) = 0;
    virtual void Visit(Square *square) = 0;
}

class Shape
{
    virtual void AcceptVisitor(ShapeVisitor *visitor) = 0;
}


class Circle : public Shape
{
    // some special members/methods

    virtual void AcceptVisitor(ShapeVisitor *visitor)
    {
        visitor->Visit(this);
    }
}

class Square : public Shape
{
    // some special members/methods

    virtual void AcceptVisitor(ShapeVisitor *visitor)
    {
        visitor->Visit(this);
    }
}

class CAD : public ShapeVisitor
{
    virtual DrawCircle(Circle *circle) = 0;
    virtual DrawSquare(Square *square) = 0;

    virtual void Visit(Circle *circle) {
        DrawCircle(circle);
    }

    virtual void Visit(Square *square) {
        DrawSquare(square);
    }
}

class SWX : public CAD
{
    virtual DrawCircle(Circle *circle){// do some stuff that draws circle on SWX system}

}

class PRO : public CAD
{
    virtual DrawCircle(Circle * circle){// do some stuff that draws circle on PRO system}
}

int main()
{
    Circle * circle = new Circle();
    Square * sq = new Square;

    vector<Shape*> shapes;
    shapes.push_back(circle);
    shapes.push_back(sq);

    SWX * swx = new SWX();

    for( int i = 0 ; i < shapes.size() ; ++i )
    {
        shapes[i]->AcceptVisitor(SWX);
    }
}

In this code I've opted for making CAD actually a subclass of ShapeVisitor. Also, since you've already got virtual methods in CAD to do the drawing, I implemented the Visit methods there (once), rather than once in each subclass. Once you switch clients over to the using AcceptVisitor instead of calling the Draw* methods directly you could make those methods protected, and then eventually move the implementation of the Visit methods down to the subclasses (that is: refactor to remove the extra level of indirection caused by having Visit(Foo*) call DrawFoo(Foo*)).

Laurence Gonsalves
@dreamlax You're right, its confusing to mention an option and then immediately say it won't work for this case. I've removed the bogus option 1, and elaborated on the visitor pattern approach, as that seems to be the best way to go given the dependency constraint.
Laurence Gonsalves
I like this solution — it takes into account the question's requirements by decoupling the Visitor plumbing from the actual drawing invocations.
jleedev
Thanks. Haven't implemented the Visitor pattern before. Perhaps I will consider this if the simple solution provided by @brianray does not work for me.
ossandcad
Thanks @laurence-gonsalves for your sample code. I am currently trying to implement your solution and had a doubt in your code. For the implementation of the AcceptVisitor is this the code you intended - `class Circle : public Shape{ // some special members/methods virtual void AcceptVisitor(ShapeVisitor *visitor) { visitor->Visit(this); }}`I added the `visitor->' part - OR did you intend to have a 'Visit' method in the Shape classes? Thanks again for your help.
ossandcad
Oops. You're right. It should say `vistor->Visit(this)`.
Laurence Gonsalves
Accepted as ANSWER since was received earliest. Thanks to everyone who posted a similar answer.
ossandcad
+1  A: 

You've got some rather wonky OO going on there, but at the very least DrawXxxx should just become Draw(). Circle, Square, and the other shapes would then define a Draw() method that provides an implementation for a virtual Draw method on Shape. Then you can just call Draw on any Shape and it will do the right thing.

The isXxxx booleans should go too. Classes know what they are and instanceof can tell you (though it shouldn't be necessary to check when you Draw since that will be a virtual method invocation).

Ry4an
Up vote for calling it "wonky". This also seems to be the solution that I want to prototype, also stated by @brianray (and perhaps others?). Thanks. Will recomment on my success/failure.
ossandcad
+6  A: 

This is a classic case for DoubleDispatch, where you need to have a separate method for each possible (Shape, CAD) pair:

  • Nuke the isSquare/isCircle fields.
  • add virtual void DrawOn(CAD*) to the Shape interface.
  • Implement Circle::DrawOn(CAD*) (for example):

    void Circle::DrawOn(CAD *c) {
      c->DrawCircle(this);
    }
    

    This is the "trick" which allows a call like myCircle->DrawOn(mySWX) to call the correct method no matter the type of the Shape or the CAD.

jleedev
He's mentioned that the Shape classes cannot depend on the CAD classes because other classes depend on the Shape classes which should not depend on the CAD classes.
dreamlax
Ah, I didn't quite notice that. Can we weasel out of that by instead making the shapes depend on a "Draws Things" interface?
jleedev
But, you can forward declare the CAD classes. Does that count as "depending"?
rlbond
A: 

This may not be the ideal solution, but you could just provide an CAD::Draw member function and overload it to handle each different type of shape. Something like:

class CAD {
  public:
    virtual void Draw(const Circle& circle) = 0;
    virtual void Draw(const Square& square) = 0;
};

Then you could just call Draw on any supported object without any if statements.

Whisty
This only works if he statically knows the concrete type of a Shape. In his example code he's determining whether a Shape is a Circle or Square at runtime.
Laurence Gonsalves
@Laurence Gonsalves, good point. Somehow I missed the fact that the shapes are being handled through a pointer to the base class.
Whisty
+1  A: 

Why not just SWX->Draw(shapes[i]);? You would need to add two Draw method with one that takes a Circle and one that takes a square?

brianray
This one seems the simplest. I will prototype this to see if it works. I had considered this, but since I don't control the creation of the Shape classes (it comes from numerous locations in the code, the example given, is a really simple version) - I was not sure of all the ways that the Shape classes are created (e.g. some developer on the team could create Square pointer and set isCircle=true) and therefore not sure if this would work well. Is this considered ADL or Koenig lookup?
ossandcad
@brianray Thanks for the suggestion, but if I implement this I get a C2664 error stating that cannot convert from Shape * to Circle *. Here is what I added ` virtual void Draw(Circle * circle) = 0;//in CAD class ` virtual void Draw(Square * sq) = 0;//in CAD class `implemented those in SWX and PRO classes and passed in shapes[i], compile error. I think my problem may be related to the fact that the pointers stored in vector<Shape*> are being retrieved as Shape*. Comments/suggestions still welcome.
ossandcad
+1  A: 

This is pretty basic OO polymorphism. Circle and Square are specialized versions of shape. They should each know what specialized behavior is needed to deal with your cad classes.

class Shape
{
    virtual void DrawWithCAD(CAD * cad) = 0;
}

class Circle : public Shape
{
    virtual void DrawWithCAD(CAD * cad)
    {
        cad->DrawCircle(this);
    }
}

class Square : public Shape
{
    virtual void DrawWithCAD(CAD * cad)
    {
        cad->DrawSquare(this);
    }
}

Then your main() loop would change to:

for( int i = 0 ; i < shapes.size() ; ++i )
{
    shapes[i]->DrawWithCAD(swx);
}
Alan
Sorry. The Shape classes cannot be aware of the CAD classes, since the CAD classes are actually plugins to Computer-aided-design systems with numerous API calls and dependencies. If Shape classes depended on the CAD classes then we would have difficulty using them for non-CAD related stuff.
ossandcad
The shape classes are not aware of the SWX or PRO classes. They know only of the CAD interface. If the CAD interface in your project represents much more than you mentioned in your example, then make some new class in the middle like 'Renderer' or 'Canvas' that your shape classes can have access to and which would know about the CAD interface.
Alan
I guess an aspect that I didn't mention in my question is that Shape classes are in a separate DLL-project from the CAD classes. Would I be able to avoid a circular dependency between the two DLL-projects if I implemented your suggested solution? Thanks in advance.
ossandcad
+1 for not going the double-dispatch way. Make an interface with drawcircle, drawsquare, etc that is implemented by CAD and couple your shape classes to that interface. When calling DrawWithCAD give them the specialized CAD objects. Simple that.
Ozan
If the renderer class has a pure virtual interface, then shape depends only on that interface. The interface depends on nothing (it just uses forward declarations for the shapes classes). The implementation of the renderer class depends on the shape classes, the renderer interface, and the cad classes. The cad classes depend on nothing new. No cycles in the dependancy graph.
Alan
A: 

The way I would solve this is to let the individual shape classes draw themselves onto some kind of drawing "canvas" (a class providing a drawing buffer and some fundamental drawing functions, e.g. for lines, rectangles, and ellipses).

The SWX class would then call the Shape's Draw method, providing it with its own canvas:

class Shape
{
public:
    virtual void DrawOnto(Canvas& canvas) = 0;
};

class Circle : public Shape
{
public:
    virtual void DrawOnto(Canvas& canvas);
};

void Circle::DrawOnto(Canvas& canvas)
{
    // ..., e.g.:
    canvas.DrawEllipse(center.x, center.y, radius, radius);
}

...

class SWX : public CAD
{
private:
    Canvas canvas;
public:
    void Draw(Shape& shape)
    {
        shape.DrawOnto(this.canvas);
        // ^ this is the place where your if..else used to be
    }
};
stakx
Sorry. The idea of Shape drawing themselves does not work, since it is the CAD classes that decide how Shapes are drawn - the Shape classes do not know. I am not sure if this helps, but this is in the field of computer-aided-design where the CAD classes are plugins to SolidWorks (SWX) and Pro/Engineer (PRO), and drawing involves numerous (and vastly different - between the two stated systems) API calls that Shapes simply cannot be aware of.
ossandcad
The above `Canvas` approach could still work, even in this case, if you provide two specialized versions of each shape class: one for `SWX`, and one for `PRO` (e.g. `class SWXCircle : public Circle`, `class PROCircle : public Circle`). Just my two cents. Sorry should I still mis-understand the exact problem.
stakx
Thanks stakx for your help. I understand your solution now. I unfortunately cannot implement specialized versions for each shape class - not an implementation issue but a project-related one - would involve work that my team won't buy. Good suggestion though.
ossandcad
A: 

Write a several global Draw functions overloaded for each Shape... (DrawCircle and friends are virtual so we can use polymorphism for calls to CAD objects)

void Draw(CAD *cad, Circle *circle){
    cad->DrawCircle(circle);
}

void Draw(CAD *cad, Square *square){
    cad->DrawSquare(square);
}
Vulcan Eager
Hadn't considered this. Usually try to avoid globals, as it may mess with our unit/functional-tests that create doubles of the CAD classes. Need to think about this a bit.
ossandcad
You would still need to add virtual member functions to square and circle that would call these, or would need to do explicit RTTI tests as in the OP example. You won't be able to call these draw functions with only a pointer to a Shape.
Alan
Alan is right. A Shape* won't work here.
Vulcan Eager
+1  A: 

Why not define a simple ICAD interface? Since CAD depends on Shape it does not increase complexity:

class Shape
{
    Draw(ICAD* cad) = 0;        
}

class Circle : public Shape
{
    Draw(ICAD* cad)
    {
        ICAD->DrawCircle(self)
     }
}

class Square : public Shape
{
    Draw(ICAD* cad)
    {
        ICAD->DrawSquare(self)
     }
}

DrawSquare(self) looks funny, but I don't know what the CAD classes do with the shape objects.

class ICAD
{
    virtual DrawSquare(Square* square) = 0;
    virtual DrawCircle(Circle * circle) = 0;
}

I assume class CAD has more than those abstract methods, which is why you don't couple it with Shape.

class CAD : public ICAD
{
    // big CAD class...
}

class SWX : public CAD
{
    virtual DrawCircle(Circle * circle){// do some stuff that draws circle on SWX system}
}

class PRO : public CAD
{
    virtual DrawCircle(Circle * circle){// do some stuff that draws circle on PRO system}
}

int main()
{
    Circle * circle = new Circle();
    Square * sq = new Square;

    vector<Shape*> shapes;
    shapes.push_back(circle);
    shapes.push_back(sq);

    SWX * swx = new SWX();

    for( int i = 0 ; i < shapes.size() ; ++i )
    {
        shapes[i]->Draw(swx);
    }
}
Ozan
Thanks for answer. Similar to laurence-gonsalves answer who replied earliest and was hence accepted instead.
ossandcad