本文共 6317 字,大约阅读时间需要 21 分钟。
今天来介绍一个很有用的设计模式,“观察者模式”,顾名思义,既然有“观察者”, 那么一定就有“被观察者”,从这个层面可以理解到,在这个设计模式中有两个对象, 一个是主体对象,一个是客体对象,在实际的代码实现上实际是“被观察者”主动通知了“观察者”。“被观察者”是主体对象,现在看不懂没关系,把下面的代码抄几遍就懂了。
PHP 提供了两个内置接口来帮助实现“观察者模式”, 其中“被观察者”需要实现 SplSubject
接口,“观察者”需要实现SplOberver
。
来看一个例子:
user['name'].' is registering.'.PHP_EOL; }}class PluginB implements SplObserver { public function update(\SplSubject $subject) { echo "Plugin B ".$subject->user['name'].' is registering.'.PHP_EOL; }}class User implements SplSubject { private $observers = []; public $user=[]; public function register($name, $password){ $this->user['name'] = $name; $this->user['password'] = $password; $this->notify(); } public function attach(\SplObserver $observer) { array_push($this->observers, $observer); } public function detach(\SplObserver $observer) { $index = array_search($observer, $this->observers, true); if ($index){ unset($this->observers[$index]); } } public function notify(){ foreach ($this->observers as $observer) { $observer->update($this); } }}//-- 实例化主体对象$user = new User();//-- end//-- 注册插件$plugins = [ new PluginA(), new PluginB(),];foreach ($plugins as $plugin) { $user->attach($plugin);}//-- end//-- 执行主体的业务代码$user->register('shanhuhai', '123456');
将以上代码保存即可运行。
以上代码模拟了一个调用注册方法register
时,通知相关插件执行相应任务的机制。它使用观察者模式实现了插件的机制。 插件对象PluginA
和PluginB
的实例就是“观察者”,User
类的实例 $user
是“被观察者”,“观察者”需要通过 attach
方法注册到“被关观察者” 上, 当register
方法被时,User
会通过notify
方法来通知所有的插件,并将主体对象即被观察者传递给插件。
这里仅仅是通过最简单的代码展示了什么是观察者模式,如果想实现插件,我们应该能传递更多的信息给插件,比如我们想在插件中针对主体对象的发生不同事件(不同的方法被调用)做不同的处理,我们可能需要“事件”的概念,你知道如何实现么?
在实际的业务中,观察者应该对不同的情况做出不同的发应,这个其实很简单,我们加一个给主体对象加一个event
的属性就ok了。
看对上一篇修改后的代码,可以自己对比下,改动并不大:
event == 'register') { echo "Plugin A ".$subject->user['name'].' is registering.'.PHP_EOL; } }}class PluginB implements SplObserver { public function update(\SplSubject $subject) { if($subject->event == 'register'){ echo "Plugin B ".$subject->user['name'].' is registering.'.PHP_EOL; } if($subject->event == 'login') { echo "Plugin B ".$subject->user['name']. ' is logining.'.PHP_EOL; } }}class User implements SplSubject { public $event; private $observers = []; public $user=[]; public function register($name, $password){ $this->user['name'] = $name; $this->user['password'] = $password; $this->event = 'register'; $this->notify(); } public function login($name, $password) { $this->user['name'] = $name; $this->user['password'] = $password; $this->event = 'login'; $this->notify(); } public function attach(\SplObserver $observer) { array_push($this->observers, $observer); } public function detach(\SplObserver $observer) { $index = array_search($observer, $this->observers, true); if ($index){ unset($this->observers[$index]); } } public function notify(){ foreach ($this->observers as $observer) { $observer->update($this); } }}//-- 实例化主体对象$user = new User();//-- end//-- 注册插件$plugins = [ new PluginA(), new PluginB(),];foreach ($plugins as $plugin) { $user->attach($plugin);}//-- end//-- 执行主体的业务代码$user->register('shanhuhai', '123456');$user->login('shanhuhai', '123456');
对比上一篇的代码,我们在notify之前都需要通过 $this->event='事件名'
,来指明本次notify 的事件是什么,这样在插件中就可以根据不同的事件做出相应的处理。
完成了这个之后你会发现一个问题,目前来看我必须将所有的插件实例化后才能注册到主体对象中,那么问题来了,如果一个插件,只处理部分事件,那么如果在整个程序处理过程中这个事件没有被触发,那么这个插件的实例化是不是浪费了?
为了解决这个问题,我们在下一篇会将代码做大幅改动,基本是为了在具体知道那个事件被触发了以后才去实例化相应的要处理这个事件的插件。
插件可以根据不同的事件来做不同的业务处理,基本上很简单,我们直接通过主体对象携带事件名称即可。
下面我们要实现的是,只有在知道具体事件后才去实例化绑定了此事件的插件。
下面是具体的实现:
event == 'register') { echo "Plugin A ".$subject->user['name'].' is registering.'.PHP_EOL; } }}class PluginB implements SplObserver { public function update(\SplSubject $subject) { if($subject->event == 'register'){ echo "Plugin B ".$subject->user['name'].' is registering.'.PHP_EOL; } if($subject->event == 'login') { echo "Plugin B ".$subject->user['name']. ' is logining.'.PHP_EOL; } }}class Observer implements SplObserver { private $config = []; public function __construct($config) { foreach ($config as $plugin => $events ) { foreach ($events as $event) { $this->config[$event][] = $plugin; } } } public function update(\SplSubject $subject) { if(array_key_exists($subject->event, $this->config)) { foreach ($this->config[$subject->event] as $plugin) { $plugin = new $plugin(); $plugin->update($subject); } } }}class User implements SplSubject { public $event; private $observers = []; public $user=[]; public function register($name, $password){ $this->user['name'] = $name; $this->user['password'] = $password; $this->event = 'register'; $this->notify(); } public function login($name, $password) { $this->user['name'] = $name; $this->user['password'] = $password; $this->event = 'login'; $this->notify(); } public function attach(\SplObserver $observer) { //array_push($this->observers, $observer); $this->observer = $observer; } public function detach(\SplObserver $observer) { $this->observer = null; } public function notify(){ $this->observer->update($this); }}//-- 实例化主体对象$user = new User();//-- end//-- 配置插件$config = [ 'PluginA' => ['register' ], 'PluginB' => ['register', 'login'],];//-- end//-- 注册插件$observer = new Observer($config);$user->attach($observer);//-- end//-- 执行主体的业务代码$user->register('shanhuhai', '123456');$user->login('shanhuhai', '123456');
跟上一篇的代码比较,我们增加了一个配置$config
, 用来说明哪些插件类绑定了哪些事件。
Observer
实现了 SplObserver
接口,我们通过给这个类传入 $config
配置来绑定插件和事件的映射关系, 当在主体对象 User
中调用 notify
时,我们调用Observer
的 update
, 这时可以根据之前绑定的插件与事件的映射关系来实例化相应的插件处理事件。 其实观察者模式还叫“发布/订阅模式”, 这跟消息队列中说的“发布/订阅”基本上是一个意思,我们在 $config
的配置说明了订阅关系,哪个插件订阅哪个事件,当被订阅的事件发生时这个插件就会收到消息。比如 PluginA
订阅了 register
事件,当主体对象中 指定的event
等于 register
时, PluginA
将会被实例化并接收到主体对象传过来的数据。
在实际应用情况中,插件应该在一个特定目录中,每个插件是一个文件,以上机制想要应用到你的项目中,还要根据实际情况做相应修改。
转载地址:http://bruws.baihongyu.com/