我需要开发一个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(); }
};


因此,以上内容的作用是允许用户实例化BodyShape对象,这些对象用于管理关联基础实现SomeShape / SomeBodyOtherShape / 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 / AShapeBBody / 级别。但是,将代码切换为BShape仅接受ABody::setShape()会使一方面的整个工厂模式无用,并且将迫使用户代码知道正在使用哪个实现。

另外,似乎AShape* / A类是B / Some的额外抽象级别,它们仅在编译时用于支持它们,但并不打算向API公开。 ,那又有什么意义呢?它们仅用作一种阻抗匹配层,将OtherSomeShape都强制进入OtherShape模具。

但是我还有哪些选择?可以使用一些运行时类型检查,例如Shapedynamic_cast<>,但我正在寻找更优雅的方法(如果可能)。

您将如何用另一种语言来做呢?

最佳答案

分析您的设计问题

您的解决方案通过以下方式实现abstract factory design pattern


AFactoryBFactory是抽象Factory的具体工厂
一方面ABodyAShape,另一方面BBodyBShape是抽象产品BodyShape的具体产品。
Axxx类构成了相关类的一个家族。 Bxxx类也是如此。


您担心的问题是方法Body::setShape()依赖于抽象的形状参数,而具体的实现实际上期望具体的形状。

正如您正确指出的那样,对具体Shape的低估表明存在潜在的设计缺陷。而且,由于整个模式在运行时是动态且灵活的,并且无法对虚拟函数进行模板化,因此不可能在编译时捕获错误。

替代方法1:使您当前的设计更安全

使用dynamic_cast<>在运行时检查向下转换是否有效。后果:


丑陋的铸件被很好地隔离在一个功能上。
仅在必要时(即仅在设置形状时)才进行运行时检查。


备选方案2:采用具有高度隔离性的设计

更好的设计是隔离不同的产品。因此,一个产品类将仅使用同一家族的其他类的抽象接口,而忽略它们的具体特异性。

后果:


非常坚固的设计,实现了关注点的卓越分离
您可以在抽象类级别分解Shape*成员,甚至可以取消虚拟化setShape()
但这是以牺牲刚性为代价的:您无法使用特定于家庭的界面。例如,如果目标是该系列代表一个本机UI,这可能非常令人尴尬,因为他们知道产品高度相互依赖并且需要使用本机API(这是《 4人帮》一书中的典型示例)。


备选方案3:对依赖类型进行模板化

选择基于模板的抽象工厂实现。通常的想法是,使用模板实现定义产品之间的内部依赖关系。

因此,在您的示例Shape中,AShapeBShape不变,因为不依赖于其他产品。但是主体取决于形状,您要具有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*


这是一个online demo

10-07 17:11