Скажем, у нас уже есть иерархия классов, например.
class Shape { virtual void get_area() = 0; };
class Square : Shape { ... };
class Circle : Shape { ... };
etc.
Теперь скажем, то, что я хочу (эффективно) добавить a virtual draw() = 0
метод к Shape
с соответствующими определениями в каждом подклассе. Однако скажем, я хочу сделать это, не изменяя те классы (поскольку они - часть библиотеки, которую я не хочу изменять).
Каков был бы лучший способ пойти об этом?
"Добавляю" ли я на самом деле a virtual
метод или не не важен, я просто хочу полиморфное поведение, учитывая массив указателей.
Моя первая мысль состояла бы в том, чтобы сделать это:
class IDrawable { virtual void draw() = 0; };
class DrawableSquare : Square, IDrawable { void draw() { ... } };
class DrawableCircle : Circle, IDrawable { void draw() { ... } };
и затем просто замените все создания Square
s и Circle
s с DrawableSquare
s и DrawableCircle
s, соответственно.
Это лучший способ выполнить это или там что-то лучше (предпочтительно что-то, что оставляет создание Square
s и Circle
s неповрежденный).
Заранее спасибо.
(Я предлагаю решение дальше ... терпите ...)
Один из способов (почти) решить вашу проблему - использовать шаблон проектирования Visitor. Примерно так:
class DrawVisitor
{
public:
void draw(const Shape &shape); // dispatches to correct private method
private:
void visitSquare(const Square &square);
void visitCircle(const Circle &circle);
};
Затем вместо этого:
Shape &shape = getShape(); // returns some Shape subclass
shape.draw(); // virtual method
Вы бы сделали:
DrawVisitor dv;
Shape &shape = getShape();
dv.draw(shape);
Обычно в шаблоне посетителя вы реализуете метод draw
следующим образом:
DrawVisitor::draw(const Shape &shape)
{
shape.accept(*this);
}
Но это работает, только если иерархия Shape
была разработана для посещения: каждый подкласс реализует виртуальный метод accept
, вызывая соответствующий метод visitXxxx
для Visitor. Скорее всего, он не был предназначен для этого.
Не имея возможности изменить иерархию классов, чтобы добавить виртуальный метод accept
в Shape
(и все подклассы), вам нужен другой способ отправки в правильный draw
метод. Один наивный подход таков:
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.
}
Это будет работать, но использование dynamic_cast в таком случае снижает производительность. Если вы можете себе это позволить, это простой подход, который легко понять, отладить, поддерживать и т. Д.
Предположим, существует перечисление всех типов фигур:
enum ShapeId { SQUARE, CIRCLE, ... };
и существует виртуальный метод ShapeId Shape :: getId () const = 0;
, который каждый подкласс должен переопределить, чтобы вернуть свой ShapeId
. Затем вы можете выполнить отправку, используя массивный оператор switch
вместо if-elsif-elsif из dynamic_cast
s. Или, возможно, вместо переключателя
используйте хеш-таблицу.В лучшем случае эту функцию сопоставления можно разместить в одном месте, чтобы можно было определять несколько посетителей без необходимости каждый раз повторять логику сопоставления.
Так что, вероятно, у вас тоже нет метода getid ()
. Печалька. Как еще можно получить уникальный идентификатор для каждого типа объекта? RTTI. Это не обязательно элегантно или надежно, но вы можете создать хеш-таблицу указателей type_info
. Вы можете построить эту хеш-таблицу в некотором коде инициализации или построить ее динамически (или и то, и другое).
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);
}
}
Возможно, там есть какие-то ошибки / синтаксические ошибки, я не пробовал скомпилировать этот пример. Я делал что-то подобное раньше - техника работает. Я не уверен, что у вас могут возникнуть проблемы с разделяемыми библиотеками.
И последнее, что я добавлю: независимо от того, как вы решите выполнять отправку, вероятно, имеет смысл создать базовый класс посетителя:
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;
};
Одно из нестандартных решений, которое вы могли бы рассмотреть, в зависимости от обстоятельств, - использовать шаблоны для дать вам полиморфное поведение во время компиляции.Прежде чем вы что-то скажете, я знаю , что это не даст вам традиционного полиморфизма времени выполнения, поэтому он может оказаться бесполезным, но в зависимости от ограничений среды, в которой вы работаете, он может оказаться полезным:
#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 >
}
Метод drawIt ()
, вероятно, является ключевым здесь, поскольку он представляет общее поведение для любого класса, отвечающего требованиям наличия метода draw ()
. Обратите внимание на раздувание кода здесь, поскольку компилятор создает отдельный метод для каждого переданного типа.
Это может быть полезно в ситуациях, когда вам нужно написать одну функцию для работы со многими типами, не имеющими общего базового класса. Я понимаю, что это не тот вопрос, который вы задали, но я подумал, что брошу его просто как альтернативу.
То, что вы описываете, чем-то похоже на шаблон декоратора . Что очень подходит для изменения поведения существующих классов во время выполнения.
Но я действительно не понимаю, как реализовать ваш практический пример, если фигуры не могут быть нарисованы, тогда нет способа изменить поведение рисования во время выполнения ...
Но я полагаю, что это просто очень упрощенный пример для stackoverflow? Если доступны все основные строительные блоки для желаемой функциональности, то реализация точного поведения во время выполнения с таким шаблоном, безусловно, является достойным вариантом.