【问题标题】:OOP: self-drawing shapes and barking dogsOOP:自绘形状和吠狗
【发布时间】:2013-01-23 20:47:01
【问题描述】:

我读过的大多数面向对象编程的书籍都使用带有Shape.draw() 成员函数的Shape 类或带有Dog.talk() 成员函数的Dog 类或类似的东西来演示多态性的概念。现在,这对我来说一直是一个困惑的根源,这与多态性无关。

class Dog : public Animal
{  
  public:
  ...
    virtual void talk() { cout << "bark! bark!" << endl; }
  ...
};

虽然这可能是一个简单的示例,但我无法想象在更复杂的应用程序中实现此功能的好方法,其中 Dog.talk() 可能需要访问另一个类的声音子例程,例如播放bark.mp3 而不是使用cout 进行输出。假设我有:

class Audio
{
   public:
   ...
     void playMP3(const string& filename)
   ...
};

在设计时从 Dog.talk() 中访问 Audio.playMP3() 的好方法是什么?将Audio.playMP3() 设为静态?传递函数指针?让Dog.talk() 返回它想要播放的文件名并让程序的另一部分处理它?

【问题讨论】:

标签: c++ oop


【解决方案1】:

一种方法可能是让Dog 构造函数引用Audio 类的实例,因为狗(通常)会发出噪音:

class Dog: public Animal {
public:
    Dog(Audio &a): audio(a) {}
    virtual void talk() { audio.playMP3("bark.mp3"); }
private:
    Audio &audio;
};

你可以这样使用它:

Audio audioDriver;
Dog fido(audioDriver);
fido.talk();

【讨论】:

    【解决方案2】:

    我的解决方案是让 Dog 类在 bark 函数中传递一个音频设备。

    狗应该一直存储指向音频设备的指针,这不是它的职责之一。如果你走这条路,你最终会得到两个对象的构造函数,基本上指向应用程序的所有其余部分(它也需要一个指向渲染器的指针,所以它可以被绘制。它需要一个指向地面的指针,然后输入管理器告诉它去哪里,然后…………疯狂就是这样。

    这些都不属于狗。如果它需要与另一个对象通信,则将该对象传递给需要它的特定方法。

    狗的职责是吠叫。树皮发出声音。所以 bark 方法需要一种方法来产生声音:它必须传递一个对音频对象的引用。整个狗不应该关心或知道这一点。

    class Dog: public Animal {
    public:
        virtual void talk(Audio& a);
    };
    

    按照同样的逻辑,形状不应该自己绘制。渲染器绘制对象,这就是它的用途。矩形对象的职责就是矩形。这个责任的一部分是能够在渲染器希望绘制矩形时将必要的绘图数据传递给它,但绘图本身不是它的一部分。

    【讨论】:

    • 谢谢。献给你们所有人。我研究了 QT 如何处理按钮/小部件自己绘制的问题,并且它们还将 draw() 函数传递给渲染器的指针,就像你描述的那样。现在一切都变得更有意义了。
    【解决方案3】:

    这是一个非常有趣的问题,因为它涉及到设计和抽象的元素。例如,您如何将 Dog 对象放在一起,以便您可以控制它的创建方式?它应该支持哪种音频对象,它应该在 MP3 或 WAV 等中“吠叫”吗?

    关于Inversion of ControlDependency Injection 的内容值得一读,因为您正在考虑的很多问题都经过深思熟虑。有很多含义,例如灵活性、可维护性、测试等。

    【讨论】:

      【解决方案4】:

      回调接口已在其他一些答案中提出,但它有缺点:

      • 许多(可能很重要)不同的类依赖于相同的接口。这些类的不同需求可能会破坏界面的清晰度,从 PlaySound(sound_name) 开始变成 PlaySound(string sound_name, bool reverb, float max_level, vector direction, bool looping, ...) 与一堆其他方法(StopSound 、RestartSound 等)
      • 对音频接口的更改将重建有关音频接口的所有信息(我发现这对 C++ 很重要)
      • 提供的接口只适用于音频系统(嗯,它应该只适用于音频系统)。视频系统和网络系统呢?

      还提到的另一种方法是使音频系统调用静态(或音频系统接口为单例)。这将使狗的构造变得简单(创建狗不再需要音频系统的知识),但不会解决上述任何问题。

      我首选的解决方案是委托。狗定义了它的通用输出接口(IE Bark(t_barkData const& data); Growl(t_growlData const& data))和其他类订阅这个接口。委托系统可能会变得相当复杂,但如果实施得当,调试起来并不比回调接口更难,还能减少重新编译时间并提高可读性。

      重要的一点是,狗的输出接口不需要是狗在构造时提供的单独类。相反,当狗决定要吠叫(或形状决定绘制)时,可以缓存和执行指向狗成员函数的指针。

      一个伟大的通用实现是 QT 的信号和插槽,但自己实现如此强大的东西将被证明是困难的。如果您想在 c++ 中使用类似的简单示例,我会考虑发布一个,但如果您不感兴趣,我不会在星期六抽出时间:)

      代表的一些缺点(我想不到): 1.调用开销,对于每秒发生数千次的事情(渲染引擎中的IE“绘制”操作),必须考虑到这一点。大多数实现比虚函数慢。对于不经常发生的操作,这种开销是完全微不足道的。 2. 代码生成,多半是C++对成员函数的支持有限造成的。模板实际上是实现易于阅读的可移植委托系统的必要条件。

      【讨论】:

        【解决方案5】:

        主要取决于您的应用程序是什么。将函数指针传递给动物不是一个好主意,除非您希望狗和猫使用不同的音频驱动程序。

        使用静态playMP3 方法的方法很好。为您的音频系统使用全局参考非常好。

        【讨论】:

        • 这让测试变得很痛苦。我认为你最好遵循 IoC 模式,并将音频组件注入狗(就像它一样)。在这种情况下测试更容易,因为如果需要,测试可以替代模拟。
        • 再次,我认为取决于您的应用程序。将每个全局引用传递给您最终得到Dog(audio, video, network, inputs, userconfig, context) 的对象,而您想要实现的只是一只小狗跳跃。您可以使用audio.set_driver(mock_driver) 进行测试。
        【解决方案6】:

        一个基本的答案是,Animal 使用 Audio 对象或更复杂的包含多个 Audio 的对象进行初始化。然后,Animal 的谈话函数会调用此 Audio 对象的方法来为动物产生谈话噪音。

        Dog 对象使用 Dogs 的 Audio 对象特征的特定实例初始化 Animal,或者(在更复杂的情况下)获取允许它构建 Audio 对象以传递给 Animal 的参数。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2014-12-06
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多