在C ++中使用多态和继承来处理一篮子水果的正确方法是什么?

时间:2013-05-17 15:34:56

标签: c++ inheritance polymorphism

说我正在编写一个处理一篮子水果的机器人。橙子需要榨汁,苹果需要切片,香蕉需要去皮。幸运的是,我们的机器人已经榨汁,切片和去皮所需的确切工具。

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吗?

5 个答案:

答案 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非法(基于friendprivate的内容)。

然后,机器人可以去镇上:

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模式的教科书案例。在下面的示例中,我没有使用标准访问者模式类或函数名称,而是在这种情况下使用与域相关的名称 - 即FruitProcessorProcess,而不是{ {1}}和Visitor。我不打算进一步评论,因为我认为代码是非常自我解释的,除了提到这个设计需要在添加更多类型的水果时手动更新FruitProcessor。有多种方法可以处理这些问题,包括注册水果加工处理程序,甚至使用抽象水果加工工厂(有关详细信息,请参阅abstract factory)。我想提一下,还有另一种方式肯定需要一看,Sean Parent在boostcon上提出:在Youtube上看到它:Value Semantics and concept based polymorphism

Apply