TL;灾难恢复版本:
我正在C++ 14中设计一个通用的类。下面,我描述一个设计问题,对于实现我正在尝试的解决方案或重新设计的建议,我将不胜感激。
假设我正在设计的类称为Algo
。它的构造函数被传递一个unique_ptr
到一种类型,即Business
,该类型实现一个接口(interface)(即,从纯虚拟类继承)并完成大多数严肃的工作。
我希望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();
在此示例中,myResult将是
std::valarray<float>
,但是我不希望Algo
或AbstractBusiness
接口(interface)必须知道这一点! b
和a
的创建者应该负责知道a.result()
应该产生什么。如果我在这个设计中走错了路,请随时告诉我。在这一点上,我有点绿色,非常愿意提出建议。
我已经尝试过...
我显然不能将auto用于虚拟方法,也不能在虚拟类中使用模板。这些是唯一脱颖而出的东西。
我正在考虑为
Business.result()
返回的任何内容创建一个容器接口(interface),并只是将指向抽象类型的指针传递给Algo.result()
。但是我开始觉得可能有更好的方法,所以我在这里乞求建议。 最佳答案
您实际上并没有描述设计问题。您已经描述了一些
您所经历的实现选择以及所遇到的障碍
遇到,但我们不知道选择的原因。
您告诉我们Algo
通过指向
多态接口(interface)AbstractBusiness
,并且必须为
该业务的数据,尽管它不知 Prop 体的类型
数据(因为它不知 Prop 体的业务类型)。
这些问题都没有明显的答案:
Algo
应该通过多态接口(interface)获取业务? Algo
应该为其业务数据提供 setter/getter ? 但是决定这样做一定会导致障碍。
多态坑洞以及如何消除它
Q1。使我们想知道
AbstractBusiness
的动机是什么?顺便说一句,可以肯定地说,您希望它提供一个统一的界面来处理和查询可能在运行时确定的各种具体类型的业务。为了完全适合该目的,
AbstractBusiness
将封装一个必要且足够的接口(interface),以执行对应用程序(包括但不限于您自己的应用程序)可能合理需要的具体业务的所有操作和查询。将该计划称为A。您发现的是它不完全适合A计划。如果应用程序有时需要操纵或查询通过AbstractBusiness
表示的业务的“数据”,那么AbstractBusiness
接口(interface)需要提供多态方法来执行所有这些操作和查询,并且每个具体的业务类都需要针对其所包含的数据类型适本地实现它们。您的
AbstractBusiness
存在问题的地方:?unknownType? result();
您需要编写虚拟方法来解决该问题的所有令人信服的答案:应用程序可能想了解概念
result()
或对此做些什么?有鉴于此,已经提出来引入另一种多态接口(interface)
AbstractData
的建议,该建议是所有具体业务的所有具体data
类型的祖先,可以被视为一种建议,以补偿AbstractBusiness
缺少的必要方法,例如:将它们分别封装在救援抽象中。最好完成未完成的AbstractBusiness
。也许这一切都很好,而且符合圣经,但是实际上阻止您完成
AbstractBusiness
的事实已经是,人们认为BusinessX
的数据可能与BusinessY
的数据本质上不同,因此不可能设计出一套单一的多态方法来是管理两者的必要条件和充分条件。在这种情况下,它告诉您不能通过单个抽象接口(interface)来全部管理业务。
AbstractBusiness
不能完全满足该目的,如果有作用,它的作用只能是管理表示更专业的抽象,BusinessTypeX
,BusinessTypeY
等的多态对象,其中每个品种(如果有的话)单个多态接口(interface)可以容纳具体类型的对象。AbstractBusiness
将仅显示共享的接口(interface)所有企业。它将完全没有
result()
,并且调用者将获得指向AbstractBusiness
的指针,目的是使用将
BusinessTypeX::result()
返回的内容动态地将源指针强制转换为BusinessTypeX *
,并仅在目标指针不为null时才通过目标指针调用result()
。我们仍然不知道
AbstractBusiness
的动机是什么。我们只是追求一个合理的想法,即您有“教科书”的抱负-计划A-要么没有意识到您还没有完成它,要么您已经意识到您所拥有的数据的多样性与之打交道将使您无法按照计划A来完成它,并且没有计划B。计划B是:加深多态层次结构,并在其dynamic_cast<LowerType *>(HigherType *)
超出其范围时使用LowerType
来确保对HigherType
接口(interface)的安全访问。 [1]Q2轮到。现在。
Algo::result()
的原因很可能很简单:因为类提供直接提供getter的方法已经完成回答客户的自然查询,在这种情况下,自然查询针对的是
Algo
拥有的业务所拥有的数据。但是,如果Algo
仅以AbstractBusiness
的身份知道其业务,则它就无法返回其业务所拥有的数据,因为已经看到的原因意味着AbstractBusiness
不能将“数据”返回给Algo
或任何其他内容。Algo::result()
的误解与AbstractBusiness::result()
的误解相同。考虑到BusinessX
的数据和BusinessY
的数据可能需要通过仍然是TODO
中的AbstractBusiness
的一些虚拟方法(计划A)或通过根本不从BusinessX
继承的BusinessY
和AbstractBusiness
的方法来查询(计划) B),Algo
就其业务当然可以并且应该支持的唯一查询是返回AbstractBusiness
指针通过它拥有自己的业务,将其留给调用方以通过指针进行查询,或者如果可能的话,将其向下转换为要查询的较低类型的接口(interface)。即使有可能按照计划A完成
AbstractBusiness
,也应该在Algo
的接口(interface)中重复所有缺少的方法报告,以使调用者永远不必接收和向下转换AbstractBusiness
指针,这种想法是不令人信服的。管理AbstractBusiness
指针的每种类型都会效仿吗?到目前为止,如果
AbstractBusiness
有充分的理由存在,那么您需要按照计划A完成它,并通过这样做的结果来工作,或者减少它,以免试图成为管理所有业务和业务的足够接口(interface)。根据计划B,通过客户通过动态转换协商的丰富多态层次结构来支持它;无论哪种情况,您都应该对Algo
和AbstractBusiness
交易中的相似Jobsworths感到满意,只是将其AbstractBusiness
指针返回给具有特殊用途的客户。更好的是,不要去那里
但是
AbstractBusiness
是否有充分理由存在的问题仍然悬而未决,您是否应该发现自己被计划B驱使,这本身会使问题变得更加尖锐:当它表现出抽象接口(interface)时,将其转换为a的根类。单一继承层次结构,无法交付计划A,那么人们对其计划架构的智慧就产生了疑问。动态类型转换来检测和获取接口(interface)是一种笨拙且昂贵的流控制方式,尤其是在令人烦恼的情况下(如您所知,您所处的情况),必须执行向下转换rigmarole的范围已经知道应该“退出”的类型是“输入”的类型。除了接口(interface)统一性之外(由于它没有提供这种功能),所有不是完美地源自根抽象的类型都需要具有一个祖先吗?通用接口(interface)的经济性是一个永远存在的目标,但运行时多态性却是在您的项目环境中实现它们的正确方法,甚至是正确的方法之一?
在您的代码草图中,
AbstractBusiness
并没有最终目的,而是提供了一个可以均匀填充类
Algo
中某些插槽的类型,结果Algo
可以在具有某些特征和特性的任何类型上正确运行行为。如图所示,
Algo
对限定类型的唯一要求是它应该具有一个返回某些内容的
result()
方法:不在乎什么。但是,您通过指定合格类型来表达Algo
的要求,这一事实表明,它不关心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;
}
您会看到以这种方式将通用性从
AbstractBusiness
转移到Algo
,前者完全保留了冗余,因此将其删除。这简略地说明了模板的引入如何改变了C++设计根和分支的游戏,从而使多态设计在其通用接口(interface)设计的大多数先前应用中被淘汰了。我们正在从您的问题上下文中进行素描:也许还有很多理由看不到
AbstractBusiness
的存在。但是,即使有,它们本身也不构成Algo
不成为模板或不依赖AbstractBusiness
的原因。也许可以通过类似的方法将它们一一淘汰。对于您来说,将
Algo
变成模板可能仍然不是可行的解决方案,但是如果不是这样,那么问题的实质将比我们所看到的更多。无论如何,这都没有这个经验法则:通用接口(interface)的模板;用于接口(interface)行为的运行时适应的多态性。[1]另一个计划可能看起来是封装每个具体的“数据”
以
boost::any
或std::experimental::any
进行业务。但是你可能可以直截了本地看到这与封装的想法基本相同
使用现成的瑞士陆军抽象将数据以营救抽象的形式呈现,
而不是自己动手制作。无论哪种说法,这个主意仍然存在
调用者将抽象转换为真正感兴趣的类型以找出答案
如果这就是他们所拥有的,从这个意义上讲,它是Plan B的一种变体。
关于c++ - 通过接口(interface)检索未知类型数据的类型安全方法,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/33046691/