2

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

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}then require each fruits to implement virtual FruitType fruitType(),但看来我只是在重新实现一个类型系统。

或者我可以拥有一些功能virtual bool isOrange(); virtual bool isApple(); ...,但正如我们所看到的那样,它很快就会失控。

我也可以使用 C++ typeid,但是这本维基书

RTTI 只能在 C++ 程序中谨慎使用。

所以我不愿意采取这种方法。

看来我在面向对象程序的设计中一定遗漏了一些基本和关键的东西。C++ 都是关于继承和多态的,那么有没有更好的方法来解决这个问题?

process()更新:我喜欢拥有一个所有Fruit都需要实现的通用功能的想法。但是,如果我现在想添加一个Lemon并想榨汁怎么办?我不想复制榨汁代码,所以我应该创建一个class Juicable : public Fruit并同时拥有 Oranges 和 LemonsJuicable吗?

4

5 回答 5

6

告诉,不要问

程序代码获取信息然后做出决定。面向对象的代码告诉对象做事。
— 亚历克夏普

你想定义一个基Fruit类,它有一个虚process方法。苹果、橙子和香蕉将各自实现自己的版本,process该版本对那种水果做正确的事情。您的机器人需要做的就是将下一个Fruit*从篮子中拉出来,然后调用process它:

Robot::processFruit(List<Fruit*> basket)
{
  foreach(Fruit *fruit, basket)
    fruit->process();
}

您在代码示例中所做的那种 if/else/else 处理正是多态性要避免的那种事情,而且绝对不是“OOP”方式。

于 2013-05-17T15:39:21.787 回答
3

我处理这种情况的方法是创建一个名为 Fruit 的超类,并使用一个名为 process() 的纯虚方法。Orange、Apple 和 Banana 都将超类水果,并且它们中的每一个都将提供一个 process() 的实现,该实现对水果的特定类型(即子类)采取适当的行动。

所以你的循环看起来更像:

Robot::processFruit(List<Fruit *> basket)
{
    foreach(Fruit *fruit : basket)
    {
        fruit->process();
    }
}

否则,Robot 的功能基本上需要一种方法来确定它正在处理的水果类型。通常,当我需要这样做时,我只是尝试将 Fruit 指针动态转换为任何其他类型。例如,假设我对一个香蕉对象有一个 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.

但是,在我看来,第一个解决方案更干净,更尊重面向对象的编程。第二种解决方案有时很有用,但应谨慎使用。

更新:如果您真的希望机器人执行处理代码,那么我建议您不要让机器人调用 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);
}
于 2013-05-17T15:42:08.290 回答
2

我认为@meagar 有更好的解决方案,但我想把它放在那里。

Robot::processFruit(List<Fruit*> basket)
{
    foreach(Fruit *fruit, basket)
        fruit->process(*this);
}

Orange::process(Robot & robot) {
    robot.JuiceIt(*this);
}

这个解决方案说水果需要一个机器人来处理它。

于 2013-05-17T16:25:39.077 回答
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) );
}

whereMyHelperFunctor返回一个具有对各种类型FruitRobotcan 句柄的覆盖的函子,并且functions<...>必须足够聪明以测试它支持的每个签名是否可以由传入的函子处理(这是不平凡的部分)并正确调度到他们。

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

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

于 2013-05-17T17:51:10.847 回答
0

这几乎是访问者模式的教科书案例。在下面的示例中,我没有使用标准的访问者模式类或函数名称,而是在这种情况下使用与域相关的名称 - 即FruitProcessorand Process,而不是Visitorand Apply。我不打算进一步评论,因为我认为代码非常自我解释,除了提到这种设计需要在添加更多类型的水果时手动更新 FruitProcessor。有多种处理方法,包括注册水果处理处理程序,甚至使用抽象水果加工厂(参见抽象工厂更多细节)。我想提一下,还有另一种方式绝对需要看一下,它是 Sean Parent 在 boostcon 上提出的:在 Youtube 上看到它:Value Semantics and concept based polymorphism

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 Apple\n";
    }
    virtual void Process(Orange& orange) override
    {
        std::cout << "Juice Orange\n";
    }
};

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;
}
于 2013-05-17T18:13:30.323 回答