在c++中使用多态性和继承来处理一篮子水果的正确方法是什么?
What is the right way to use polymorphism and inheritance in C++ to handle a basket of fruit?
假设我正在编写一个处理一篮水果的机器人。橙子需要榨汁,苹果需要切片,香蕉需要去皮。幸运的是,我们的机器人有榨汁、切片和削皮所需的精确工具。
Robot::processFruit(List<Fruit*> basket)
{
foreach(Fruit *fruit, basket)
{
if( ? ) { juiceIt(fruit); }
else if( ? ) { sliceIt(fruit); }
else if( ? ) { peelIt(fruit); }
}
}
这是我偶尔遇到的一个问题的一般例子。我有一种直觉,我的设计中有一些错误,以至于我甚至导致了一个processFruit()
函数,因为我使用的是面向对象的语言,但它似乎没有一个干净的解决方案来解决这个问题。
我可以创建一个enum FruitType { Orange, Apple, Banana}
,然后要求每个水果实现virtual FruitType fruitType()
,但似乎我只是重新实现一个类型系统。
或者我可以有函数virtual bool isOrange(); virtual bool isApple(); ...
,但正如我们所看到的,它很快就会失控。
我也可以用c++的typeid
,但是这本维基书上说
RTTI应该只在c++程序中少量使用。
所以我不愿意采取那种方法。
看来我一定在面向对象程序的设计中遗漏了一些基本的和关键的东西。c++都是关于继承和多态性的,那么有没有更好的方法来解决这个问题呢?
更新:我喜欢所有Fruit
都需要实现的通用process()
函数的想法。但如果我现在想要添加Lemon
并想要榨汁呢?我不想重复榨汁代码,所以我应该创建一个class Juicable : public Fruit
,让橙子和柠檬都是Juicable
吗?
看,不要问:
过程代码获取信息,然后做出决定。面向对象代码告诉对象去做一些事情。
——Alec Sharp
你想定义一个基本的Fruit
类,它有一个虚拟的process
方法。苹果、橙子和香蕉将各自执行各自版本的process
,为该类型的水果做正确的事情。你的机器人所需要做的就是把下一个Fruit*
从篮子里拉出来,并调用process
:
Robot::processFruit(List<Fruit*> basket)
{
foreach(Fruit *fruit, basket)
fruit->process();
}
你在代码示例中所做的那种if/else/else处理正是多态性所要避免的那种事情,而且它绝对是而不是以"OOP"的方式来做这件事。
我处理这种情况的方法是创建一个名为Fruit的超类,并使用一个名为process()的纯虚拟方法。橙子、苹果和香蕉都将是水果的超类,它们中的每一个都将提供process()的实现,该实现对水果的特定类型(即子类)采取适当的操作。
那么你的循环看起来更像:
Robot::processFruit(List<Fruit *> basket)
{
foreach(Fruit *fruit : basket)
{
fruit->process();
}
}
否则,Robot的函数基本上需要一种方法来确定它正在处理的水果的类型。通常,当我需要这样做时,我只是尝试将Fruit指针dynamic_cast<>()转换为其他类型。例如,假设我有一个Fruit *对象,它是一个Banana。我将利用以下条件:
Fruit *f = new Banana();
Orange *o = dynamic_cast<Orange *>(f); // o is NULL, since f is NOT an Orange.
Banana *b = dynamic_cast<Banana *>(f); // b points to the same object as f now.
然而,在我看来,第一个解决方案要干净得多,也更尊重面向对象编程。第二种方法有时是有用的,但应谨慎使用。
更新:如果你真的想让机器人做处理代码,那么我建议,而不是让机器人调用process(),让你的机器人类实现处理方法,然后让水果本身利用机器人-例如:
Robot::juiceIt(Orange *o)
{
// ...
}
Robot::sliceIt(Apple *a)
{
// ...
}
Robot::peelIt(Banana *b)
{
// ...
}
Orange::process(Robot *r)
{
r->juiceIt(this);
}
Apple::process(Robot *r)
{
r->sliceIt(this);
}
Banana::process(Robot *r)
{
r->peelIt(this);
}
我认为@meagar有更好的解决方案,但我想把这个放在那里。
Robot::processFruit(List<Fruit*> basket)
{
foreach(Fruit *fruit, basket)
fruit->process(*this);
}
Orange::process(Robot & robot) {
robot.JuiceIt(*this);
}
这个解决方案说一个水果需要一个机器人来处理。
如果我真的,真的需要解决这个问题,我不能设计我的方法…我将编写一个类型感知的双分派处理器,并将其用作只对参数类型进行单分派的处理器。
首先,编写一个functions
类,它是一个类型擦除的函数重载集。(这可不是小事!)
typedef functions< void(Apple*), void(Orange*), void(Banana*) > processors;
第二,在编译时列表中维护一组FruitTypes
:
type_list< Apple, Orange, Banana > CanonincalFruitTypes;
typedef functions< void(Apple*), void(Orange*), void(Banana*) > FruitProcessor;
必须在某处维护。这两个列表之间的关系可以通过一些工作自动实现(因此FruitProcessors
是由CanonicalFruitTypes
生成的)
接下来,写一个反射风格的分派:
class Fruit {
public:
virtual void typed_dispatch( FruitProcessor ) = 0;
virtual void typed_dispatch( FruitProcessor ) const = 0;
};
template<typename Derived>
class FruitImpl: public Fruit {
static_assert( std::is_base< Derived, FruitImpl<Derived> >::value, "bad CRTP" );
static_assert( /* Derived is in the Fruit type list */, "add fruit type to fruit list" );
Derived* self() { return static_cast<Derived*>(this); }
Derived const* self() const { return static_cast<Derived*>(this); }
virtual void typed_dispatch( FruitProcessor f ) final overrode {
f(self());
}
virtual void typed_dispatch( FruitProcessor f ) const final overrode {
f(self());
}
};
使用技巧使从Fruit
派生而非通过FruitImpl
(基于friend
和private
的东西)是非法的。
然后,机器人可以去镇上:
void Robot::process( Fruit* fruit ) {
fruit->typed_dispatch( MyHelperFunctor(this) );
}
其中MyHelperFunctor
返回一个函子,该函子覆盖了Robot
可以处理的Fruit
的各种类型,functions<...>
必须足够聪明,以测试它支持的每个签名都可以由传入的函子处理(这是重要的部分),并对它们进行适当的分派。
如果有人从Fruit
派生,他们通过FruitImpl
来做,这强制它在水果列表中。当水果列表发生变化时,typed_dispatch
签名也会发生变化,这就要求CC_30的所有用户实现一个覆盖,可以接受列表中的所有水果类型。
这允许您将Fruit
的virtual
行为与Fruit
实现本身解耦,同时强制执行编译时检查所有类型都被处理。如果它们是Fruit
的层次结构,则Robot
的helper函函数可以根据该层次结构(在functions
中转换为运行时决策)做出编译时决策来调度调用。开销是一个额外的virtual
函数调用(对于类型已擦除的functions
),以及构造类型已擦除的functions
对象。
这几乎是访问者模式的教科书案例。在下面的示例中,我没有使用标准的访问者模式类或函数名,而是使用与本例中的域相关的名称——即FruitProcessor
和Process
,而不是Visitor
和Apply
。我不打算进一步评论,因为我认为代码是相当不言自明的,除了提到这个设计需要手动更新水果处理器时添加更多类型的水果。有多种方法可以处理这个问题,包括注册水果加工处理程序,甚至使用抽象水果加工厂(参见抽象工厂了解更多细节)。我想提一下,还有另一种方式,肯定需要看看,这是由Sean Parent在boostcon上提出的:在Youtube上看到它:值语义和基于概念的多态性
class Apple;
class Orange;
class FruitProcessor
{
public:
virtual void Process(Apple& apple) = 0;
virtual void Process(Orange& orange) = 0;
};
class Fruit
{
public:
virtual void Process(FruitProcessor& proc) = 0;
};
class Apple : public Fruit
{
public:
virtual void Process(FruitProcessor& proc) override
{
proc.Process(*this);
}
};
class Orange : public Fruit
{
public:
virtual void Process(FruitProcessor& proc) override
{
proc.Process(*this);
}
};
class RobotFruitProcessor : public FruitProcessor
{
public:
virtual void Process(Apple& apple) override
{
std::cout << "Peel Applen";
}
virtual void Process(Orange& orange) override
{
std::cout << "Juice Orangen";
}
};
int main()
{
std::vector<Fruit*> fruit_basket;
fruit_basket.push_back(new Apple());
fruit_basket.push_back(new Orange());
RobotFruitProcessor rfp;
for(auto f : fruit_basket)
{
f->Process(rfp);
}
return 0;
}
- 在 c++ 中拥有一组结构的正确方法是什么?
- 在 OpenCV C++ 中估计基本矩阵之前对相应点进行归一化的正确方法是什么?
- 具有相同特征的两个对象是否只在内存中存储一次?无论定义它们的函数是什么,都是不同的
- 在 c++ 或 python 中生成一个体面的视差图以在 Raspberry Pi 上实现的最佳方法(算法或函数)是什么
- 将一种数据类型的向量复制到同一数据类型的结构向量中的有效方法是什么
- 在C++编程中继续下一行的另一种方法是什么?
- 搜索一组点,其长度总和最小为矩形.算法是什么
- 有人能解释一下最多一次不变和存在、所有权和守恒规则是什么吗?
- 访问像 arr[1,2,3] 这样的一维数组C++ - 这是什么意思
- 改进malloc()算法的下一步是什么
- Boost:创建一组线程并等待所有线程的正确习惯用法是什么
- 对于tanh激活函数神经网络,对负/非数值数据进行归一化的最佳方法是什么
- 在给定的 c++ 代码中"delete [] element"以实现一维数组的目的是什么?
- 对一组数字进行排序的最快数据结构(和排序算法)是什么
- 从一组有序对 (x, y) 中找到最长链的最有效算法是什么
- 从一组集合中找出所有不相交集合的算法是什么
- 在一台有n个内核的机器中,确定要启动的线程数的最佳方法是什么?(C++)
- 问答:我如何知道本月的最后一天是什么?
- 学习C++,下一步是什么?还有什么是推荐的编译器?
- 在C++中使用 Scripting.FileSystemObject 时,#import 后的下一步是什么?