【问题标题】:Interface Bloated with Callbacks接口因回调而臃肿
【发布时间】:2012-06-29 18:46:42
【问题描述】:

想象以下类层次结构:

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();
        }
    }
}

所以这里我有一个包含 Notify* 功能的界面。这些是回调。不同的游戏模式对游戏的不同事件感兴趣。实际上不可能访问创建这些事件的具体对象,因为它们被埋在 worldObjects 数组中。想象一下,我们正在为我们的游戏添加新的游戏模式。 IRules 接口将变得非常臃肿,包含游戏模式可能感兴趣的所有可能的东西,并且大多数调用将被存根! 如何防止这种情况发生?

编辑 2:具体示例

【问题讨论】:

    标签: oop design-patterns interface dependency-injection


    【解决方案1】:

    似乎您的Process 逻辑发出了很多事件。如果你想给这些事件一个名字,你就可以让你的观察者订阅它们。

    然后甚至可以创建一个“过滤”观察者,可以将事件转发给任何其他观察者(装饰器模式):

    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);
          }
       }
    };
    

    您的程序可能如下所示:

    Main main;
    
    EventEventDispatcher f1;
    
    f1.listeners[Event::A].push_back(listener1);
    
    main.listener=f1;
    

    注意:代码未经测试 - 抓住这个想法。

    如果您真的想将发送者与接收器分离,您可以在两者之间放置一个事件系统。此处给出的示例非常专用且轻量级,但请务必查看各种现有实现:在 Qt 和 Boost 中实现的信号和插槽,来自 C# 的代表,...

    【讨论】:

    • 我的C++有点生疏了,让我仔细看看这个。我还发布了一个更具体的示例,以帮助澄清问题。
    • 这个设计非常酷,将所有复杂性都转移到了事件调度器(我可以在其中列出我想要监听的事件),然后我可以轻松地添加新事件而无需修改界面。整洁!
    • @deemen:我使用了顶级 C++11 变体。您还需要一些额外的包含(&lt;map&gt;、&lt;vector&gt;、&lt;memory&gt;)和foreach 可能通过简单的for 循环更容易掌握......我会重写它。
    【解决方案2】:

    如果我错过了什么道歉,但为什么不使用事件?基本上让IController 暴露void Callback() 方法,然后Main 将能够订阅任何回调到自己的事件:

    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();
        }        
    }
    

    编辑:

    这就是我要做的:将每个规则动作提取到单独的接口中,这样你就可以在具体的类中实现你需要的东西,例如 CaptureTheFlag 类现在只执行 PickupFlag 动作,所以不需要伤害/死亡方法,所以只需标记IPickupable 就可以了。然后只需检查具体实例是否支持具体操作并继续执行。

    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();
                }                      
             }
         }
    }
    

    【讨论】:

    • 这与方法调用有何不同?所有事件定义必须仍在界面中。
    • 据我了解,每个实现IController 的类实际上只实现了一个Notify() 方法,其他的都是空的?
    • 不一定,这取决于字母类对什么感兴趣。您可以拥有实现 NotifyA 和 NotifyB 的类 F。
    • NotifyA&&NotifyB 在F 中实现时会做什么? F 是否引用了 A 和 B 的实例来通知他们?这里肯定有问题,但我不确定我对所有这些东西有什么想法,请提供一些对 A 和 B 逻辑的真实描述的 cmets,无法想象这种设计背后的真实商业案例。
    • 为你添加了一个具体的例子!
    【解决方案3】:

    我的最终解决方案是与 xtofl 发布的 C# 等价的解决方案。我创建了一个类,其中存储了一堆代表。这些委托从默认值开始(因此它们永远不会为空),不同的具体 IRules 类可以选择是否覆盖它们。这比抽象或存根方法更有效,因为它不会用不相关的方法阻塞接口。

    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++;
        }
    }
    

    每个规则集都可以选择要监听的事件。游戏只需调用 Rules.Events.ItemPickedUp();。保证永远不会为空。

    感谢 xtofl 的创意!

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-11-23
      • 2011-03-18
      • 2015-03-17
      • 1970-01-01
      • 2011-03-01
      • 1970-01-01
      • 2014-12-30
      • 1970-01-01
      相关资源
      最近更新 更多