简体   繁体   中英

Most Elegant Way to Get Around This Polymorphism Issue

EDIT: I'm working with C++.

So, I am creating methods/functions to test intersection between shapes. I essentially have this:

class Shape {};

class Rectangle : public Shape {};

class Circle : public Shape {};

class Line : public Shape {};

Now, I need to decide on the best way to write the actual methods/functions to test intersection. But all my shapes will be stored in a list of Shape pointers, so I will be calling a method/function of the basic form:

bool intersects (Shape* a, Shape* b);

At that point, I need to determine which types of shapes 'a' and 'b' are, so I can properly detect collisions. I can easily do one of them, by just using some virtual methods:

class Shape
{
    virtual bool intersects (Shape* b) = 0;
}

That would determine one of the shapes ('a' is now 'this'). However, I would still need to get the type of 'b'. The obvious solution is to give Shape an 'id' variable to classify which shape it is, and then 'switch' through those, and then use dynamic_cast. However, that is not very elegant, and it feels like there should be a more OO way to do this.

Any suggestions?

As @Mandarse pointed out, this is typical double dispatch issue. In Object Oriented languages, or like C++ languages that can implement Object Oriented concepts, this is usually solved using the Visitor Pattern.

The Visitor interface itself defines one callback per concrete type, in general.

class Circle;
class Rectangle;
class Square;

class Visitor {
public:
  virtual void visit(Circle const& c) = 0;
  virtual void visit(Rectangle const& r) = 0;
  virtual void visit(Square const& s) = 0;
};

Then, the Shape hierarchy is adapted for this. We need two methods: one to accept any type of visitor, the other to create the "appropriate" intersection visitor.

class Visitor;
class Intersecter;

class Shape {
public:
  virtual void accept(Visitor&) const = 0; // generic
  virtual Intersecter* intersecter() const = 0;
};

The intersecter is simple:

#include "project/Visitor.hpp"

class Intersecter: public Visitor {
public:
  Intersecter(): result(false) {}
  bool result;
};

For example, for Circle it will give:

#include "project/Intersecter.hpp"
#include "project/Shape.hpp"

class Circle;

class CircleIntersecter: public Intersecter {
public:
  explicit CircleIntersecter(Circle const& c): _left(c) {}

  virtual void visit(Circle const& c);    // left is Circle, right is Circle
  virtual void visit(Rectangle const& r); // left is Circle, right is Rectangle
  virtual void visit(Square const& s);    // left is Circle, right is Square

private:
  Circle const& _left;
}; // class CircleIntersecter


class Circle: public Shape {
public:
  virtual void accept(Visitor& v) const { v.visit(*this); }

  virtual CircleIntersecter* intersecter() const {
    return new CircleIntersecter(*this);
  }
};

And the usage:

#include "project/Intersecter.hpp"
#include "project/Shape.hpp"

bool intersects(Shape const& left, Shape const& right) {
  boost::scope_ptr<Intersecter> intersecter(left.intersecter());
  right.accept(*intersecter);
  return intersecter->result;
};

If other methods need a double-dispatch mechanism, then all you need to do is create another "Intersecter-like" class that wrap the result and inherits from Visitor and a new "Factory" method rooted in Shape that is overriden by derived classes to provide the appropriate operation. It is a bit long-winded, but does work.

Note: it is reasonable to except intersect(circle, rectangle) and intersect(rectangle, circle) to yield the same result. You can factor the code is some methods and have CircleIntersecter::visit delegates to the concrete implementation. This avoid code duplication.

Andrei Alexandrescu detailed this problem in his classic Modern C++ Design . The companion library Loki contains the the implementation for Multi-Methods .

Update

Loki provides three implementations of Multi-Methods, depending on the user's needs. Some are for simplicity, some are for speed, some are good for low coupling and some provide more safety than others. The chapter in the book spans nearly 40 pages, and it assumes the reader is familiar with many of the book's concepts -- if you are comfortable using boost, then Loki may be down your alley. I really can't distill that to an answer acceptable for SO, but I've pointed you to the best explanation of the subject for C++ that I know of.

C++ run-time polymorphism has a single dispatch (the base class vtable).

