deemen
deemen

Reputation: 118

Interface Bloated with Callbacks

Imagine the following class hierarchy:

interface IRules
{
    void NotifyPickup(object pickedUp);
    void NotifyDeath();
    void NotifyDamage();
}

class CaptureTheFlag : IRules
{
    public void NotifyPickup(Pickup pickedUp)
    {
        if(pickedUp is Flag)
            GameOver();
    }

    public void NotifyDeath()
    {
    }

    public void NotifyDamage()
    {
    }
}

class DeathMatch : IRules
{
    public void NotifyPickup(Pickup pickedUp)
    {
        points++;
    }

    public void NotifyDeath()
    {
        lives--;
    }

    public void NotifyDamage()
    {
    }
}

class GameWorld
{
    IRules gameMode;

    public Main(IRules gameMode)
    {
        this.gameMode = gameMode;
    }

    object[] worldObjects;

    public void GameLoop()
    {
        foreach(object obj in worldObjects)
        {
            // This call may have a bunch of sideeffects, like getting a pickup
            // Or a player dying
            // Or damage being taken
            // Different game modes are interested in different events / statistics.
            obj.Update();


            // Stuff happens...
            gameMode.NotifyDamage();
            // Stuff happens...
            gameMode.NotifyDeath();
        }
    }
}

So here I've got an interface which contains Notify* functions. These are callbacks. Different game modes are interested in different events of the game. It's not really possible to access the concrete objects creating these events because they're buried in the worldObjects array. Imagine we are adding new game modes to our game. The IRules interface will get hugely bloated, containing all the possible things a game mode may be interested in, and most calls will be stubbed! How can I prevent this?

Edit 2: Concrete example

Upvotes: 2

Views: 141

Answers (3)

deemen
deemen

Reputation: 118

My final solution was the C# equivalent of what xtofl posted. I created a class which stored a bunch of delegates in it. These delegates started off with default values (so they would never be null) and the different concrete IRules classes could choose to overwrite them or not. This worked better than abstract or stubbed methods because it doesn't clog the interface with unrelated methods.

class GameEvents
{
    public Action<Player> PlayerKilled = p => {};
    public Func<Entity, bool> EntityValid = e => true;
    public Action ItemPickedUp = () => {};
    public Action FlagPickedUp = () => {};
}

class IRules
{
    GameEvents Events { get; }
}

class CaptureTheFlag : IRules
{
    GameEvents events = new GameEvents();
    public GameEvents Events
    {
        get { return events; }
    }

    public CaptureTheFlag()
    {
        events.FlagPickedUp = FlagPickedUp;
    }

    public void FlagPickedUp()
    {
        score++;
    }
}

Each rule set can choose which events it wants to listen to. The game simply calls then by doing Rules.Events.ItemPickedUp();. It's guaranteed never to be null.

Thanks to xtofl for the idea!

Upvotes: 0

sll
sll

Reputation: 62544

Apologizes if I missed something but why not use event? Basically let IController expose void Callback() method, then Main would be able subscribe any callback to own event:

class Main
{
    private event EventHandler SomeEvent;

    public Main(IController controller)
    {
        // use weak events to avoid memory leaks or
        // implement IDisposable here and unsubscribe explicitly
        this.SomeEvent += controller.Callback;
    }

    public void ProcessStuff()
    {
        // invoke event here
        SomeEvent();
    }        
}

EDIT:

This is what I would do: extract each rule action into the separate interface so you just implement what you need in concrete classes, for instance CaptureTheFlag class does only PickupFlag action for now so does not need Damage/Death methods, so just mark by IPickupable and that's it. Then just check whether concrete instance supports concrete actions and proceed with execute.

interface IPickupable
{
    void NotifyPickup(object pickedUp);
}

interface IDeathable
{
    void NotifyDeath();
}

interface IDamagable
{
    void NotifyDamage();
}    

class CaptureTheFlag : IPickupable
{
    public void NotifyPickup(Pickup pickedUp)
    {
        if (pickedUp is Flag)
            GameOver();
    }
}

class DeathMatch : IPickupable, IDeathable
{
    public void NotifyPickup(Pickup pickedUp)
    {
        points++;
    }

    public void NotifyDeath()
    {
        lives--;
    }
}

class GameWorld
{
    public void GameLoop()
    {       
        foreach(object obj in worldObjects)     
        {
            obj.Update();
            IPickupable pickupable = gameMode as IPickupable;
            IDeathable deathable = gameMode as IDeathable; 
            IDamagable damagable = gameMode as IDamagable;
            if (pickupable != null)
            {
                pickupable.NotifyPickup();
            }

            if (deathable != null)
            {
                deathable.NotifyDeath();
            }

            if (damagable != null)
            {
                damagable.NotifyDamage();
            }                      
         }
     }
}

Upvotes: 1

xtofl
xtofl

Reputation: 41519

Seems like your Process logic sends out a lot of events. If you would give these events a name, you could subscribe your observers to them.

Then it would even be possible to create a 'filtering' observer that can forward the events to any other observer (a decorator pattern):

struct Event {
  enum e { A, B, /*...*/ };
  e name;
};

class IEventListener {
public:
   virtual void event( Event e ) = 0;
};

// an event dispatcher implementation:
using namespace std;

class EventDispatcher {
public:
   typedef std::shared_ptr<IEventListener> IEventListenerPtr;
   map<Event::e,vector<IEventListenerPtr>> listeners;

   void event(Event e){ 
      const vector<IEventListenerPtr> e_listeners=listeners[e.name].second;
      //foreach(begin(e_listeners)
      //       ,end(e_listeners)
      //       ,bind(IEventListener::event,_1,e));
      for(vector<IEventListenerPtr>::const_iterator it=e_listeners.begin()
         ; it!=e_listeners.end()
         ; ++it)
      {
        (*it)->event(e);
      }
   }
};

You program could look like this:

Main main;

EventEventDispatcher f1;

f1.listeners[Event::A].push_back(listener1);

main.listener=f1;

Note: code untested - grab the idea.

If you really want to decouple the sender from the sink, you put an event system in between. The example given here is very dedicated and lightweight, but do sure take a look at various existing implementations: Signals and Slots implemented in Qt and in Boost, the delegates from C#, ...

Upvotes: 1

Related Questions