Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

OOP: self-drawing shapes and barking dogs

Tags:

c++

oop

Most of the books on object-oriented programming I've read used either a Shape class with a Shape.draw() member function or a Dog class with a Dog.talk() member function, or something similar, to demonstrate the concept of polymorphism. Now, this has been a source of confusion for me, which has nothing to do with polymorphism.

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

While this might work as a simple example, I just can't imagine a good way to make this work in a more complicated application, where Dog.talk() might need to access sound subroutines of another class, e.g. to play bark.mp3 instead of using cout for output. Let's say I have:

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

What would be a good way to access Audio.playMP3() from within Dog.talk() at design time? Make Audio.playMP3() static? Pass around function pointers? Have Dog.talk() return the filename it wants to play and let another part of the program deal with it?

like image 336
deorst Avatar asked Feb 27 '10 20:02

deorst


4 Answers

One way might be to have the Dog constructor take a reference to an instance of an Audio class, because dogs (usually) make noise:

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

You might use it like this:

Audio audioDriver;
Dog fido(audioDriver);
fido.talk();
like image 76
Greg Hewgill Avatar answered Oct 31 '22 22:10

Greg Hewgill


My solution would be for the Dog class to be passed an audio device in the bark function.

The dog should not store a pointer to the audio device all the time, that's not one of its responsibilities. If you go that route, you end up with the constructor taking two dozen objects, essentially pointing to all the rest of the application (it needs a pointer to the renderer too, so it can be drawn. It needs a pointer to the ground, and to the input manager telling it where to go, and........... Madness lies that way.

None of that belongs in the dog. If it needs to communicate with another object, pass that object to the specific method that needs it.

The dog's responsibility is to bark. A bark makes a sound. So the bark method needs a way to generate a sound: It must be passed a reference to an audio object. The dog as a whole shouldn't care or know about that.

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

By the same logic, shapes should not draw themselves. The renderer draws objects, that's what it's for. The rectangle object's responsibility is just to be rectangular. Part of this responsibility is to be able to pass the necessary drawing data to the renderer when it wishes to draw the rectangle, but drawing itself is not part of it.

like image 40
jalf Avatar answered Oct 31 '22 22:10

jalf


This is a really interesting question as it touches on elements of design and abstraction. For example, how do you put a Dog object together so that you retain control over how it is created? What sort of Audio object should it support and should it 'bark' in MP3 or WAV etc?

It's worth reading through a bit about Inversion of Control and Dependency Injection as a lot of the issues you're thinking about have been thought through quite a bit. There are quite a few implications such as flexibility, maintainability, testing etc.

like image 3
Component 10 Avatar answered Oct 31 '22 22:10

Component 10


The callback interface has been suggested in a few of the other answers, but it has drawbacks:

  • Many (potentially significantly) different classes relying on the same interface. These classes different needs may corrupt the clarity of the interface, what started out as PlaySound( sound_name ) becomes PlaySound( string sound_name, bool reverb, float max_level, vector direction, bool looping, ... ) with a bunch of other methods (StopSound, RestartSound, etc etc)
  • Changes to the audio interface will rebuild everything that knows about the audio interface (I find this does matter with C++)
  • The provided interface only works for the audio system (well, it should only be for the audio system). What about the video system, and the networking system?

One alternative that has also been mentioned is to make the audio system calls static (or the audio system interface a singleton). This will keep dog construction simple (creating a dog no longer requires knowledge of the audio system), but doesn't address any of the issues above.

My prefered solution is delegates. The dog defines its generic output interface (IE Bark( t_barkData const& data); Growl( t_growlData const& data ) ) and other classes subscribe to this interface. Delegate systems can become quite complex, but when properly implemented they are no more difficult to debug than a callback interface, reduce recompile times, and improve readability.

An important note is that the dog's output interface does not need to be a separate class that the dog is provided with at construction. Instead pointers to the dogs member functions can be cached and executed when the dog decides it wants to bark (or the shape decides to draw).

A great generic implementation is QT's signals and slots, but implementing something so powerful yourself will prove difficult. If you would like a simple example of something like this in c++ I would consider posting one but if you're not interested I'm not going to take the time out of my Saturday :)

Some drawbacks to delegates (off the top of my head): 1. Call overhead, for things that happen thousands of time a second (IE "draw" operations in a rendering engine) this has to be taken into account. Most implementations are slower than virtual functions. This overhead is utterly insignificant for operations which do not happen extremely frequently. 2. Code generation, mostly the fault of C++'s limited pointer-to-member-function support. Templates are practically a requirement to implement an easy to read portable delegate system.

like image 1
Dan O Avatar answered Oct 31 '22 22:10

Dan O