c++类设计为每个不同的行为提供多个接口

C++ class design having multiple interfaces for each distinct behavior

本文关键字:接口 c++      更新时间:2023-10-16

这是我的第一篇文章,所以请大家谅解。这是我最近在面试中遇到的一个问题,但是我搜索了(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只有两个行为,即LaughDudeBehaviorWalkDudeBehavior,但由于它是一个访问者模式,您可以向Dude添加任意数量的行为,而无需编辑类Dude。例如,如果您想添加EatDudeBehaviorStudyCplusCplusDudeBehavior,那么您只需要将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。

当存在越来越多的状态类和固定数量的行为时,使用接口:这就是它们的作用。