委托和消息系统

意义和用途

  一个对象内部触发事件影响外部的对象。
  想象不同功能按钮的点击事件。

简单处理多个点击事件

#include <FilePanel.h>
#include <Program.h>

class Button
{
public:
	enum 
	{
		OpenButton,
		QuitButton
	};
	void click()
	{
		switch (_type)
		{
			case OpenButton:
				FilePanel.open();
				break;
			case QuitButton:
				Program.exit();
				break;
		}
	}
private:
	int _type;
};

...
void update()
{
	if (hitTest(button))//某处界面检测时
	{
		button.click();
	}
}

  这样硬编码功能多了以后会写出一个巨大的类,后来的扩展维护会很头疼。

使用继承

class ButtonCallback
{
public:
	virtual void click() = 0;
};

#include <FilePanel.h>
class ButtonOpen: public ButtonCallback
{
public:
	virtual void click()
	{
		FilePanel.open();
	}
};

#include <Program.h>
class ButtonQuit: public ButtonCallback
{
public:
	virtual void click()
	{
		Program.exit();
	}
};

...
void update()
{
	if (hitTest(button))//某处界面检测时
	{
		ButtonCallback* callback = (ButtonCallback*)button;
		callback->click();
	}
}

  比硬编码好得多,每个模块只做自己的事都比较干净,但是只为了一个函数的不同就重载出大量的类,代码会膨胀。

使用委托

class Button
{
public:
	void click()
	{
		if (clicked != NULL)
		{
			clicked();//调用委托
		}
	}
	delegate<void ()> clicked;//定义一个委托
};

Button openButton, quitButton;

void onClickOpenButton()
{
	FilePanel.open();
}

void onClickQuitButton()
{
	Program.exit();
}

openButton.clicked += onClickOpenButton;//连接委托要做的事
quitButton.clicked += onClickQuitButton;

  说到底只是一个点击后做的事的不同,于是把做什么事变成一个接口,然后根据需要来组合出程序,这样的代码结构更清晰简洁。
  此外和委托功能类似的还有一套实现方案叫做信号槽。使用信号槽的例子像这样:

class Button
{
public:
	void click()
	{
		clicked();//发出信号
	}
	signal0<> clicked;//定义一个信号
};

class Window: public has_slots<>
{
public:
	void init()
	{
    	/* 连接信号与槽 */
		_openButton.click.connect(this, &Window::onClickOpenButton);
		_quitButton.click.connect(this, &Window::onClickQuitButton);
	}
	void onClickOpenButton()
	{
		FilePanel.open();
	}
	void onClickQuitButton()
	{
		Program.exit();
	}
private:
	Button _openButton;
	Button _quitButton;
};

  果然和委托很像吧。

使用消息系统

void onOpenFile()
{
	FilePanel.open();
}
Event::addType("OpenFile");
Event::addListener("OpenFile", onOpenFile);

class OpenButton//打开文件的按钮
{
public:
	void click()
	{
		Event::send("OpenFile");
	}
};

class OpenMenuItem//打开文件的菜单项
{
public:
	void select()
	{
		Event::send("OpenFile");
	}
};

  我认为消息系统,是用来从消息的产生、分发和执行的角度考虑来做程序设计的另一套思路。
  比如上面的例子就考虑打开文件消息的产生是从按钮点击或是菜单选择中触发,然后通过事件的名字发布,查找监听这个事件名字的监听者,接着监听者执行指定的动作,完成一个消息分发的流程。
  使用消息系统的好处是让程序里的各种模块更加独立,模块间通过消息系统联系而不直接互相作用,模块之间不需要知道各自的细节,从而实现传说中的低耦合高内聚。无论多复杂的系统,通过消息系统的组织下都会显得更有条理。不过消息系统的缺点是随着消息的增加,需要定义大量的消息类型,每个消息在哪里发送,哪里接收,叫什么名字,是做什么用需要额外费力去管理维护。

委托的简单实现

//成员函数的委托(非C++11的一个方法)
class Object
{};

typedef void (Object::*CallFunc)();

#define callfunc(item) (CallFunc)(&item)

class delegate
{
public:
	delegate():
	_func(NULL),
	_item(NULL){}
	void set(CallFunc func, Object* item)
	{
		_func = func;
		_item = item;
	}
	bool empty()
	{
		return _func == NULL || _item == NULL;
	}
	void execute() 
	{
		(_item->*_func)();
	}
private:
	CallFunc _func;
	Object* _item;
};

class Button: public Object
{
public:
	void click()
	{
		if (!clicked.empty())
		{
			clicked.execute();
		}
	}
	delegate clicked;
};

class Window: public Object
{
public:
	void init()
	{
		_button.clicked.set(callfunc(Window::onButtonClicked), this);
	}
	void onButtonClicked()
	{
		...
	}
private:
	Button _button;
};

  如果想在自己的C++项目中使用委托的话,推荐使用这个更完美的实现。
  当然想使用类似功能的更轻量级的库,可以使用上面提到的信号槽系统,比如这个实现。

消息系统的简单实现

/* 事件参数类,继承它来传递不同的参数 */
class EventArg
	/* 事件的名字 */
	string Name;

/* 事件类型,里面包含一个监听该事件类型的队列 */
class EventType
	/* Listener的队列 */
	vector<EventListener> ListenerList;

/* 事件系统对外的管理接口类,负责新增事件类型,注册事件的监听器 */
class EventManager
	/* string为Key,EventType为Value的字典 */
	unordered_map<string, EventType> TypeMap;

class EventListener
	string Name;//事件的名字
    /* 事件函数的委托,接受的对象为返回值是void,
     有一个参数为EventArg*的函数 */
	delegate<void (EventArg*)> Handler;

数据结构如下:
       Manager
          |
       TypeMap
      /   |   \
  Type0 Type1 Type2
    |
ListenerList
    |      \
Listener0  Listaner1

  EventManager::addType("消息A"); 会向TypeMap中添加一个名为“消息A”的新项。
  EventManager::addListener("消息A", 执行的委托函数); 会在TypeMap中找到名为“消息A”的Type,并向Type的ListenerList中添加新的Listener对象。
  EventManager::send("消息A", 消息A对应的EventArg对象); 会在TypeMap中找到名为“消息A”的Type,然后遍历Type中的ListenerList,调用每个Listener的Handler委托并传入消息参数。

标题目录