2010-02-28 7 views
6

Digamos que ya tenemos una jerarquía de clases, p.Agregar funciones virtuales sin modificar las clases originales

class Shape { virtual void get_area() = 0; }; 
class Square : Shape { ... }; 
class Circle : Shape { ... }; 
etc. 

Ahora digamos que quiero (efectivamente) añadir un método virtual draw() = 0 a Shape con las definiciones apropiadas en cada subclase. Sin embargo,, digamos que quiero hacer esto sin modificar esas clases (ya que son parte de una biblioteca que no quiero cambiar).

¿Cuál sería la mejor manera de hacerlo?

Si realmente "agrego" un método virtual o no, no es importante, solo quiero el comportamiento polimórfico dado una serie de punteros.

Mi primera idea sería hacer esto:

class IDrawable { virtual void draw() = 0; }; 
class DrawableSquare : Square, IDrawable { void draw() { ... } }; 
class DrawableCircle : Circle, IDrawable { void draw() { ... } }; 

y luego simplemente reemplazar todas las creaciones de Square s y s Circle con DrawableSquare s y DrawableCircle s, respectivamente.

¿Es esa la mejor manera de lograr esto, o hay algo mejor (preferiblemente algo que deja la creación de Square sy Circle s intactos).

Gracias de antemano.

Respuesta

5

(I do proponer una solución más abajo ... oso conmigo ...)

Una forma de (casi) resolver su problema es el uso de un patrón de diseño del visitante. Algo como esto:

class DrawVisitor 
{ 
public: 
    void draw(const Shape &shape); // dispatches to correct private method 
private: 
    void visitSquare(const Square &square); 
    void visitCircle(const Circle &circle); 
}; 

Entonces, en lugar de esto:

Shape &shape = getShape(); // returns some Shape subclass 
shape.draw(); // virtual method 

puede hacer:

DrawVisitor dv; 
Shape &shape = getShape(); 
dv.draw(shape); 

Normalmente en un patrón de Visitantes que sería implementar el método draw así:

DrawVisitor::draw(const Shape &shape) 
{ 
    shape.accept(*this); 
} 

Pero eso solo funciona si la jerarquía Shape fue diseñada para ser visitada: cada subclase implementa el método virtual accept llamando al método visitXxxx apropiado en el Visitor. Lo más probable es que no fue diseñado para eso.

Sin ser capaz de modificar la jerarquía de clases para agregar un método virtual accept-Shape (y todas las subclases), necesita alguna otra manera de ser trasladados al método correcto draw. Un enfoque ingenuo es este:

DrawVisitor::draw(const Shape &shape) 
{ 
    if (const Square *pSquare = dynamic_cast<const Square *>(&shape)) 
    { 
    visitSquare(*pSquare); 
    } 
    else if (const Circle *pCircle = dynamic_cast<const Circle *>(&shape)) 
    { 
    visitCircle(*pCircle); 
    } 
    // etc. 
} 

Eso funcionará, pero hay un golpe de rendimiento al usar dynamic_cast de esa manera. Si se lo puede permitir ese golpe, es un enfoque sencillo que es fácil de comprender, depurar, mantener, etc.

Supongamos que hubo una enumeración de todos los tipos de forma:

enum ShapeId { SQUARE, CIRCLE, ... }; 

y había una virtuales método ShapeId Shape::getId() const = 0; que cada subclase anularía para devolver su ShapeId. A continuación, puede hacer su envío utilizando una declaración masiva switch en lugar de if-elsif-elsif de dynamic_cast s. O quizás en lugar de switch use una tabla hash. El mejor de los casos es poner esta función de mapeo en un lugar, para que pueda definir múltiples visitantes sin tener que repetir la lógica de mapeo cada vez.

Probablemente tampoco tenga un método getid(). Demasiado. ¿Cuál es otra forma de obtener una identificación que sea única para cada tipo de objeto? RTTI. Esto no es necesariamente elegante o infalible, pero puede crear una tabla hash de type_info punteros. Puedes construir esta tabla hash en algún código de inicialización o compilarla dinámicamente (o ambas cosas).

DrawVisitor::init() // static method or ctor 
{ 
    typeMap_[&typeid(Square)] = &visitSquare; 
    typeMap_[&typeid(Circle)] = &visitCircle; 
    // etc. 
} 

DrawVisitor::draw(const Shape &shape) 
{ 
    type_info *ti = typeid(shape); 
    typedef void (DrawVisitor::*VisitFun)(const Shape &shape); 
    VisitFun visit = 0; // or default draw method? 
    TypeMap::iterator iter = typeMap_.find(ti); 
    if (iter != typeMap_.end()) 
    { 
    visit = iter->second; 
    } 
    else if (const Square *pSquare = dynamic_cast<const Square *>(&shape)) 
    { 
    visit = typeMap_[ti] = &visitSquare; 
    } 
    else if (const Circle *pCircle = dynamic_cast<const Circle *>(&shape)) 
    { 
    visit = typeMap_[ti] = &visitCircle; 
    } 
    // etc. 

    if (visit) 
    { 
    // will have to do static_cast<> inside the function 
    ((*this).*(visit))(shape); 
    } 
} 

