tags:

views:

240

answers:

6

Greetings. Most of the books on OOP 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. While...

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

...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 a...

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?

+9  A: 

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();
Greg Hewgill
A: 

Mainly depends on what your application is. Passing function pointers to the animals is not a good idea unless you want dogs and cats to use different audio drivers.

The approach with the static playMP3 method is fine. Using a global reference for your audio system is perfectly fine.

Otto Allmendinger
This makes testing a pain. I think you're better off following an IoC pattern, and inject the audio component into the dog (as it were). Testing is easier in this scenario since the tests can substitute a mock if required.
Brian Agnew
Again, I think depends on your application. passing every global reference to the object you end up with `Dog(audio, video, network, inputs, userconfig, context)` when all you wanted to achieve is a little puppy jumping along. You can do the testing with `audio.set_driver(mock_driver)`.
Otto Allmendinger
A: 

A basic answer is that an Animal gets initialized with either an Audio object or a more complex object that contains multiple Audio's. An Animal's talk function then calls a method on this Audio object to produce the talk noise for the animal.

The Dog object initializes the Animal with a particular instance of an Audio object characteristic of Dogs, or (in more complex cases) takes parameters that allow it to build the Audio object to pass to Animal.

swestrup
+3  A: 

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.

Robin Welch
+1  A: 

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.

Dan O
+9  A: 

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.

jalf
Thanks. To all of you. I've looked into how QT handles the issue of buttons/widgets drawing themselves and they also pass the draw() function a pointer to the renderer, just like you described. It all makes more sense now.
fbcocq