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

What is the right way to use polymorphism and inheritance in C++ to handle a basket of fruit?

本文关键字:一篮 是什么 方法 c++ 多态性 处理 继承      更新时间:2023-10-16

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

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(基于friendprivate的东西)是非法的。

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

void Robot::process( Fruit* fruit ) {
  fruit->typed_dispatch( MyHelperFunctor(this) );
}

其中MyHelperFunctor返回一个函子,该函子覆盖了Robot可以处理的Fruit的各种类型,functions<...>必须足够聪明,以测试它支持的每个签名都可以由传入的函子处理(这是重要的部分),并对它们进行适当的分派。

如果有人从Fruit派生,他们通过FruitImpl来做,这强制它在水果列表中。当水果列表发生变化时,typed_dispatch签名也会发生变化,这就要求CC_30的所有用户实现一个覆盖,可以接受列表中的所有水果类型。

这允许您将Fruitvirtual行为与Fruit实现本身解耦,同时强制执行编译时检查所有类型都被处理。如果它们是Fruit的层次结构,则Robot的helper函函数可以根据该层次结构(在functions中转换为运行时决策)做出编译时决策来调度调用。开销是一个额外的virtual函数调用(对于类型已擦除的functions),以及构造类型已擦除的functions对象。

这几乎是访问者模式的教科书案例。在下面的示例中,我没有使用标准的访问者模式类或函数名,而是使用与本例中的域相关的名称——即FruitProcessorProcess,而不是VisitorApply。我不打算进一步评论,因为我认为代码是相当不言自明的,除了提到这个设计需要手动更新水果处理器时添加更多类型的水果。有多种方法可以处理这个问题,包括注册水果加工处理程序,甚至使用抽象水果加工厂(参见抽象工厂了解更多细节)。我想提一下,还有另一种方式,肯定需要看看,这是由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;
}
相关文章: