Ассоциация детей через родителей в параллельных иерархиях наследования

Мне нужно разработать решение 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, но я ищу что-то более элегантное, если это возможно.

Как бы вы сделали это на другом языке?

1

Решение

Анализ вашей проблемы дизайна

Ваше решение реализует абстрактный дизайн фабрики шаблон, с:

  • AFactory а также BFactory конкретные фабрики абстрактного Factory
  • ABody а также AShape с одной стороны и BBody а также BShape с другой стороны, это конкретные продукты абстрактных продуктов Body а также Shape,
  • Классы Axxx образуют семейство родственных классов. Так делают классы Bxxx.

Вы беспокоитесь о том, что метод Body::setShape() зависит от аргумента абстрактной формы, тогда как конкретная реализация в действительности ожидает конкретной формы.

Как вы правильно заметили, опускание до бетона Shape предлагает потенциальный недостаток дизайна. И будет невозможно отловить ошибки во время компиляции, потому что весь шаблон спроектирован так, чтобы быть динамичным и гибким во время выполнения, а виртуальная функция не может быть шаблонизирована.

Альтернатива 1: сделать ваш текущий дизайн немного более безопасным

Использовать dynamic_cast<> проверить во время выполнения, действителен ли downcast. Последствие:

  • уродливая отливка очень хорошо изолирована в одной функции.
  • проверка во время выполнения выполняется только в случае необходимости, т. е. только когда вы устанавливаете форму.

Альтернатива 2: принять дизайн с сильной изоляцией

Лучшим дизайном было бы выделение различных продуктов. Таким образом, один класс продукта будет использовать только абстрактный интерфейс других классов того же семейства и игнорировать их конкретную специфику.

Последствия:

  • очень надежная конструкция, обеспечивающая превосходное разделение интересов
  • Вы могли бы факторизовать Shape* член на уровне абстрактного класса, и, возможно, даже de-virtualize setShape(),
  • но это происходит за счет жесткости: вы не можете использовать семейный интерфейс. Это может быть очень неудобно, если, например, цель состоит в том, чтобы семья представляла собственный пользовательский интерфейс, зная, что продукты сильно взаимозависимы и должны использовать собственный API (это типичный пример в книге «Банды 4»).

Альтернатива 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*

И здесь онлайн демо

0

Другие решения

Других решений пока нет …