Мне нужно разработать решение C ++ для представления объекта с функциями, где объекты и функции представлены различными объектами, но фактическая реализация ассоциации реализована в производном классе, который существует для инкапсуляции внешней реализации. Я знаю, что такие вещи типичны для проблем, связанных с наследованием, поэтому я хочу мнения о правильном решении. Часть реализации должна рассматриваться как своего рода граница API — пользовательский код не должен ее видеть или видеть только один раз, чтобы выбрать реализацию.
Вот пример:
#include <cstdio>
// External implementation 1
class SomeShape {};
class SomeBody { public: SomeShape *shape; };
// External implementation 2
class OtherShape {};
class OtherBody { public: OtherShape *shape; };
//////////////
class Shape
{
public:
virtual const char *name() { return "Shape"; }
};
class Body
{
public:
virtual void setShape(Shape *s) = 0;
};
class Factory
{
public:
virtual Shape *makeShape() = 0;
virtual Body *makeBody() = 0;
};
//////////////
class AShape : public Shape
{
public:
SomeShape *someShape;
virtual const char *name() { return "AShape"; }
};
class ABody : public Body
{
protected:
SomeBody *someBody;
AShape *shape;
public:
ABody() { someBody = new SomeBody; }
virtual void setShape(Shape *s)
{
shape = static_cast<AShape*>(s);
printf("Setting shape: %s\n", s->name());
someBody->shape = shape->someShape;
}
};
class AFactory : public Factory
{
public:
virtual Shape *makeShape()
{ return new AShape(); }
virtual Body *makeBody()
{ return new ABody(); }
};
//////////////
class BShape : public Shape
{
public:
OtherShape *otherShape;
virtual const char *name() { return "BShape"; }
};
class BBody : public Body
{
protected:
OtherBody *otherBody;
BShape *shape;
public:
BBody() { otherBody = new OtherBody; }
virtual void setShape(Shape *s)
{
shape = static_cast<BShape*>(s);
printf("Setting shape: %s\n", s->name());
otherBody->shape = shape->otherShape;
}
};
class BFactory : public Factory
{
public:
virtual Shape *makeShape()
{ return new BShape(); }
virtual Body *makeBody()
{ return new BBody(); }
};
Таким образом, роль вышеупомянутого состоит в том, чтобы позволить пользователю создавать Body
а также Shape
объекты, которые существуют для управления связанными базовыми реализациями SomeShape
/SomeBody
или же OtherShape
/OtherBody
,
Тогда основной функцией, реализующей обе реализации, может быть
int main()
{
// Of course in a real program we would return
// a particular Factory from some selection function,
// this should ideally be the only place the user is
// exposed to the implementation selection.
AFactory f1;
BFactory f2;
// Associate a shape and body in implementation 1
Shape *s1 = f1.makeShape();
Body *b1 = f1.makeBody();
b1->setShape(s1);
// Associate a shape and body in implementation 2
Shape *s2 = f2.makeShape();
Body *b2 = f2.makeBody();
b2->setShape(s2);
// This should not be possible, compiler error ideally
b2->setShape(s1);
return 0;
}
Итак, части, которые меня не устраивают, это static_cast<>
звонит в setShape()
потому что они строят предположение, что был передан правильный тип объекта, без какой-либо проверки типа во время компиляции. В то же время, setShape()
может принять любой Shape
когда в действительности только производный класс должен быть принят здесь.
Однако я не вижу возможности проверки типов во время компиляции, если я хочу, чтобы пользовательский код работал с Body
/Shape
уровень, а не ABody
/AShape
или же BBody
/BShape
уровень. Тем не менее, переключение кода так, чтобы ABody::setShape()
принимает только AShape*
с одной стороны, сделает весь шаблон фабрики бесполезным и заставит пользовательский код знать, какая реализация используется.
Кроме того, похоже, что A
/B
классы являются дополнительным уровнем абстракции над Some
/Other
, которые существуют только для поддержки их во время компиляции, но они не предназначены для доступа к API, так в чем смысл … они служат только как своего рода слой соответствия импеданса, заставляя оба SomeShape
а также OtherShape
в Shape
плесень.
Но каковы мои альтернативные варианты? Можно использовать некоторую проверку типов во время выполнения, такую как dynamic_cast<>
или enum
, но я ищу что-то более элегантное, если это возможно.
Как бы вы сделали это на другом языке?
Анализ вашей проблемы дизайна
Ваше решение реализует абстрактный дизайн фабрики шаблон, с:
AFactory
а также BFactory
конкретные фабрики абстрактного Factory
ABody
а также AShape
с одной стороны и BBody
а также BShape
с другой стороны, это конкретные продукты абстрактных продуктов Body
а также Shape
, Вы беспокоитесь о том, что метод Body::setShape()
зависит от аргумента абстрактной формы, тогда как конкретная реализация в действительности ожидает конкретной формы.
Как вы правильно заметили, опускание до бетона Shape
предлагает потенциальный недостаток дизайна. И будет невозможно отловить ошибки во время компиляции, потому что весь шаблон спроектирован так, чтобы быть динамичным и гибким во время выполнения, а виртуальная функция не может быть шаблонизирована.
Альтернатива 1: сделать ваш текущий дизайн немного более безопасным
Использовать dynamic_cast<>
проверить во время выполнения, действителен ли downcast. Последствие:
Альтернатива 2: принять дизайн с сильной изоляцией
Лучшим дизайном было бы выделение различных продуктов. Таким образом, один класс продукта будет использовать только абстрактный интерфейс других классов того же семейства и игнорировать их конкретную специфику.
Последствия:
Shape*
член на уровне абстрактного класса, и, возможно, даже de-virtualize setShape()
, Альтернатива 3: шаблонизировать зависимые типы
Выберите вариант на основе шаблона вашей абстрактной фабрики. Общая идея заключается в том, что вы определяете внутренние зависимости между продуктами, используя реализацию шаблона.
Итак, в вашем примере Shape, AShape
а также BShape
неизменны, так как нет зависимости от других продуктов. Но тело зависит от формы, объявление, которое вы хотите иметь ABody
в зависимости от AShape
, в то время как BBody
должно зависеть от BShape
,
Хитрость заключается в том, чтобы использовать шаблон вместо абстрактного класса:
template<class Shape>
class Body
{
Shape *shape;
public:
void setShape(Shape *s) {
shape=s;
printf("Setting shape: %s\n", s->name());
}
};
Тогда вы бы определили ABody
выводя его из Body<AShape>
:
class ABody : public Body<AShape>
{
protected:
SomeBody *someBody;
public:
ABody() { someBody = new SomeBody; }
};
Это все очень хорошо, но как это будет работать с абстрактной фабрикой? Ну тот же принцип: шаблонизировать вместо виртуализации.
template <class Shape, class Body>
class Factory
{
public:
Shape *makeShape()
{ return new Shape(); }
Body *makeBody()
{ return new Body(); }
};
// and now the concrete factories
using BFactory = Factory<BShape, BBody>;
using AFactory = Factory<AShape, ABody>;
Следствием этого является то, что вы должны знать во время компиляции, какой конкретный завод и конкретные продукты вы собираетесь использовать. Это можно сделать с помощью C ++ 11 auto
:
AFactory f1; // as before
auto *s1 = f1.makeShape(); // type is deduced from the concrete factory
auto *b1 = f1.makeBody();
b1->setShape(s1);
При таком подходе вы больше не сможете смешивать продукты разных семейств. Следующее утверждение приведет к ошибке:
b2->setShape(s1); // error: no way to convert an AShape* to a BShape*
И здесь онлайн демо
Других решений пока нет …