仅将继承用于 C++ 中的多态性
Use inheritance only for polymorphism in c++
我正在用 C++ 设计一个项目,到了我怀疑的地步,而我应该使用继承来获得多态性。
具体来说,我有一个 class JazzQuartet
,它有 4 个对象:Saxophonist
、Pianist
、Bassist
和 Drummer
,每个使用 play()
和 listen()
方法,具有不同的实现。
我想让它们都继承自 class Musician
这样我就可以拥有一个 Musician
对象数组并调用每个对象的 play()
和 listen()
方法,因此我可以将任何 Musician
listen()
转换为任何其他方法。但由于它们的实现完全不同,我将使用这种继承来获得多态性,我不确定这是否是一个好的设计选择。
有什么建议吗?
先谢谢大家了!
我认为没有理由担心您的 Musician
实现没有任何通用代码。其实这就是所谓的Pure Abstract Class。一般来说,分离接口概念是个好主意。
它为您提供了比您提到的更多的优势,最重要的是您很可能会发现您的其他代码不需要知道它使用的是什么特定类型的 Musician
,因此你的主要代码会更简单。
而且这也不是 "just" 多态性,它还促进了封装,因为用户将被迫使用 Musician
接口。
此外,我认为将来您可能会发现您实际上需要 不同音乐家之间的一些通用代码(例如,director/conductor 对象引用?)。
这是对多态性的一种非常合理的用法。 Saxophonist
、Pianist
、Bassist
和 Drummer
都表现出与 Musician
的 "Is-a" 关系。
您的 Musician
class 将是纯虚拟的(也称为接口)。
"... so I can have an array of Musician objects and call each one´s play() and listen() methods, and also so I can make any Musician listen() to any other."
Musician
应该是一个抽象 class,即接口:
class Musician {
public:
virtual void play() = 0;
virtual void listen(Musician& other) = 0;
virtual bool isPlaying() = 0;
virtual ~Musician() {}
};
是的,提升界面被认为是好的设计。
通过这种方式,您强制派生的 classes 必须实现这些功能,并允许客户端访问 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();
}
只是为了提倡diavolo,
您还可以使用组合 - 例如你有一个 class 音乐家和 2 个委托,非虚拟方法 - 听和玩。然后你应该有 4 个 claeses,每个 claeses 每种类型的音乐家。然后在 Musician 构造函数中,您将从这四个类中提供 class。
但是很大但是 - 你将再次需要基地 class。组合/委派/策略的优势 "pattern" 是值得怀疑的,至少在这种情况下是这样。
这只说明你目前的做法是正确的。继续继承:)
应用增长是这里的主要因素。除非这仅用于学术实践(在这种情况下它将具有教育价值),否则如果应用程序没有增长则不会。我们必须小心的一件事是永远不要过度设计解决方案。我们的解决方案应该始终是满足业务需求的最简单的解决方案。为什么?仅仅因为,我们无法预测未来,我们需要确保我们的努力用于为我们的客户提供价值的解决方案,并因此为我们自己提供价值。否则,如果您的应用程序正在增长并且当前的设计阻碍了您的发展,这可能就是您现在所处的位置,我们需要增强设计。回答您的问题多态性可以在以下方面增强您的设计:
首先,由于所有音乐家都将实现 Musician 接口,他们都将能够多态地演奏和聆听,即您可以循环遍历您的数组。这是多态的主要好处。
其次,它将使代码重用和增强应用程序变得更加容易。例如,这将使将来添加音乐家变得容易。只需要看一下界面就知道音乐家需要实现哪些方法。在代码重用方面,在较大的应用程序中,您可以在管弦乐队应用程序中重用大部分代码。
第三,重构在未来的迭代中变得更容易。例如,假设您需要在 Pianist 上重新实现 play() 方法,在 JazzQuartet 上不需要更改任何其他内容,而如果您保持原样,则可能需要这样做。如何?实现接口也称为 design by contract 因为,它强制 subclass 实现接口上指定的方法。您现有的设计没有这种限制,因此没有什么可以阻止您或其他提交者将播放方法的名称更改为 PianistPlay,这将对 JazzQuartet 进行必要的更改。
第四,提高了可维护性。维护代码的人肯定会更清楚这四个对象是相关的并实现相同的接口,而不是保留四个不同的对象。在大多数情况下,查看界面可能足以确定如何使用或增强对象。另一方面,简单地使用对象将需要查看每个实现,这取决于应用程序的增长规模可能非常乏味。尝试增强独特对象时,事情可能会变得非常复杂。如果你正在使用一个接口并且你想添加一个新方法,你将不得不去每个 class 并实现它,但是想象一下在没有接口的情况下尝试做同样的事情,如果使用 good 可能会非常相似命名约定,如果不是,则非常乏味,即如果所有播放方法都包含 class 的名称,例如 bassistPlay().
您可以考虑类型擦除模式。 Sean Parent 一直提倡使用值类型,他经常使用类型擦除来处理没有共同基础的类型。开销与使用接口基 class 相同,但如果您不在擦除上下文中使用类型,则没有开销。
您可能会使用 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);
}
这消除了对任何继承或 (public)(虚拟)接口的需要。基本模型比较复杂,但模型的实现很简单。
我正在用 C++ 设计一个项目,到了我怀疑的地步,而我应该使用继承来获得多态性。
具体来说,我有一个 class JazzQuartet
,它有 4 个对象:Saxophonist
、Pianist
、Bassist
和 Drummer
,每个使用 play()
和 listen()
方法,具有不同的实现。
我想让它们都继承自 class Musician
这样我就可以拥有一个 Musician
对象数组并调用每个对象的 play()
和 listen()
方法,因此我可以将任何 Musician
listen()
转换为任何其他方法。但由于它们的实现完全不同,我将使用这种继承来获得多态性,我不确定这是否是一个好的设计选择。
有什么建议吗?
先谢谢大家了!
我认为没有理由担心您的 Musician
实现没有任何通用代码。其实这就是所谓的Pure Abstract Class。一般来说,分离接口概念是个好主意。
它为您提供了比您提到的更多的优势,最重要的是您很可能会发现您的其他代码不需要知道它使用的是什么特定类型的 Musician
,因此你的主要代码会更简单。
而且这也不是 "just" 多态性,它还促进了封装,因为用户将被迫使用 Musician
接口。
此外,我认为将来您可能会发现您实际上需要 不同音乐家之间的一些通用代码(例如,director/conductor 对象引用?)。
这是对多态性的一种非常合理的用法。 Saxophonist
、Pianist
、Bassist
和 Drummer
都表现出与 Musician
的 "Is-a" 关系。
您的 Musician
class 将是纯虚拟的(也称为接口)。
"... so I can have an array of Musician objects and call each one´s play() and listen() methods, and also so I can make any Musician listen() to any other."
Musician
应该是一个抽象 class,即接口:
class Musician {
public:
virtual void play() = 0;
virtual void listen(Musician& other) = 0;
virtual bool isPlaying() = 0;
virtual ~Musician() {}
};
是的,提升界面被认为是好的设计。
通过这种方式,您强制派生的 classes 必须实现这些功能,并允许客户端访问 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();
}
只是为了提倡diavolo,
您还可以使用组合 - 例如你有一个 class 音乐家和 2 个委托,非虚拟方法 - 听和玩。然后你应该有 4 个 claeses,每个 claeses 每种类型的音乐家。然后在 Musician 构造函数中,您将从这四个类中提供 class。
但是很大但是 - 你将再次需要基地 class。组合/委派/策略的优势 "pattern" 是值得怀疑的,至少在这种情况下是这样。
这只说明你目前的做法是正确的。继续继承:)
应用增长是这里的主要因素。除非这仅用于学术实践(在这种情况下它将具有教育价值),否则如果应用程序没有增长则不会。我们必须小心的一件事是永远不要过度设计解决方案。我们的解决方案应该始终是满足业务需求的最简单的解决方案。为什么?仅仅因为,我们无法预测未来,我们需要确保我们的努力用于为我们的客户提供价值的解决方案,并因此为我们自己提供价值。否则,如果您的应用程序正在增长并且当前的设计阻碍了您的发展,这可能就是您现在所处的位置,我们需要增强设计。回答您的问题多态性可以在以下方面增强您的设计:
首先,由于所有音乐家都将实现 Musician 接口,他们都将能够多态地演奏和聆听,即您可以循环遍历您的数组。这是多态的主要好处。
其次,它将使代码重用和增强应用程序变得更加容易。例如,这将使将来添加音乐家变得容易。只需要看一下界面就知道音乐家需要实现哪些方法。在代码重用方面,在较大的应用程序中,您可以在管弦乐队应用程序中重用大部分代码。
第三,重构在未来的迭代中变得更容易。例如,假设您需要在 Pianist 上重新实现 play() 方法,在 JazzQuartet 上不需要更改任何其他内容,而如果您保持原样,则可能需要这样做。如何?实现接口也称为 design by contract 因为,它强制 subclass 实现接口上指定的方法。您现有的设计没有这种限制,因此没有什么可以阻止您或其他提交者将播放方法的名称更改为 PianistPlay,这将对 JazzQuartet 进行必要的更改。
第四,提高了可维护性。维护代码的人肯定会更清楚这四个对象是相关的并实现相同的接口,而不是保留四个不同的对象。在大多数情况下,查看界面可能足以确定如何使用或增强对象。另一方面,简单地使用对象将需要查看每个实现,这取决于应用程序的增长规模可能非常乏味。尝试增强独特对象时,事情可能会变得非常复杂。如果你正在使用一个接口并且你想添加一个新方法,你将不得不去每个 class 并实现它,但是想象一下在没有接口的情况下尝试做同样的事情,如果使用 good 可能会非常相似命名约定,如果不是,则非常乏味,即如果所有播放方法都包含 class 的名称,例如 bassistPlay().
您可以考虑类型擦除模式。 Sean Parent 一直提倡使用值类型,他经常使用类型擦除来处理没有共同基础的类型。开销与使用接口基 class 相同,但如果您不在擦除上下文中使用类型,则没有开销。
您可能会使用 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);
}
这消除了对任何继承或 (public)(虚拟)接口的需要。基本模型比较复杂,但模型的实现很简单。