问题描述
考虑我有一个 Plant
类,它派生了 Fruit
和 vegetable
类,而 Fruit
类有一些更多的派生类,例如 Orange
和 { {1}},而 Apple
派生了 vegetable
和 Potato
。假设 Tomato
有 Plant
方法:
Plant::onConsume()=0;
假设,我有一个带有 Consumer::consume(Plant) 方法的 class Plant
{
public:
virtual void onConsume(void)=0;
};
class Fruit:public Plant
{
};
class Orange:public Fruit
{
void onConsume(void)
{
// Do something specific here
}
};
class Apple:public Fruit
{
void onConsume(void)
{
// Do something specific here
}
};
class vegetable:public Plant
{
};
class Potato:public vegetable
{
void onConsume(void)
{
// Do something specific here
}
};
class Tomato:public vegetable
{
void onConsume(void)
{
// Do something specific here
}
};
class Consumer
{
public:
void consume(Plant &p)
{
p.onConsume();
// Specific actions depending on actual p type here
// like send REST command to the remote host for Orange
// or draw a red square on the screen for Tomato
}
};
类。这个“consume”方法应该为不同的“Plants”实例/类型执行不同的操作,其中为任何“Plants”调用Plant::onConsume()。这些动作与 Consumer
类没有直接关系,需要很多不同的附加动作和参数,实际上可以是完全任意的,因此无法在 onConsume 方法中实现。
实现这一点的首选方法是什么?据我了解,可以实现一些“Plant::getPlantType()=0”方法,这将返回植物类型,但在这种情况下,我不确定它应该返回什么。如果返回的值是枚举,则每次添加新的派生类时都需要更改此枚举。无论如何,无法控制多个派生类可以返回相同的值。
此外,我知道有一个 dynamic_cast 转换,如果无法进行转换,则返回 nullptr,而 typeid() 运算符返回 std::typeinfo(即使使用 typeinfo::name()),可用于switch() (这对我来说很好)。但我担心它会显着减慢执行速度并使代码更重。
所以,我的问题是,在 C++ 中这样做的首选方法是什么?也许我只是忘记了一些更简单的方法来实现它?
一点点更新。感谢您对继承、封装等的解释!我认为我的问题很清楚,但事实并非如此,我对此感到抱歉。所以,请考虑一下,就像我无法访问整个 Plant
源层次结构一样,只需要实现这个 Consumer::onConsume(Plant)。所以我不能在其中添加新的特定方法。或者,它也可以被视为一个 Plants 库,我必须编写一次,并使其可用于其他开发人员。因此,我可以将用例/功能分为两部分:一个在 Plant::onConsume() 方法中实现“每个类”,第二个是未知的,将根据使用情况而有所不同。
解决方法
一个选项是访问者模式,但这需要某个类中的每种类型一个函数。基本上,您创建一个基类 PlantVisitor
,每个对象类型有一个 Visit
函数,然后将一个虚方法添加到 Plant
,该方法接收 PlantVisitor
对象并调用访问者将自身作为参数传递:
class PlantVisitor
{
public:
virtual void Visit(Orange& orange) = 0;
virtual void Visit(Tomato& tomato) = 0;
...
};
class Plant
{
public:
virtual void Accept(PlantVisitor& visitor) = 0;
};
class Orange : public Plant
{
public:
void Accept(PlantVisitor& visitor) override
{
visitor.Visit(*this);
}
};
class Tomato : public Plant
{
public:
void Accept(PlantVisitor& visitor) override
{
visitor.Visit(*this);
}
};
这将允许您执行以下操作:
class TypePrintVisitor : public PlantVisitor
{
public:
void Visit(Orange& orange) override
{
std::cout << "Orange\n";
}
void Visit(Tomato& tomato) override
{
std::cout << "Tomato\n";
}
};
std::vector<std::unique_ptr<Plant>> plants;
plants.emplace_back(std::make_unique<Orange>());
plants.emplace_back(std::make_unique<Tomato>());
TypePrintVisitor visitor;
for (size_t i = 0; i != plants.size(); ++i)
{
std::cout << "plant " << (i+1) << " is a ";
plants[i]->Accept(visitor);
}
不确定是否需要这样做并不表示设计效率低下。
顺便说一句:如果您有多个访问者并且不一定要为所有访问者中的每一个类型都实现逻辑,您可以在 PlantVisitor
中添加默认实现来调用超类型的函数,而不是指定纯虚函数。
多态就是不必知道特定的类型。如果您发现必须明确检测特定类型,通常您的设计就有缺陷。
一开始:
void Consumer::consume(Plant p)
没有按预期工作! Plant
对象按值接受,即。 e.它的字节被一一复制;但是,仅 Plant
类型,任何其他类型(派生类型的)都将被忽略并丢失在 consume
函数中 - 这称为 object slicing。
多态仅适用于引用或指针。
现在假设您想要执行以下操作(不完整的代码!):
void Consumer::consume(Plant& p) // must be reference or pointer!
{
p.onConsume();
generalCode1();
if(/* p is apple */)
{
appleSpecific();
}
else if(/* p is orange */)
{
orangeSpecific();
}
generalCode2();
}
您不想自己决定类型,而是让 Plant
类为您做事,这意味着您可以适当地扩展其接口:
class Plant
{
public:
virtual void onConsume() = 0;
virtual void specific() = 0;
};
消费函数的代码现在改为:
void Consumer::consume(Plant const& p) // must be reference or pointer!
{
p.onConsume();
generalCode1();
p.specific();
generalCode2();
}
您可以在任何需要特定行为的地方执行此操作(specific
只是一个演示名称,请选择一个能够很好地描述函数实际意图的名称)。
p.onConsume();
generalCode1();
p.specific1();
generalCode2();
p.specific2();
generalCode3();
p.specific3();
generalCode4();
// ...
当然,您现在需要在派生类中提供适当的实现:
class Orange:public Fruit
{
void onConsume() override
{ }
void specific() override
{
orangeSpecific();
}
};
class Apple:public Fruit
{
void onConsume() override
{ }
void specific() override
{
appleSpecific();
}
};
注意添加了 override
关键字,它可以防止您意外创建重载函数,而不是在签名不匹配的情况下实际覆盖。如果您发现必须更改基类中的函数签名,它还可以帮助您找到所有需要更改的位置。