c++类设计为每个不同的行为提供多个接口
C++ class design having multiple interfaces for each distinct behavior
这是我的第一篇文章,所以请大家谅解。这是我最近在面试中遇到的一个问题,但是我搜索了(google, c++ FAQ等)都没有找到答案。
存在行为为b1()的接口I1。有A、B、C三个班。所有这些类都通过覆盖b1()来实现接口I1。有第四个类D,它在接口I1中定义了行为(b1)和一个额外的行为b2
问题是你如何设计d类。
我的答案是创建另一个接口I2,它定义行为b2(),并使类D实现I1和I2 (c++中的多重继承)通过覆盖b1()和b2()
面试官同意了这个解决方案,但又问如果将来有新的班级出现新的行为,我们该如何处理
我只能想到添加更多的接口(I3, I4等)和做多重继承,但我知道,在这里你最终在大量的派生类与相应的接口
面试官似乎在期待一个更好的解决方案,但他没有透露答案。我很想知道这里的专家如何解决这个设计问题。PS:在认真思考这个问题之后,我认为答案可能在于使用设计模式,但是查看常见的设计模式,我找不到任何符合这个问题的
编辑1:我有更多的问题和澄清,所以编辑这里的帖子,不确定这是正确的方式还是我需要张贴这个作为我自己问题的答案。
首先让我感谢@Nawaz, @Alexandre和@Sjoerd的宝贵意见。我刚刚开始学习c++/设计模式的设计方面,所以请原谅我对这方面的无知。
@Nawaz的来访者模式的例子确实很有帮助,但我想这只是面试官最初问的问题的一个特例。@Alexandre正确地指出了这里的情况。
让我解释另一个方面。当我们谈论行为时,我们需要根据
对它们进行分组1)与一组对象或对象相关的共同行为。(这是直观的或可以在现实世界中观察到的)一个男人的行为(以@Nawaz为例)——走路、吃饭、学习等等。
2)与群体相关的不寻常或非常奇特的行为(这是反直觉的)为了便于讨论,假设一个作曲的Dude(我知道这个例子并不完美)
3)与群体完全无关的行为。我想不出一个例子,但我的观点是,假设出于某种奇怪的原因,我们需要赋予对象这种行为。
所以我认为访问者模式可以解决1)的问题,但我怀疑它不会为2)和3)。
以IDude为例,我们需要做以下更改来创建一个可以作曲的Dude。
class IComposerBehavior;
class IComposer
{
public:
virtual ~IComposer() {}
virtual void accept(IComposerBehavior *behaviour) = 0 ;
};
class IComposerBehavior
{
public:
virtual ~IComposerBehavior() {}
virtual void visit(IComposer * ) = 0;
};
class Dude : public IDude, public IComposer
{
public:
virtual void accept(IDudeBehavior *behaviour)
{
behaviour->visit(this);
}
virtual void accept(IComposerBehavior *behaviour)
{
behaviour->visit(this);
}
};
class SymphonyComposerBehavior : public IComposerBehavior
{
public:
virtual void visit(IComposer *dude) { cout << "Dude is composing a symphony" << endl; }
};
同样,我们也需要修改客户端代码来考虑SymphonyComposerBehavior
所以基本上我们最终改变了Dude类代码和客户端代码,这否定了模式的效果。我认为面试官问的是新的行为,不能放在一组相关的行为,以前确定。所以在这种情况下,即使类是固定的访问者模式可以解决@Alexandre指出?
让我在这里给出一个例子,只是我的头顶(不确定这是否是一个正确的例子来代表这个问题)。假设我需要为一家机器人制造公司设计一个应用程序。需求逐渐增长
- Initially We are only producing Toy Robots
- Then Human helper Robots
- Then Self Healing Robots (would just correct itself when defective)
- Then Humanoid Robots
- Then machine Robots (that are not human like but as a substitute for any machine you can think of) . I have deliberately put this here even though its place should be before with a correct evolution scheme.
- finally Humanoid Robots with life (atleast we can dream :-) )
因此,如果我们在设计应用程序之前知道完整的机器人列表,我们可以提出更好的设计,但是,当每个新类型按上述顺序依次引入时,我们该如何设计呢?我的观点是,我们知道机器人具有某些行为或特征,但当稍后必须引入不寻常的功能(如自我修复,机器机器人)时,我们该如何处理?
谢谢。
我想面试官希望你谈谈访问者模式。
是的,访问者模式允许您向现有的类结构中添加新的行为,而无需向结构中进一步添加/派生类/接口。它只需要实现行为,而访问者模式允许您将此行为添加到类的结构中。
阅读这个wiki条目;它解释了模式:
- 访问者模式
class IDudeBehavior;
class IDude
{
public:
virtual ~IDude() {}
virtual void accept(IDudeBehavior *behaviour) = 0 ;
};
class IDudeBehavior
{
public:
virtual ~IDudeBehavior() {}
virtual void visit(IDude * ) = 0;
};
class Dude : public IDude
{
public:
virtual void accept(IDudeBehavior *behaviour)
{
behaviour->visit(this);
}
};
class LaughDudeBehavior : public IDudeBehavior
{
public:
virtual void visit(IDude *dude) { cout << "Dude is Laughing" << endl; }
};
class WalkDudeBehavior : public IDudeBehavior
{
public:
virtual void visit(IDude *dude) { cout << "Dude is Walking" << endl; }
};
int main() {
IDude *dude = new Dude();
dude->accept(new LaughDudeBehavior());
dude->accept(new WalkDudeBehavior());
return 0;
}
在线演示:http://ideone.com/Kqqdt
到目前为止,类Dude
只有两个行为,即LaughDudeBehavior
和WalkDudeBehavior
,但由于它是一个访问者模式,您可以向Dude
添加任意数量的行为,而无需编辑类Dude
。例如,如果您想添加EatDudeBehavior
和StudyCplusCplusDudeBehavior
,那么您只需要将IDudeBehavior
实现为:
class EatDudeBehavior : public IDudeBehavior
{
public:
virtual void visit(IDude *dude) { cout << "Dude is Eating" << endl; }
};
class StudyCplusCplusDudeBehavior : public IDudeBehavior
{
public:
virtual void visit(IDude *dude) { cout << "Dude is Studying C++" << endl; }
};
然后你需要接受这些行为
dude->accept(new EatDudeBehavior ());
dude->accept(new StudyCplusCplusDudeBehavior ());
添加这些新行为后的演示:http://ideone.com/9jdEv
避免内存泄漏
上面的代码有一个问题。一切看起来都很好,除了内存泄漏。该程序使用new
创建了许多类的实例,但从未使用delete
释放它们。所以你也需要考虑一下这个
内存泄漏可以很容易地修复:
int main() {
IDude *dude = new Dude();
std::vector<IDudeBehavior*> behaviours;
behaviours.push_back(new LaughDudeBehavior());
behaviours.push_back(new WalkDudeBehavior());
behaviours.push_back(new EatDudeBehavior());
behaviours.push_back(new StudyCplusCplusDudeBehavior());
for(size_t i = 0 ; i < behaviours.size() ; i++ )
dude->accept(behaviours[i]);
//deallcation of memory!
for(size_t i = 0 ; i < behaviours.size() ; i++ )
delete behaviours[i];
delete dude;
return 0;
}
现在没有内存泄漏。
(…)将来会出现带有新行为集的新类
这里有两个不同的东西。新的类和新的行为集。
如果不添加新类,这意味着您有一组固定的类和大量的行为:这使得访问者模式成为潜在的候选者。访问者模式的目标是将行为转换为类,并允许它在其作用的层次结构的类上模式匹配(不强制转换)。
然而,Visitor实现起来很麻烦,并且如果层次结构非常简单(即。只有两个主要分支你想要区分),你最好实现行为作为自由函数,并使用dynamic_cast
来选择对象的行为属于层次结构的哪个分支。查看dynamic_cast
的合理用例。
使用Visitor(或简单的dynamic_cast
调度,如果适用)的真正优点是,与行为相关的完整代码只在一个地方维护。这与接口的情况不同,接口的每个实现可能分散在各种实现文件中。
现在,如果必须添加一堆新类,并且行为集是固定的,那么使用和滥用接口是正确的方法。接口对于抽象行为非常有用。然而,一旦行为数量增加,维护起来就很麻烦,因为代码在各种类实现文件中变得混乱。
参见模板方法模式,它可以应用在这里。
请参见这个关于接口不可思议的有用性的问题。
如果类的数量和行为的数量增加会怎样?恐怕没有独立解决问题的好办法。你唯一的办法就是聪明地抽象,这样不同的行为就不必关心它们作用于哪个类了。
考虑下面的例子。你有n
容器类(vector, list, deque, set等),和m
算法(find_if, count, copy, for_each等)。
你不能在每个容器类中实现每个算法:这意味着你必须编写O(nm)代码。标准库保留的解决方案是抽象结构体的遍历:每个容器类都公开一对迭代器,算法作用于迭代器对。这允许编写O(n + m)个代码。
总之,在状态类和行为数量不断增加的情况下,您必须找到使行为真正独立于状态类的抽象。这是没有设计模式的:你必须动动脑子。
在存在固定数量的状态类和增加数量的行为的情况下,要么正确抽象,要么最后使用Visitor。
当存在越来越多的状态类和固定数量的行为时,使用接口:这就是它们的作用。
- C++核心准则 C35 对于接口类"A base class destructor should be either public and virtual, or protected and nonv
- Visual C++GC接口如何启用它以及要包含哪个库
- Windows.h与GLFW.h的接口
- 当字段可以为null时,如何使用C++接口在Avro中写入数据
- 提供与TMP和SFINAE的通用接口
- 为重写std::exception的库生成swig接口时出错
- 内联如何影响模块接口中的成员函数
- COM 接口 c# 封送数组数组
- 如何在 SCIP C++ 接口中获取 MILP 约束矩阵中的系数值
- 重载 -> shared_ptr 个实例中的箭头运算符<interface>,接口中没有纯虚拟析构函数
- 如何绑定 C++ gRPC 客户端的网络接口
- 模板化接口 - 创建一个泛型模板类以返回任何容器
- 如何从实现接口的模板化类实例访问结构
- 带有进度表的 curl 多接口程序
- 设计帮助 - 为不同类型的消息处理通用接口的设计模式
- 我可以在具有一个标头和一个接口的 cpp 文件中有多个嵌入吗?
- 类接口,可以创建N个方法
- 类具有相同的接口,但参数的类型不同
- 如何与 Cheerp/js 中的 extern 变量接口?
- 如何使用现代 CMake 安装捆绑的接口依赖项?