说我正在编写一个处理一篮子水果的机器人。橙子需要榨汁,苹果需要切片,香蕉需要去皮。幸运的是,我们的机器人已经榨汁,切片和去皮所需的确切工具。
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
,但this wikibook说
RTTI只应在C ++程序中谨慎使用。
所以我不愿采取这种方法。
在面向对象程序的设计中,似乎必须缺少一些基本和关键的东西。 C ++是关于继承和多态的,所以有更好的方法来解决这个问题吗?
更新:我喜欢使用通用process()
函数来实现所有Fruit
。但是,如果我现在想添加一个Lemon
并希望获取该怎么办呢?我不想复制榨汁代码,所以我应该创建一个class Juicable : public Fruit
并且橘子和柠檬都是Juicable
吗?
答案 0 :(得分:6)
请参阅Tell, don't ask:
程序代码获取信息然后做出决定。面向对象的代码告诉对象做事 - 亚历克夏普
您想要定义一个基类Fruit
类,它具有虚拟process
方法。苹果,橘子和香蕉各自都会实现自己的process
版本,为这种水果做正确的事情。你的机器人需要做的就是将下一个Fruit*
拉出篮子,并打电话给process
:
Robot::processFruit(List<Fruit*> basket)
{
foreach(Fruit *fruit, basket)
fruit->process();
}
你在代码示例中执行的if / else / else处理类型正是多态意味着要避免的那种,并且肯定不这样做“OOP”方式。
答案 1 :(得分:3)
我处理这种情况的方法是使用名为process()的纯虚方法创建一个名为Fruit的超类。 Orange,Apple和Banana都将超级水果,并且每个都将提供process()的实现,该方法对该特定类型(即子类)的水果采取适当的行动。
所以你的循环看起来更像是:
Robot::processFruit(List<Fruit *> basket)
{
foreach(Fruit *fruit : basket)
{
fruit->process();
}
}
否则,机器人的功能基本上需要一种方法来确定它正在处理的水果类型。通常当我需要这样做时,我只是尝试使用dynamic_cast&lt;&gt;()将Fruit指针指向其可能的任何其他类型。例如,假设我对一个Banana对象有一个Fruit *。我会利用以下内容:
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.
然而,在我看来,第一个解决方案是更清晰,更尊重面向对象的编程。第二种解决方案有时很有用,但应谨慎使用。
更新:如果你真的想让机器人做处理代码,那么我建议,让你的机器人类实现处理方法,然后让水果本身利用机器人 - 例如:
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);
}
答案 2 :(得分:2)
我认为@meagar有更好的解决方案,但我想把它放在那里。
Robot::processFruit(List<Fruit*> basket)
{
foreach(Fruit *fruit, basket)
fruit->process(*this);
}
Orange::process(Robot & robot) {
robot.JuiceIt(*this);
}
这个解决方案说水果需要一个机器人来处理它。
答案 3 :(得分:1)
如果我真的,真的需要解决这个问题,我无法绕过它...我会编写一个类型感知的双调度处理器,并将其用作参数类型的单一调度仅
首先,编写一个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
返回一个仿函数,其中包含Fruit
Robot
可以处理的各种类型的functions<...>
,并且Fruit
必须足够聪明才能测试每个签名支持可以由传入的仿函数处理(这是非常重要的部分)并对它们进行适当的调度。
如果某人派生自FruitImpl
,则他们会通过typed_dispatch
执行,这会强制它在水果列表中。当水果列表发生变化时,virtual
签名会发生变化,这要求其所有用户都实现覆盖,该覆盖可以接受列表中的每种水果类型。
这允许您在Fruit
实现本身的Robot
处理时解除Fruit
Fruit
行为,同时强制执行所有类型的编译时检查处理。如果它们是Robot
的层次结构,那么functions
的帮助函子可以做出编译时决策,根据层次结构调度调用(在{{1中转换为运行时决策) }})。开销是额外的virtual
函数调用(对于类型擦除的functions
),以及类型擦除的functions
对象的构造。
答案 4 :(得分:0)
这几乎是Visitor模式的教科书案例。在下面的示例中,我没有使用标准访问者模式类或函数名称,而是在这种情况下使用与域相关的名称 - 即FruitProcessor
和Process
,而不是{ {1}}和Visitor
。我不打算进一步评论,因为我认为代码是非常自我解释的,除了提到这个设计需要在添加更多类型的水果时手动更新FruitProcessor。有多种方法可以处理这些问题,包括注册水果加工处理程序,甚至使用抽象水果加工工厂(有关详细信息,请参阅abstract factory)。我想提一下,还有另一种方式肯定需要一看,Sean Parent在boostcon上提出:在Youtube上看到它:Value Semantics and concept based polymorphism
Apply