今天來介紹一個很有用的設計模式,“觀察者模式”,顧名思義,既然有“觀察者”, 那麼一定就有“被觀察者”,從這個層面可以理解到,在這個設計模式中有兩個對象, 一個是主體對象,一個是客體對象,在實際的代碼實現上實際是“被觀察者”主動通知了“觀察者”。“被觀察者”是主體對象,現在看不懂沒關係,把下面的代碼抄幾遍就懂了。
PHP 提供了兩個內置接口來幫助實現“觀察者模式”, 其中“被觀察者”需要實現 SplSubject
接口,“觀察者”需要實現SplOberver
。
來看一個例子:
<?php
class PluginA implements SplObserver {
public function update(\SplSubject $subject) {
echo "Plugin A ".$subject->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了。
看對上一篇修改後的代碼,可以自己對比下,改動並不大:
<?php
/**
* 支持了事件的版本
*/
class PluginA implements SplObserver {
public function update(\SplSubject $subject) {
if($subject->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 的事件是什麼,這樣在插件中就可以根據不同的事件做出相應的處理。
完成了這個之後你會發現一個問題,目前來看我必須將所有的插件實例化後才能註冊到主體對象中,那麼問題來了,如果一個插件,只處理部分事件,那麼如果在整個程序處理過程中這個事件沒有被觸發,那麼這個插件的實例化是不是浪費了?
爲了解決這個問題,我們在下一篇會將代碼做大幅改動,基本是爲了在具體知道那個事件被觸發了以後纔去實例化相應的要處理這個事件的插件。
插件可以根據不同的事件來做不同的業務處理,基本上很簡單,我們直接通過主體對象攜帶事件名稱即可。
下面我們要實現的是,只有在知道具體事件後纔去實例化綁定了此事件的插件。
下面是具體的實現:
<?php
/**
* Author: shanhuhai
* Date: 2017/8/28 21:26
* Mail: [email protected]
*/
class PluginA implements SplObserver {
public function update(\SplSubject $subject) {
if($subject->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
將會被實例化並接收到主體對象傳過來的數據。
在實際應用情況中,插件應該在一個特定目錄中,每個插件是一個文件,以上機制想要應用到你的項目中,還要根據實際情況做相應修改。