在平行继承层次结构中通过父母关联孩子

Posted

技术标签:

【中文标题】在平行继承层次结构中通过父母关联孩子【英文标题】:Associating children via parents in parallel inheritence hierarchies 【发布时间】:2016-03-08 18:27:04 【问题描述】:

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

因此,我在这里不满意的部分是 setShape() 中的 static_cast&lt;&gt; 调用,因为它们建立在假设已传入正确的对象类型,而没有任何编译时类型检查。同时,setShape() 可以接受任何Shape,而实际上这里应该只接受派生类。

但是,如果我希望用户代码在 Body/Shape 级别而不是 ABody/AShape 或 @987654337 级别上运行,我看不出如何进行编译时类型检查@/BShape 级别。但是,将代码切换为 ABody::setShape() 只接受 AShape* 会使整个工厂模式变得无用,一方面,还会迫使用户代码知道正在使用哪个实现。

此外,A/B 类似乎是在 Some/Other 之上的额外抽象级别,它们的存在只是为了在编译时支持它们,但这些并不打算公开到 API,那么有什么意义...它们仅用作一种阻抗匹配层,迫使 SomeShapeOtherShape 进入 Shape 模具。

但是我的替代选择是什么?可以使用一些运行时类型检查,例如 dynamic_cast&lt;&gt;enum,但如果可能的话,我正在寻找更优雅的东西。

你会如何用另一种语言做到这一点?

【问题讨论】:

我认为,根据我对您所说的理解,使用 dynamic_cast 可能是最简单的选择。我认为在使用运行时多态性时,您将无法进行任何编译时类型检查。 我在想一种解决方案可能是将 makeShape() 从工厂移动到 Body(),返回一个 已经 关联的对象.. 但确切的解决方案仍然逃避我。也不确定它的普遍性。我问这个问题是因为我觉得在这里可以做一些聪明的事情......程序员痒......;)但也许我走错了路。 嗯,这是一个有趣的想法。如果您修改 Factory 以具有一个返回 body 和 shape 对的虚拟方法怎么办?返回的主体在哪里与返回的形状相关联? 不错,明天我会尝试这个想法。我没有提到给定的身体可能有多种形状,但这是一个开始.. 我在这里想到了另一个想法,我认为这更符合我的目标。我将尝试使用一组非常基本的类来表示 body->shape 映射的结构,而不是建立一个将实现表示为派生类的层次结构。在第二遍中,我将遍历此结构并生成对应的“后端”结构,使用特定实现来镜像所需的关系。我只是想弄清楚当结构更新时这是否需要反向指针来提高效率.. 【参考方案1】:

分析您的设计问题

您的解决方案实现了abstract factory design pattern,其中:

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

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

正如您正确指出的那样,对具体的Shape 的沮丧表明存在潜在的设计缺陷。并且不可能在编译时捕获错误,因为整个模式被设计为在运行时是动态和灵活的,而虚函数不能模板化。

备选方案 1:让您当前的设计更安全

使用dynamic_cast&lt;&gt; 在运行时检查向下转换是否有效。结果:

丑陋的强制转换在单个函数中得到了很好的隔离。 运行时检查仅在必要时进行,即您设置形状的唯一时间。

方案二:采用强隔离设计

更好的设计是隔离不同的产品。所以一个产品类只会使用同族其他类的抽象接口,而忽略它们的具体特性。

后果:

非常健壮的设计实现了卓越的关注点分离 您可以在抽象类级别分解Shape* 成员,甚至可以对setShape() 进行去虚拟化。 但这是以僵化为代价的:你不能使用家庭特定的界面。这可能会很尴尬,例如目标是家庭代表原生 UI,知道产品高度相互依赖并且需要使用原生 API(这是 Gang of 4 书中的典型示例)。

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

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

因此,在您的示例 Shape 中,AShapeBShape 保持不变,因为不依赖于其他产品。但是 Body 取决于 Shape,您想要的广告 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());
  
;

然后您将通过从Body&lt;AShape&gt; 派生来定义ABody

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

【讨论】:

最后我采用了不同的策略,我不确定它是否适合您的答案,也许是替代方案 3?基本上,我取消了工厂,用户只处理直接创建BodyShape。然后这整件事基本上被传递给 AShape/ABody 创建函数,该函数获取 Body/Shape 对象并将它们转换为 ABody/AShape 对象,并对它们进行操作。对于 BBody/BShape 也是如此。因此,用户永远不会看到实现。

以上是关于在平行继承层次结构中通过父母关联孩子的主要内容,如果未能解决你的问题,请参考以下文章

Mybatis双向关联

让所有父母为孩子

显示隐形父母的孩子

跳过孩子到父母的获取 - JPA

避免并行继承层次结构

Laravel 使用 ajax 请求在同一张桌子上返回父母的孩子?