There are various solution to your problem but none of them is "elegant", since they all try to force the language to do more that it can natively support (Alexandrescu Loki multimethods is a very goodly hidden set of hacks: it encapsulates the "bad things", but doesn't make then good)

The concept, here, it that you need to write all the N 2 functions of the possible combinations and find a way to call them based on the actual runtime-type of TWO parameters. The "visitor pattern" (call back a virtual unction from another virtual function), the "mutimethod" technic (use a generic dspatch table), the "dynamic cast" into a virtual function or the "dual dynamic_cast" out all of the functions all do the same thing: call a function after two indirection. None of them can technically be defined "better then the other" since the resulting performance is mostly the same.

But some of them cost more then the other in code writing and other cost more in code maintenance. You have most likely to try to estimate in your case what the trade-off is. How many other classes do you think you may need to add in the future?

You can add a field shapeType to each Shape

For example:

class Shape {
  virtual shapetype_t getShapeType() const;
  // ...
}

I've played with the shapes intersection resolving dispatch approach just for fun. I didn't like the idea of extending classes each time new shape appear. I thought of the collection of intersection resolvers which is iterated to find out if there is one that supports given pair of shapes. If new shape appear new intersection resolvers are to be added to the collection.

I don't think that it is a most optimal approach in terms of performance, since the resolvers iterated through and dynamic casts executed until proper resolver is found.

But, nevertheless...

Intersection resolver takes two shapes and return resolving result which contain supported and intersect flags.

struct Intersection_resolution {
    bool supported;
    bool intersect;
};

class IIntersection_resolver {
    public:
        virtual Intersection_resolution intersect(Shape& shape1, Shape& shape2) = 0;
};

Resolver implementation. Template class, takes two shapes, checks if it supports them and if so invokes check_intersection method. Latter should be defined in specification. Note that the the pair should be specified only ones, ie if Rectangle-Circle specified, no need to specify Circle-Rectangle.

template<typename S1, typename S2>
class Intersection_resolver : public IIntersection_resolver {
    private:
        bool check_intersection(S1& s1, S2& s2);
    public:
        Intersection_resolution intersect(Shape& shape1, Shape& shape2) override final {
            S1* s1 = dynamic_cast<S1*>(&shape1);
            S2* s2{nullptr};
            if (s1) 
                s2 = dynamic_cast<S2*>(&shape2);
            else {
                s1 = dynamic_cast<S1*>(&shape2);
                if (s1)
                    s2 = dynamic_cast<S2*>(&shape1);
            }
            bool supported{false};
            bool intersect{false};
            if (s1 && s2) {
                supported = true;
                intersect = check_intersection(*s1, *s2);
            }
            return Intersection_resolution{supported, intersect};
        }
};

Couple of specifications...

template<>
bool Intersection_resolver<Rectangle, Rectangle>::check_intersection(Rectangle& r1, Rectangle& r2) {
    cout << "rectangles intersect" << endl;
    return true;
}

template<>
bool Intersection_resolver<Rectangle, Circle>::check_intersection(Rectangle& r1, Circle& r2) {
    cout << "rectangle intersect circle" << endl;
    return true;
}

Resolvers collection.

class Intersection_resolvers {
    std::vector<IIntersection_resolver*> resolvers_;
    public:
    Intersection_resolvers(std::vector<IIntersection_resolver*> resolvers) :resolvers_{resolvers} {}
    Intersection_resolution intersect(Shape& s1, Shape& s2) {
        Intersection_resolution intersection_resolution;
        for (IIntersection_resolver* resolver : resolvers_) {
            intersection_resolution = resolver->intersect(s1, s2);
            if (intersection_resolution.supported)
                break;
        }
        return intersection_resolution;
    }
};

Intersection_resolver<Rectangle, Rectangle> rri;
Intersection_resolver<Rectangle, Circle> rci;

Intersection_resolvers intersection_resolvers{{&rri, &rci}};

Usage.

int main() {
    Rectangle r;
    Triangle t;
    Circle c;
    Shape* shapes[]{&r, &t, &c};
    for (auto shape : shapes) {
        shape->draw();
    }
    for (auto shape : shapes) {
        for (auto other : shapes) {
            auto intersection_resolution = intersection_resolvers.intersect(*shape, *other);
            if (!intersection_resolution.supported) {
                cout << typeid(*shape).name() << " - " << typeid(*other).name() << " intersection resolving not supported" << endl;
            }
        }
    }
}

Output.

rectangle drawn
triangle drawn
circle drawn
rectangles intersect
9Rectangle - 8Triangle intersection resolving not supported
rectangle intersect circle
8Triangle - 9Rectangle intersection resolving not supported
8Triangle - 8Triangle intersection resolving not supported
8Triangle - 6Circle intersection resolving not supported
rectangle intersect circle
6Circle - 8Triangle intersection resolving not supported
6Circle - 6Circle intersection resolving not supported

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM