Tim*_*tro 6 c++ polymorphism design-patterns c++14
TL; DR版本:
我正在设计一个C++ 14中的类是通用的.下面我描述一个设计问题,我将非常感谢能够实现我正在尝试的解决方案或重新设计的建议.
说我正在设计的课程被称为Algo
.它的构造函数被传递unique_ptr
给一个类型,比如说Business
,它实现了一个接口(即,继承自纯虚拟类)并完成了大部分认真的工作.
我希望类型的对象Algo
能够从Business
它拥有的对象返回数据成员的指针(甚至是副本).但它无法知道Business
想要返回的类型.我希望老板Algo
知道根据Business
他传入的内容会发生什么.
在我的C日里,我会通过传递void*并根据需要进行投射来吹掉类型系统.但是现在这种事情对我来说很糟糕.
更多详情:
因此,上述情况的一种伪C++ 14实现可能如下所示:
// perhaps a template here?
class AbstractBusiness {
. . .
public:
?unknownType? result();
};
class Algo {
//Could be public if needbe.
unique_ptr<AbstractBusiness> concreteBusiness_;
public:
Algo(std::unique_ptr<AbstractBusiness> concreteBusiness);
auto result() {return concreteBusiness_.result();}
};
class Business : public AbstractBusiness {
. . .
public:
std::valarray<float> data_;
std::valarray<float> result() {return data_;}
};
:::
auto b = std::unique_ptr<AbstractBusiness>{std::move(new Business())};
Algo a(std::move(b));
auto myResult = a.result();
Run Code Online (Sandbox Code Playgroud)
在这个例子中,myResult将是一个std::valarray<float>
,但我不想要Algo
或AbstractBusiness
接口必须知道!创造者b
和a
应该负责知道应该发生什么a.result()
.
如果我在这个设计中采取了错误的转弯,请不要犹豫,让我知道.在这一点上我有点绿,对建议非常开放.
我试过...... 我显然不能将auto用于虚拟方法,也不能在虚拟类中使用模板.这些是唯一突出的东西.
我正在Business.result()
尝试为任何返回创建容器接口,并将指针传递给抽象类型Algo.result()
.但我开始觉得可能有更好的方法,所以我在这里乞求建议.
您实际上还没有描述设计问题。您描述了您所采用的一些实施选择以及遇到的障碍,但我们不知道这些选择的原因。
您告诉我们,它Algo
通过指向多态接口的指针获取业务的所有权AbstractBusiness
,并且必须为该业务的数据提供 getter,尽管它不知道该数据的具体类型(因为它不知道该业务的具体类型)商业)。
这些问题都没有明显的答案:-
Algo
通过多态接口获取业务?Algo
为其业务的数据提供getter?但决定必须如此会导致障碍。
多态坑洞以及如何摆脱它
Q1. 让我们想知道动机是什么AbstractBusiness
?可以肯定地说,您希望它提供一个统一的接口来操作和查询可能在运行时确定的各种具体类型的业务。
为了完全适合该目的,AbstractBusiness
将封装一个必要且充分的接口,用于执行应用程序(包括但不限于您自己的)可以合理预期需要的对具体业务的所有操作和查询。将此称为 A 计划。您发现它并不完全适合 A 计划。如果应用程序有时需要操作或查询通过 表示给它的业务的“数据” AbstractBusiness
,则AbstractBusiness
接口需要提供多态方法来释放所有这些操作和查询,并且每个具体业务类都需要实现它们适合其包含的数据类型。
你AbstractBusiness
有问题的地方:
?unknownType? result();
Run Code Online (Sandbox Code Playgroud)
您需要编写虚拟方法来解决以下问题的所有令人信服的答案:应用程序可能想了解名义上的 result()
或对其执行什么操作?
有鉴于此,已经讨论过引入另一个多态接口的建议,它AbstractData
是所有具体业务的所有具体类型的祖先,可以被视为通过单独封装它们data
来弥补所缺少的必要方法的建议。AbstractBusiness
在救援抽象中。最好把未完成的事情完成AbstractBusiness
。
也许这一切都很好并且合乎圣经,但也许真正阻止您完成的AbstractBusiness
是这样一种观念,即 的数据BusinessX
可能与的数据本质上不同BusinessY
,因此不可能设计出一套必要且必要的多态方法。足以管理两者。
如果是这样的话,它告诉您业务不能全部通过单个抽象接口进行管理。AbstractBusiness
不能完全适合该目的,并且,如果它有作用,它的作用只能是管理代表更专业的抽象的多态对象,BusinessTypeX
等等BusinessTypeY
,其中每个对象都有具体类型的多样性(如果有的话)可以通过单个多态接口来容纳。
AbstractBusiness
然后将仅呈现所有业务共享的界面。它将result()
根本没有,并且获取指向的指针AbstractBusiness
并打算对返回的内容执行某些操作的调用者BusinessTypeX::result()
将通过动态地将源指针转换为BusinessTypeX *
,并result()
仅在目标指针不为空时才通过目标指针进行调用。
我们仍然不知道其动机是什么AbstractBusiness
。我们刚刚追求了一个相当合理的想法,即您对它有“教科书式”的雄心——A 计划——并且要么没有意识到您只是还没有完成它,要么您已经意识到您正在处理的数据的多样性。处理会阻止您按照 A 计划完成它,并且没有 B 计划。 B 计划是:加深多态层次结构,并在接口超出接口范围时用于dynamic_cast<LowerType *>(HigherType *)
确保对接口的安全访问。[1]LowerType
HigherType
轮到Q2了。现在。最有可能的原因Algo::result()
很简单:因为类已经完成了提供直接回答客户端自然查询的 getters 的任务,在这种情况下,自然查询是针对 Algo
. 但是,如果Algo
只知道其业务为AbstractBusiness
,那么它就无法返回其业务拥有的数据,因为已经看到的原因意味着AbstractBusiness
无法将“数据”返回到Algo
或其他任何东西。
Algo::result()
被误解与AbstractBusiness::result()
被误解是一样的。考虑到BusinessX
s data 和BusinessY
s data 可能需要通过仍然存在TODO
于(计划 A)中的一些虚拟方法来查询,或者可能需要通过根本不继承自 和的AbstractBusiness
方法(计划 B),唯一的查询是当然可以而且应该支持它的业务是返回它拥有其业务的指针,将其留给调用者通过指针进行查询,或者如果可以的话,将其向下转换到他们想要查询的较低类型的接口。即使有可能按照 A 计划完成,缺少的方法报告都应该在接口中复制,以便调用者永远不必接收和向下转换指针,这种想法并不令人信服。所有管理指针的类型都会效仿吗?BusinessX
BusinessY
AbstractBusiness
Algo
AbstractBusiness
AbstractBusiness
Algo
AbstractBusiness
AbstractBusiness
到目前为止,总结一下,如果AbstractBusiness
有充分的理由存在,那么您需要要么按照 A 计划完成它,并解决这样做的后果,要么限制它,使其无法成为管理所有业务和支持的足够接口。它具有丰富的多态层次结构,客户可以根据 B 计划通过动态转换进行协商;无论哪种情况,您都应该满足于行业Algo
中类似的工作价值AbstractBusiness
,只是将其指针返回AbstractBusiness
给有专门用途的客户。
比那更好,不要去那里
但是AbstractBusiness
是否有充分的存在理由的问题仍然悬而未决,如果你发现自己被迫采取 B 计划,这本身就会使问题变得更加尖锐:当它出现一个抽象接口时,将其转换为单个的根类继承层次结构,无法交付 A 计划,那么就会对其所描绘的架构的智慧产生怀疑。检测和获取接口的动态转换是一种笨重且昂贵的流量控制模式,尤其是当您告诉我们您的情况时,必须执行向下转换的繁琐的范围已经知道它应该“退出”的类型是它“放入”的类型。出于接口一致性以外的原因,所有从根抽象不完美派生的类型是否都需要有一个祖先(因为它没有给它们这样的条件)?通用接口的经济性是一个始终存在的目标,但是运行时多态性是在项目上下文中实现它们的正确方法,甚至是正确的方法之一吗?
在您的代码草图中,AbstractBusiness
没有最终目的,而是提供一种可以统一填充 class 中某些槽的类型Algo
,其效果是Algo
可以在表现出某些特征和行为的任何类型上正确运行。正如所描绘的,Algo
限定类型的唯一要求是它应该有一个result()
返回某些内容的方法:它不关心返回什么内容。Algo
但是,您通过指定它应该是 an 来表达对限定类型的要求这一事实AbstractBusiness
禁止它result()
不关心:返回的内容:AbstractBusiness
不能执行该result()
方法,尽管它的任何后代都可以执行该方法。
假设在这种情况下,您放弃AbstractBusiness
了强制执行可操作类型的通用属性的工作,Algo
并Algo
通过将其设为模板来让自己执行此操作?- 因为看起来好像AbstractBusiness
所做的事情Algo
是为了服务模板参数的目的,但却破坏了这个目的:
#include <memory>
template<class T>
class Algo {
std::unique_ptr<T> concreteBusiness_;
public:
explicit Algo(T * concreteBusiness)
: concreteBusiness_{concreteBusiness}{};
auto result() { return concreteBusiness_->result(); }
};
#include <valarray>
#include <algorithm>
struct MathBusiness {
std::valarray<float> data_{1.1,2.2,3.3};
float result() const {
return std::accumulate(std::begin(data_),std::end(data_),0.0);
}
};
#include <string>
struct StringBusiness {
std::string data_{"Hello World"};
std::string result() const { return data_; }
};
#include <iostream>
int main()
{
Algo<MathBusiness> am{new MathBusiness};
auto ram = am.result();
Algo<StringBusiness> as{new StringBusiness};
auto ras = as.result();
std::cout << ram << '\n' << ras << '\n';
return 0;
}
Run Code Online (Sandbox Code Playgroud)
您会看到,通过这种将通用性从 转移AbstractBusiness
到 的方式Algo
,前者完全是多余的,因此被删除。这是模板的引入如何改变 C++ 设计根本和分支的游戏的简单说明,使得多态设计对于大多数先前的通用接口设计应用来说已经过时了。
我们正在根据您的问题背景进行工作:也许仍然存在一些不存在的充分理由AbstractBusiness
。但即使有,它们本身并不构成Algo
不成为模板或对AbstractBusiness
. 而且或许可以通过类似的治疗将它们一一消除。
制作Algo
模板对您来说可能仍然不是一个可行的解决方案,但如果不是,那么问题本质上比我们看到的要多。无论如何,去掉这个经验法则:通用接口的模板;用于接口行为的运行时适应的多态性。
[1]另一个计划可能是将每个具体业务的“数据”封装在 或boost::any
中std::experimental::any
。但您可能会立即看出,这与使用现成的瑞士陆军抽象将数据封装在救援抽象中的想法本质上相同,而不是自己制作。无论哪种形式,这个想法仍然让调用者将抽象降级为真正感兴趣的类型,以查明这是否是他们所拥有的,从这个意义上说,这是 B 计划的一个变体。