繁体   English   中英

仅将继承用于c ++中的多态

[英]Use inheritance only for polymorphism in c++

我正在用C ++设计一个项目并且到了我怀疑的地步,而我应该使用继承来获得多态性。

具体来说,我有一个JazzQuartet类,它有4个对象: SaxophonistPianistBassistDrummer ,每个都有play()listen()方法,具有不同的实现。

我想让它们都继承自一个Musician类,所以我可以拥有一系列Musician对象并调用每个人的play()listen()方法,这样我就可以让任何Musician listen()任何Musician其他。 但是由于它们的实现完全不同,我只是为了获得多态性而使用这种遗产,我不确定这是否是一个好的设计选择。

对此事有何建议?

谢谢大家!

“...所以我可以拥有一系列音乐家对象并调用每个人的play()和listen()方法,这样我就可以让任何音乐家听()给任何其他人。”

Musician应该是一个抽象类,即一个接口:

 class Musician {
 public:
     virtual void play() = 0;
     virtual void listen(Musician& other) = 0;
     virtual bool isPlaying() = 0;
     virtual ~Musician() {}
 };

是的,它被认为是促进界面的好设计。

通过这种方式,您可以强制派生类必须实现这些功能,并允许客户端访问Musician实例,而无需了解具体的派生类型。

正如您一直要求将整个集合存储到数组中:

通过上述设计,您可以使用std::unique_ptr<Musician>数组来聚合特定的音乐家群。

std::vector<std::unique_ptr<Musician>> jazzQuartet(4);
std::unique_ptr<Saxophonist> sax = new Saxophonist();
std::unique_ptr<Pianist> piano = new Pianist();
std::unique_ptr<Bassist> bass = new Bassist();
std::unique_ptr<Drummer> drums = new Drummer();

jazzQuartet[0] = sax;
jazzQuartet[1] = piano;
jazzQuartet[2] = bass;
jazzQuartet[3] = drums;

// And wire them up as necessary
//------------------------------------

// Usually everyone in the combo needs to listen to the drums
sax->listen(*drums);
piano->listen(*drums);
bass->listen(*drums);

...

// Let them all play
for(auto& m : jazzQuartet) { // Note the & to avoid copies made for the items
    m->play();
}

我认为没有理由担心你的Musician实现不会有任何通用代码。 事实上,这正是所谓的纯抽象类 通常,分离界面概念是个好主意。

它提供了比你提到的更多的优点,最重要的是你很可能会发现你的其他代码不需要知道它正在使用什么特定类型的Musician ,因此你的主代码会更简单。

而且这也不是“只是”多态,它也会促使封装很多,因为用户将被迫使用Musician界面。

另外,我认为将来你可能会发现你真的需要一些不同音乐家之间的共同代码(比如导演/指挥对象参考?)。

这是多态性的完全合理用法。 SaxophonistPianistBassistDrummer都与Musician展现出“Is-a”的关系。

您的Musician类将是纯虚拟的(也称为接口)。

只是为了提倡diavolo,

你也可以使用作曲 - 例如你有单班音乐家和2名代表,非虚拟方法 - 听和玩。 那么你应该再有4个claeses,每个都为每种类型的音乐家。 然后在Musician构造函数中,您将提供这四个分类的类。

但是很大但是 - 你再次需要基础课。 至少在这种情况下,组合/委派/策略“模式”的优点是双重的。

这只意味着你目前的方法是合理的。 继承继承:)

应用程序增长是这里的主要因素。 除非这仅用于学术实践(在这种情况下它将具有教育价值),否则如果申请没有增长则不会。 我们必须要小心的一件事是永远不要过度设计解决方案。 我们的解决方案应始终是最简单的解决业务需求的解决方案。 为什么? 仅仅因为,我们无法预测未来,我们需要确保我们的努力朝着为客户提供价值并因此为我们自己提供价值的解决方案。 否则,如果您的应用程序正在增长并且当前的设计正在阻碍(可能是您现在所处的位置),我们需要增强设计。 要回答您的问题,多态性可以在以下方面增强您的设计:

