Reputation: 2594
I´m designing a project in C++ and got to the point where I´m in doubt whereas I should use inheritance just to have polymorphism.
Specifically, I have a class JazzQuartet
which has 4 objects: Saxophonist
, Pianist
, Bassist
and Drummer
, each with a play()
and listen()
methods, with different implementations.
I´d like to make them all inherit from a class Musician
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. But as their implementation is completely different from each other, I´d be using this heritage just to get polymorphism, and I´m not sure if that´s a good design choice.
Any advice on this matter?
Thank you all in advance!
Upvotes: 4
Views: 351
Reputation: 11
Application growth is the main factor here. Unless this is for academic practice only (in which case it will have educational value), it will not if the application is not growing. One of the things we have to be careful is to never over-engineer solutions. Our solutions should always be the simplest that addresses the business needs. Why? Simply because, we cannot predict the future and, we need to ensure our efforts go towards solutions which will provide value to our clients and because of that to ourselves. Otherwise, if you have come to a point where your application is growing and the current design is getting in the way, which may be where you are now, we need enhance the design. To answer your question polymorphism could enhance your design in the following areas:
First, Since all musicians will implement the Musician interface, they will all be able to play and listen polymorphically , i.e. you could loop through your array. This is the main benefit of polymorphism.
Second, It will make code reuse and enhancing the application easier. For example, it will make it easy to add musicians in the future. All it takes is to take a look at the interface to know what methods a musician needs to implement. In terms of code reuse, in a larger application, you could reuse most of your code on an orchestra application for instance.
Third, refactoring becomes easier in future iterations. For example, say you need to re-implement the play() method on the Pianist nothing else will need to be changed on JazzQuartet whereas if you leave it as it is you may need to. How? Implementing to an interface is also called design by contract because, it forces the subclass to implement the methods as specified on the interface. You do not have that restriction with your existing design so nothing stops you or another committer from changing the name of the play method to say PianistPlay which will make a change to JazzQuartet necessary.
Fourth, It improves maintainability. It will certainly be more clear to the person maintaining the code that these four objects are related and implement the same interface than if they remain four distinct objects. That is looking at the interface may be enough in most cases to determine how to use or enhance the objects. On the other hand simply using the objects will require to look at each implementation which depending how large the application is growing may be quite tedious. Things may get very complicated when attempting to enhance the unique objects. If you are using an interface and you want to add a new method, you will have to go to each class and implement it, but imagine trying to do the same without the interface, it could be quite similar if using good naming conventions and quite tedious if not, i.e. if the all play methods include the name of the class for example bassistPlay().
Upvotes: 0
Reputation:
You might use 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);
}
This removes the need for any inheritance or (public) (virtual) interfaces. The basic model is more complicated, but realizations of the model are trivial.
Upvotes: 0
Reputation: 1
"... 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
should be an abstract class, i.e. an interface:
class Musician {
public:
virtual void play() = 0;
virtual void listen(Musician& other) = 0;
virtual bool isPlaying() = 0;
virtual ~Musician() {}
};
And yes it's considered good design to promote interfaces.
This way you enforce that derived classes must implement these functions, and allow clients to access Musician
instances, without need to know the concrete derived type.
As you've been asking to store the whole ensemble into an array:
With the above design you can use an array of std::unique_ptr<Musician>
to aggregate a particular ensemble of musicians.
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();
}
Upvotes: 8
Reputation: 3740
You might consider a type erasure pattern. Sean Parent has been advocating using value types, and he often uses type erasure to do this with types that do not have a common base. Overhead is the same as using an interface base class, but if you don't use the types in the context of erasure then there is no overhead.
Upvotes: -1
Reputation: 10007
I see no reason to worry that your Musician
implementations will not have any common code. In fact this is exactly what is called Pure Abstract Class. In general, this is good idea to separate interface concepts.
It gives you more advantages then the ones you have mentioned, most important that you will most probably find that your other code does not need to know what particular type of Musician
it is working with, and thus your main code will be simpler.
And that's also not "just" polymorphism, it also promotes encapsulation a lot as the users will be forced to use Musician
interface.
Also, I think that in future you might find that you actually need some common code between different musicians (say, the director/conductor object reference?).
Upvotes: 7
Reputation: 10559
Just to be advocato diavolo,
you can also use composition - e.g. you have single class Musician and 2 delegate, non virtual methods - listen and play. Then you should have 4 more claeses, each for each type of musicians. Then in Musician constructor you will provide class from those four clases.
But big but - you once again will need base class. And advantages of composition / delegating / strategy "pattern" are doubious, at least in this case.
This only means your current aproach is sound. Go with inheritance :)
Upvotes: 2
Reputation: 6775
This is a perfectly reasonable usage of polymorphism. Saxophonist
, Pianist
, Bassist
, and Drummer
all exhibit an "Is-a" relationship with Musician
.
Your Musician
class would be pure virtual (also called an Interface).
Upvotes: 5