Podría haber algunos errores de sintaxis/errores allí, no he intentado compilar este ejemplo. He hecho algo como esto antes: la técnica funciona. Sin embargo, no estoy seguro de si podría tener problemas con las bibliotecas compartidas.

Una última cosa que voy a añadir: independientemente de lo que decida hacer el envío, es probable que tenga sentido para hacer una clase base de visitantes:

class ShapeVisitor 
{ 
public: 
    void visit(const Shape &shape); // not virtual 
private: 
    virtual void visitSquare(const Square &square) = 0; 
    virtual void visitCircle(const Circle &circle) = 0; 
}; 
+0

¿Quiere decir 'visitCircle (const Circle & circle)' en lugar de visitar Square allí? –

+0

@Philip: uy ... arreglado. – Dan

+0

Solución interesante, me gusta el uso de solo una parte del patrón 'Visitor'. Los patrones están destinados a adaptarse a la situación, y no al revés :) –

3

Lo que está describiendo es algo así como decorator pattern. Lo cual es muy adecuado para cambiar el comportamiento del tiempo de ejecución de las clases existentes.

Pero yo no veo cómo implementar su ejemplo práctico, si las formas no tienen manera de ser dibujado, entonces no hay manera de cambiar el comportamiento de dibujo en tiempo de ejecución, ya sea ...

Pero supongo que esto es solo un ejemplo muy simplificado para stackoverflow? Si todos los bloques de construcción básicos para la funcionalidad deseada están disponibles, implementar el comportamiento exacto del tiempo de ejecución con un patrón así es ciertamente una opción decente.

+0

Este no es un ejemplo simplificado para SO. Literalmente tengo formas a las que quiero agregar métodos de dibujo. Específicamente, las formas son de las formas de colisión del SDK de Bullet Physics y quiero poder agregar funcionalidad de dibujo para la depuración. –

+0

Corrígeme si me equivoco, pero no creo que el decorador resuelva este problema. Decorator agrega un nuevo árbol de comportamiento a las clases existentes, en lugar de agregar comportamiento al árbol existente. –

+0

Parece que el Bullet SDK tiene una interfaz 'btIDebugDraw': http://bulletphysics.com/Bullet/BulletFull/classbtIDebugDraw.html. Obtiene una clase de ella que implementa funciones como 'drawBox()' y 'drawSphere()', luego la asigna a 'btCollisionWorld' o' btDynamicsWorld', y luego hace que dibuje ese mundo. ¿Eso haría lo que necesitabas? –

0

Uno 'de la pared' solución que te pueden gustar para considerar, dependiendo de las circunstancias, es utilizar plantillas para darle un comportamiento polimórfico en tiempo de compilación. Antes de decir nada, yo sé que esto no le dará tiempo de ejecución polimorfismo tradicional por lo que bien puede no ser útil, pero dependiendo de las limitaciones del entorno en el que se está trabajando, puede resultar útil:

#include <iostream> 

using namespace std; 

// This bit's a bit like your library. 
struct Square{}; 
struct Circle{}; 
struct AShape{}; 

// and this is your extra stuff. 
template < class T > 
class Drawable { public: void draw() const { cout << "General Shape" << endl; } }; 

template <> void Drawable<Square>::draw() const { cout << "Square!" << endl; }; 
template <> void Drawable<Circle>::draw() const { cout << "Circle!" << endl; }; 

template < class T > 
void drawIt(const T& obj) 
{ 
    obj.draw(); 
} 

int main(int argc, char* argv[]) 
{ 
    Drawable<Square> a; 
    Drawable<Circle> b; 
    Drawable<AShape> c; 

    a.draw(); // prints "Square!" 
    b.draw(); // prints "Circle!" 
    c.draw(); // prints "General Shape" as there's no specific specialisation for an Drawable<AShape> 

    drawIt(a); // prints "Square!" 
    drawIt(b); // prints "Circle!" 
    drawIt(c); // prints "General Shape" as there's no specific specialisation for an Drawable<AShape> 
} 

El método drawIt() es probablemente la clave, ya que representa un comportamiento genérico para cualquier clase que cumpla el requisito de tener un método draw(). Tenga cuidado con el código hinchado aquí, ya que el compilador creará una instancia de un método diferente para cada tipo pasado.

Esto puede ser útil en situaciones donde necesita escribir una función para trabajar en muchos tipos que no tienen una clase base común. Soy consciente de que esta no es la pregunta que me hiciste, pero pensé que la lanzaría solo como una alternativa.

Cuestiones relacionadas