在平行继承层次结构中通过父母关联孩子
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();
;
因此,上面的作用是允许用户实例化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;
因此,我在这里不满意的部分是 setShape()
中的 static_cast<>
调用,因为它们建立在假设已传入正确的对象类型,而没有任何编译时类型检查。同时,setShape()
可以接受任何Shape
,而实际上这里应该只接受派生类。
但是,如果我希望用户代码在 Body
/Shape
级别而不是 ABody
/AShape
或 @987654337 级别上运行,我看不出如何进行编译时类型检查@/BShape
级别。但是,将代码切换为 ABody::setShape()
只接受 AShape*
会使整个工厂模式变得无用,一方面,还会迫使用户代码知道正在使用哪个实现。
此外,A
/B
类似乎是在 Some
/Other
之上的额外抽象级别,它们的存在只是为了在编译时支持它们,但这些并不打算公开到 API,那么有什么意义...它们仅用作一种阻抗匹配层,迫使 SomeShape
和 OtherShape
进入 Shape
模具。
但是我的替代选择是什么?可以使用一些运行时类型检查,例如 dynamic_cast<>
或 enum
,但如果可能的话,我正在寻找更优雅的东西。
你会如何用另一种语言做到这一点?
【问题讨论】:
我认为,根据我对您所说的理解,使用 dynamic_cast 可能是最简单的选择。我认为在使用运行时多态性时,您将无法进行任何编译时类型检查。 我在想一种解决方案可能是将 makeShape() 从工厂移动到 Body(),返回一个 已经 关联的对象.. 但确切的解决方案仍然逃避我。也不确定它的普遍性。我问这个问题是因为我觉得在这里可以做一些聪明的事情......程序员痒......;)但也许我走错了路。 嗯,这是一个有趣的想法。如果您修改 Factory 以具有一个返回 body 和 shape 对的虚拟方法怎么办?返回的主体在哪里与返回的形状相关联? 不错,明天我会尝试这个想法。我没有提到给定的身体可能有多种形状,但这是一个开始.. 我在这里想到了另一个想法,我认为这更符合我的目标。我将尝试使用一组非常基本的类来表示 body->shape 映射的结构,而不是建立一个将实现表示为派生类的层次结构。在第二遍中,我将遍历此结构并生成对应的“后端”结构,使用特定实现来镜像所需的关系。我只是想弄清楚当结构更新时这是否需要反向指针来提高效率.. 【参考方案1】:分析您的设计问题
您的解决方案实现了abstract factory design pattern,其中:
AFactory
和 BFactory
是抽象 Factory
的具体工厂
一方面ABody
和AShape
,另一方面BBody
和BShape
是抽象产品Body
和Shape
的具体产品。
Axxx 类构成了一系列相关类。 Bxxx 类也是如此。
您担心的问题是Body::setShape()
方法依赖于抽象形状参数,而具体实现实际上需要具体形状。
正如您正确指出的那样,对具体的Shape
的沮丧表明存在潜在的设计缺陷。并且不可能在编译时捕获错误,因为整个模式被设计为在运行时是动态和灵活的,而虚函数不能模板化。
备选方案 1:让您当前的设计更安全
使用dynamic_cast<>
在运行时检查向下转换是否有效。结果:
方案二:采用强隔离设计
更好的设计是隔离不同的产品。所以一个产品类只会使用同族其他类的抽象接口,而忽略它们的具体特性。
后果:
非常健壮的设计实现了卓越的关注点分离 您可以在抽象类级别分解Shape*
成员,甚至可以对setShape()
进行去虚拟化。
但这是以僵化为代价的:你不能使用家庭特定的界面。这可能会很尴尬,例如目标是家庭代表原生 UI,知道产品高度相互依赖并且需要使用原生 API(这是 Gang of 4 书中的典型示例)。
备选方案 3:模板化依赖类型
选择基于模板的抽象工厂实现。一般的想法是,您使用模板实现定义产品之间的内部依赖关系。
因此,在您的示例 Shape 中,AShape
和 BShape
保持不变,因为不依赖于其他产品。但是 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<AShape>
派生来定义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?基本上,我取消了工厂,用户只处理直接创建Body
和Shape
。然后这整件事基本上被传递给 AShape/ABody 创建函数,该函数获取 Body/Shape 对象并将它们转换为 ABody/AShape 对象,并对它们进行操作。对于 BBody/BShape 也是如此。因此,用户永远不会看到实现。以上是关于在平行继承层次结构中通过父母关联孩子的主要内容,如果未能解决你的问题,请参考以下文章