首先,由于所有音乐家都将实现音乐家界面,他们都能够多态地播放和收听,即你可以循环播放你的阵列。 这是多态性的主要好处。

其次,它将使代码重用和增强应用程序更容易。 例如,将来可以轻松添加音乐家。 所需要的只是看一下界面,了解音乐家需要实现的方法。 在代码重用方面,在更大的应用程序中,您可以在管道应用程序上重用大部分代码。

第三,在未来的迭代中,重构变得更容易。 例如,假设您需要在钢琴家上重新实现play()方法,在JazzQuartet上不需要更改任何其他内容,而如果您保留原样,则可能需要。 怎么样? 实现到接口也称为契约设计,因为它强制子类实现接口上指定的方法。 您没有对现有设计的限制,因此没有什么能阻止您或其他提交者更改播放方法的名称,以说PianistPlay将更改JazzQuartet是必要的。

第四,它提高了可维护性。 维护代码的人肯定会更清楚这四个对象是相关的并且实现相同的接口,而不是它们保持四个不同的对象。 在大多数情况下,查看界面可能足以确定如何使用或增强对象。 另一方面,简单地使用对象将需要查看每个实现,这取决于应用程序的增长程度可能相当繁琐。 在尝试增强唯一对象时,事情可能变得非常复杂。 如果你正在使用一个接口而你想要添加一个新方法,你将不得不去每个类并实现它,但想象一下如果没有接口就尝试做同样的事情,如果使用良好的命名约定并且非常相似如果不是,则单调乏味,即所有播放方法是否包括类的名称,例如bassistPlay()。

您可以使用Duck Typing:

#include <iostream>
#include <memory>
#include <vector>

class Musician
{
    // Construction
    // ============

    public:
    template <typename T>
    Musician(const T& other) : m_concept(new Model<T>(other))
    {}

    Musician(const Musician& other) : m_concept(other.m_concept->copy())
    {}

    Musician(Musician&& x) = default;

    Musician& operator = (Musician other) {
        m_concept = std::move(other.m_concept);
        return *this;
    }

    // Duck Typing Interface
    // =====================

    public:
    // Enable generic functionality
    friend void play(Musician& musician) { musician.m_concept->play(); }
    // Invoke external functionality
    template <typename T> static void invoke_play(T& musician) { play(musician); }

    // Concept
    // =======

    private:
    struct Concept
    {
        virtual ~Concept() = default;
        virtual Concept* copy() = 0;
        virtual void play() = 0;
    };

    // Model
    // =====

    private:
    template <typename T>
    struct Model : Concept 
    {
        T object;

        Model(const T& other)
        : object(other) {}

        Concept* copy() override {
            return new Model(*this);
        }
        void play() override {
            invoke_play(object);
       }
    };

    private:
    std::unique_ptr<Concept> m_concept;
};


// Test
// ====

class Saxophonist {};
class Pianist {};
class Bassist {};
class Drummer {};

void play(Saxophonist&) { std::cout << "Saxophone\n"; }
void play(Pianist&) { std::cout << "Piano\n"; }
void play(Bassist&) { std::cout << "Bass\n"; }
void play(Drummer&) { std::cout << "Drum\n"; }

using JazzQuartet = std::vector<Musician>;
void play(JazzQuartet& quartet) {
    for (auto& musician : quartet)
        play(musician);
}

int main() {
    JazzQuartet quartet;
    quartet.emplace_back(Saxophonist());
    quartet.emplace_back(Pianist());
    quartet.emplace_back(Bassist());
    quartet.emplace_back(Drummer());
    play(quartet);
}

这消除了对任何继承或(公共)(虚拟)接口的需要。 基本模型更复杂,但模型的实现是微不足道的。

您可能会考虑类型擦除模式。 Sean Parent一直在倡导使用值类型,并且他经常使用类型擦除来使用没有共同基础的类型。 开销与使用接口基类相同,但如果不在擦除上下文中使用类型,则没有开销。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM