设计模式:灵活编程(观察者模式) | BNDong
0%

设计模式:灵活编程(观察者模式)

系统中的每个类应将重点放在某一个功能上,而不是其他方面。一个对象只做一件事情,并且将他做好。

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,有可能导致其它依赖对象的修改更新,那么开发任务会很快变成一个产生bug和消除bug的恶性循环。当我们创建一个对象的时候,一个对象的创建应当尽可能减少和其它对象间的耦合!一个对象的改变尽可能的不会引起代码库其它地方的修改。使用观察者模式能有效的解决此问题,一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知并被自动更新。

观察者模式(有时又被称为模型-视图(View)模式、源-收听者(Listener)模式或从属者模式)是软件设计模式的一种。在此种模式中,一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。这通常通过呼叫各观察者所提供的方法来实现。此种模式通常被用来实现事件处理系统。

问题

假设一个负责处理用户登录的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Login
{
const LOGIN_USER_UNKNOWN = 1;
const LOGIN_WRONG_PASS = 2;
const LOGIN_ACCESS = 3;
private $_status = array();

public function handleLogin($user, $pass, $ip)
{
switch (rand(1,3))
{
case self::LOGIN_ACCESS:
$this->setStatus(self::LOGIN_ACCESS, $user, $ip);
$ret = true; break;
case self::LOGIN_WRONG_PASS:
$this->setStatus(self::LOGIN_WRONG_PASS, $user, $ip);
$ret = false; break;
case self::LOGIN_USER_UNKNOWN:
default:
$this->setStatus(self::LOGIN_USER_UNKNOWN, $user, $ip);
$ret = false; break;
}
return $ret;
}

private function setStatus($status, $user, $ip)
{
$this->_status = array($status, $user, $ip);
}

public function getStatus()
{
return $this->_status;
}
}

$login = new Login();
$login->handleLogin('BNDong', '123456', '127.0.0.1');
var_dump($login->getStatus());

当然这个类并没有实际功能, handleLogin 方法会存储验证用户数据,该方法有3个潜在的结果。状态标签会被设置为 LOGIN_USER_UNKNOWNLOGIN_WRONG_PASSLOGIN_ACCESS

现在看上去还可以,但是一个登录组件可不可能只有这面点东西,我们试着增加需求:(代码的腐败就是不断的迭代出来的)

记录登录IP地址

1
2
3
4
5
6
public function handleLogin($user, $pass, $ip)
{
...
Logger::logIp($user, $ip, $this->getStatus());
...
}

登录失败发送邮件通知管理员

1
2
3
4
5
6
public function handleLogin($user, $pass, $ip)
{
...
!$ret && Notifier::mailWarning($user, $ip, $this->getStatus());
...
}

当然这些都是简单的功能,但是依这种方式来处理 Login 类,会发现该类和系统的依赖越来越深,代码的扩展和复用性越来越差! handleLogin 处理的东西越来越多。

实现

观察者模式的核心是把客户元素(观察者)从一个中心类(主体)中分离开来。当主体知道事件发生时,观察者需要被通知到。同时,我们并不希望将主体与观察者之间的关系进行硬编码。

为了达到这个目的,我们允许观察者在主体上进行注册。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
interface Observable
{
public function attach(Observer $observer);
public function detach(Observer $observer);
public function notify();
}

class Login implements Observable
{
const LOGIN_USER_UNKNOWN = 1;
const LOGIN_WRONG_PASS = 2;
const LOGIN_ACCESS = 3;
private $_status = array();
private $_observers;

public function __construct()
{
$this->_observers = array();
}

public function handleLogin($user, $pass, $ip)
{
switch (rand(1,3))
{
case self::LOGIN_ACCESS:
$this->setStatus(self::LOGIN_ACCESS, $user, $ip);
$ret = true; break;
case self::LOGIN_WRONG_PASS:
$this->setStatus(self::LOGIN_WRONG_PASS, $user, $ip);
$ret = false; break;
case self::LOGIN_USER_UNKNOWN:
default:
$this->setStatus(self::LOGIN_USER_UNKNOWN, $user, $ip);
$ret = false; break;
}
$this->notify();
return $ret;
}

private function setStatus($status, $user, $ip)
{
$this->_status = array($status, $user, $ip);
}

public function getStatus()
{
return $this->_status;
}

public function attach(Observer $observer)
{
$this->_observers[] = $observer;
}

public function detach(Observer $observer)
{
$newobservers = array();
foreach ($this->_observers as $obs) {
if ($obs !== $observer) {
$newobservers[] = $obs;
}
}
$this->_observers = $newobservers;
}

public function notify()
{
foreach ($this->_observers as $obs) {
$obs->update($this);
}
}
}

现在 Login 类管理着一系列观察者对象。这些观察者可以由第三方通过attach 方法添加进Login 类,也可以通过 detach 方法来移除。 notify 方法用来告诉观察者一些相关事情发生了。 notify 方法会遍历观察者列表,调用每个观察者的 update 方法。

Login 类在它的 handleLogin 方法中调用notify 方法。然后定义 Observer 接口,任何实现这个接口的对象都可以通过 attach 方法加入 Login 类中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface Observer
{
public function update(Observable $observable);
}

class SecurityMonitor implements Observer
{
public function update(Observable $observable)
{
$status = $observable->getStatus();
if ($status[0] == Login::LOGIN_WRONG_PASS) {
// 发送邮件给系统管理员
print __CLASS__.":发送邮件给系统给管理员<br>";
}
}
}

$login = new Login();
$login->attach(new SecurityMonitor());
$login->handleLogin('BNDong', '123456', '127.0.0.1');

至此实现了一个观察者模式,减少了各个对象之间的耦合。

优化

这里还存在一个问题,获取主体类状态是通过 getStatus 方法来获取的,而并不能判断调用的 getStatus 方法是存在并且可用的,所以要解决这个问题。

第一种方法:修改接口 Observerupdate 方法参数 $observable 类型约束为 Login ,但是这样整个结构就被一个类限制了,多个登录类不能兼容,所以不推荐!!

第二种方法:在接口 Observable 中添加 getStatus 方法,但是这样会失去接口的通用性!!

第三种方法:继续保持 Observable 接口的通用性,将会添加 Observer 类型的对象来执行一些它们共有的任务。

下面针对第三种方法来优化上面的代码:

使用自建类优化

创建一个抽象超类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
abstract class LoginObserver implements Observer
{
private $_login;

public function __construct(Login $login)
{
$this->_login = $login;
$login->attach($this);
}

public function update(Observable $observable)
{
if ($observable == $this->_login) {
$this->doUpdate($observable);
}
}

abstract protected function doUpdate(Login $login);
}

LoginObserver 类的构造函数需要一个 Login 对象作为参数。 LoginObserver 保存对Login 对象的引用,并且调用 Login::attach() 方法。当 update 方法被调用时, LoginObserver 会检查参数传入的 Observable 对象是否是正确的引用,然后 LoginObserver 会调用模板方法 doUpdate 。现在可以创建一批 LoginObserver 对象,它们能够判断使用的是 Login 对象,而不是任意 Observable 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class SecurityMonitor extends LoginObserver
{
public function doUpdate(Login $login)
{
$status = $login->getStatus();
if ($status[0] == Login::LOGIN_WRONG_PASS) {
// 发送邮件给系统管理员
print __CLASS__.":发送邮件给系统给管理员<br>";
}
}
}

class GeneralLogger extends LoginObserver
{
public function doUpdate(Login $login)
{
$status = $login->getStatus();
// 记录登录数据到日志
print __CLASS__.":记录登录数据到日志<br>";
}
}

$login = new Login();
new SecurityMonitor($login);
new GeneralLogger($login);
$login->handleLogin('BNDong', '123456', '127.0.0.1');

因此在主体类和观察者之间创建了一个很灵活的关系。

使用PHP内置SPL优化

PHP 通过内置的 SPL(Standard PHP Library,PHP标准类)扩展提供了对观察者模式的原生支持。其中的观察者(Observer)由3个元素组成:SplObserverSplSubjectSplObjectStorageSplObserverSplSubject 都是接口,与之前示例中的 ObserverObservable 接口完全相同。SplObjectStorage 是一个工具类,用于更好的存储对象和删除对象。

SplSubject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* The <b>SplSubject</b> interface is used alongside
* <b>SplObserver</b> to implement the Observer Design Pattern.
* @link http://php.net/manual/en/class.splsubject.php
*/
interface SplSubject {

/**
* Attach an SplObserver
* @link http://php.net/manual/en/splsubject.attach.php
* @param SplObserver $observer <p>
* The <b>SplObserver</b> to attach.
* </p>
* @return void
* @since 5.1.0
*/
public function attach (SplObserver $observer);

/**
* Detach an observer
* @link http://php.net/manual/en/splsubject.detach.php
* @param SplObserver $observer <p>
* The <b>SplObserver</b> to detach.
* </p>
* @return void
* @since 5.1.0
*/
public function detach (SplObserver $observer);

/**
* Notify an observer
* @link http://php.net/manual/en/splsubject.notify.php
* @return void
* @since 5.1.0
*/
public function notify ();

}

SplObserver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* The <b>SplObserver</b> interface is used alongside
* <b>SplSubject</b> to implement the Observer Design Pattern.
* @link http://php.net/manual/en/class.splobserver.php
*/
interface SplObserver {

/**
* Receive update from subject
* @link http://php.net/manual/en/splobserver.update.php
* @param SplSubject $subject <p>
* The <b>SplSubject</b> notifying the observer of an update.
* </p>
* @return void
* @since 5.1.0
*/
public function update (SplSubject $subject);

}

SplObjectStorage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
/**
* The SplObjectStorage class provides a map from objects to data or, by
* ignoring data, an object set. This dual purpose can be useful in many
* cases involving the need to uniquely identify objects.
* @link http://php.net/manual/en/class.splobjectstorage.php
*/
class SplObjectStorage implements Countable, Iterator, Traversable, Serializable, ArrayAccess {

/**
* Adds an object in the storage
* @link http://php.net/manual/en/splobjectstorage.attach.php
* @param object $object <p>
* The object to add.
* </p>
* @param mixed $data [optional] <p>
* The data to associate with the object.
* </p>
* @return void
* @since 5.1.0
*/
public function attach ($object, $data = null) {}

/**
* Removes an object from the storage
* @link http://php.net/manual/en/splobjectstorage.detach.php
* @param object $object <p>
* The object to remove.
* </p>
* @return void
* @since 5.1.0
*/
public function detach ($object) {}

/**
* Checks if the storage contains a specific object
* @link http://php.net/manual/en/splobjectstorage.contains.php
* @param object $object <p>
* The object to look for.
* </p>
* @return bool true if the object is in the storage, false otherwise.
* @since 5.1.0
*/
public function contains ($object) {}

/**
* Adds all objects from another storage
* @link http://php.net/manual/en/splobjectstorage.addall.php
* @param SplObjectStorage $storage <p>
* The storage you want to import.
* </p>
* @return void
* @since 5.3.0
*/
public function addAll ($storage) {}

/**
* Removes objects contained in another storage from the current storage
* @link http://php.net/manual/en/splobjectstorage.removeall.php
* @param SplObjectStorage $storage <p>
* The storage containing the elements to remove.
* </p>
* @return void
* @since 5.3.0
*/
public function removeAll ($storage) {}

/**
* Removes all objects except for those contained in another storage from the current storage
* @link http://php.net/manual/en/splobjectstorage.removeallexcept.php
* @param SplObjectStorage $storage <p>
* The storage containing the elements to retain in the current storage.
* </p>
* @return void
* @since 5.3.6
*/
public function removeAllExcept ($storage) {}

/**
* Returns the data associated with the current iterator entry
* @link http://php.net/manual/en/splobjectstorage.getinfo.php
* @return mixed The data associated with the current iterator position.
* @since 5.3.0
*/
public function getInfo () {}

/**
* Sets the data associated with the current iterator entry
* @link http://php.net/manual/en/splobjectstorage.setinfo.php
* @param mixed $data <p>
* The data to associate with the current iterator entry.
* </p>
* @return void
* @since 5.3.0
*/
public function setInfo ($data) {}

/**
* Returns the number of objects in the storage
* @link http://php.net/manual/en/splobjectstorage.count.php
* @return int The number of objects in the storage.
* @since 5.1.0
*/
public function count () {}

/**
* Rewind the iterator to the first storage element
* @link http://php.net/manual/en/splobjectstorage.rewind.php
* @return void
* @since 5.1.0
*/
public function rewind () {}

/**
* Returns if the current iterator entry is valid
* @link http://php.net/manual/en/splobjectstorage.valid.php
* @return bool true if the iterator entry is valid, false otherwise.
* @since 5.1.0
*/
public function valid () {}

/**
* Returns the index at which the iterator currently is
* @link http://php.net/manual/en/splobjectstorage.key.php
* @return int The index corresponding to the position of the iterator.
* @since 5.1.0
*/
public function key () {}

/**
* Returns the current storage entry
* @link http://php.net/manual/en/splobjectstorage.current.php
* @return object The object at the current iterator position.
* @since 5.1.0
*/
public function current () {}

/**
* Move to the next entry
* @link http://php.net/manual/en/splobjectstorage.next.php
* @return void
* @since 5.1.0
*/
public function next () {}

/**
* Unserializes a storage from its string representation
* @link http://php.net/manual/en/splobjectstorage.unserialize.php
* @param string $serialized <p>
* The serialized representation of a storage.
* </p>
* @return void
* @since 5.2.2
*/
public function unserialize ($serialized) {}

/**
* Serializes the storage
* @link http://php.net/manual/en/splobjectstorage.serialize.php
* @return string A string representing the storage.
* @since 5.2.2
*/
public function serialize () {}

/**
* Checks whether an object exists in the storage
* @link http://php.net/manual/en/splobjectstorage.offsetexists.php
* @param object $object <p>
* The object to look for.
* </p>
* @return bool true if the object exists in the storage,
* and false otherwise.
* @since 5.3.0
*/
public function offsetExists ($object) {}

/**
* Associates data to an object in the storage
* @link http://php.net/manual/en/splobjectstorage.offsetset.php
* @param object $object <p>
* The object to associate data with.
* </p>
* @param mixed $data [optional] <p>
* The data to associate with the object.
* </p>
* @return void
* @since 5.3.0
*/
public function offsetSet ($object, $data = null) {}

/**
* Removes an object from the storage
* @link http://php.net/manual/en/splobjectstorage.offsetunset.php
* @param object $object <p>
* The object to remove.
* </p>
* @return void
* @since 5.3.0
*/
public function offsetUnset ($object) {}

/**
* Returns the data associated with an <type>object</type>
* @link http://php.net/manual/en/splobjectstorage.offsetget.php
* @param object $object <p>
* The object to look for.
* </p>
* @return mixed The data previously associated with the object in the storage.
* @since 5.3.0
*/
public function offsetGet ($object) {}

/**
* Calculate a unique identifier for the contained objects
* @link http://php.net/manual/en/splobjectstorage.gethash.php
* @param $object <p>
* object whose identifier is to be calculated.
* @return string A string with the calculated identifier.
* @since 5.4.0
*/
public function getHash($object) {}

}

下面是改进过的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
class Login implements SplSubject
{
const LOGIN_USER_UNKNOWN = 1;
const LOGIN_WRONG_PASS = 2;
const LOGIN_ACCESS = 3;
private $_status = array();
private $_storage;

public function __construct()
{
$this->_storage = new SplObjectStorage();
}

public function handleLogin($user, $pass, $ip)
{
switch (rand(1,3))
{
case self::LOGIN_ACCESS:
$this->setStatus(self::LOGIN_ACCESS, $user, $ip);
$ret = true; break;
case self::LOGIN_WRONG_PASS:
$this->setStatus(self::LOGIN_WRONG_PASS, $user, $ip);
$ret = false; break;
case self::LOGIN_USER_UNKNOWN:
default:
$this->setStatus(self::LOGIN_USER_UNKNOWN, $user, $ip);
$ret = false; break;
}
$this->notify();
return $ret;
}

private function setStatus($status, $user, $ip)
{
$this->_status = array($status, $user, $ip);
}

public function getStatus()
{
return $this->_status;
}

public function attach(SplObserver $observer)
{
$this->_storage->attach($observer);
}

public function detach(SplObserver $observer)
{
$this->_storage->detach($observer);

}

public function notify()
{
foreach ($this->_storage as $obs) {
$obs->update($this);
}
}
}

abstract class LoginObserver implements SplObserver
{
private $_login;

public function __construct(Login $login)
{
$this->_login = $login;
$login->attach($this);
}

public function update(SplSubject $subject)
{
if ($subject == $this->_login) {
$this->doUpdate($subject);
}
}

abstract protected function doUpdate(Login $login);
}

class SecurityMonitor extends LoginObserver
{
public function doUpdate(Login $login)
{
$status = $login->getStatus();
if ($status[0] == Login::LOGIN_WRONG_PASS) {
// 发送邮件给系统管理员
print __CLASS__.":发送邮件给系统给管理员<br>";
}
}
}

class GeneralLogger extends LoginObserver
{
public function doUpdate(Login $login)
{
$status = $login->getStatus();
// 记录登录数据到日志
print __CLASS__.":记录登录数据到日志<br>";
}
}

$login = new Login();
new SecurityMonitor($login);
new GeneralLogger($login);
$login->handleLogin('BNDong', '123456', '127.0.0.1');

参考资料

《深入PHP面向对象、模式与实践》(第三版)

https://baike.baidu.com/item/%E8%A7%82%E5%AF%9F%E8%80%85%E6%A8%A1%E5%BC%8F/5881786?fr=aladdin

http://www.runoob.com/design-pattern/observer-pattern.html

↓赏一个鸡腿... 要不,半个也行↓