面向切面编程AOP

我从来就没有太阳 所以不怕失去。

什么是AOP?

AOP编程,也叫做面向切面编程,是一种非倾入式编程的方法,采用外部注入的方式来取代嵌入代码。可以实现非常好的模块低耦合。
假设你的框架有一个 Frameworkd::init方法,功能是初始化框架资源。现在有db,template的初始化也需要在这个阶段执行,传统的做法就是只能修改 Frameworkd::init在里面加入 db,template的方法调用。未来如果增加了新的模块,比如cache。那就需要修改Frameworkd::init的代码。这种做法显然是侵入性的。
当然也可以用hook list的方式来实现。在需要外部注入的地方加入一个hook list,遍历执行外部注入的接口。但远没有AOP强大,而且还需要不断加入hook list的遍历点。

示例

假设我们要给程序中的每个方法在他们执行前后都要进行日志输出, 一般我们都会这么写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
/**
* Test
*/
class Test
{
/**
* 某个方法
*/
public function doSomething()
{
// 初始化 LOG
$logger = new Log();
// 写log
$logger->save('before do something.');
// 程序功能代码
// ...
// 写log
$logger->save('before do something.');
}
}

可是如果今天这个记录 log 的这个动作只是临时的,或是在未来可能会需要再加入不同的动作时 (例如邮件) ,难道我们还要在原有方法的代码里修修改改吗?有没有什麽方式能协助我们动态地把记录的动作插在原有动作之后呢?

AOP 就是从这个角度所延伸出来的一种观念,它能协助我们在不侵入原有类别程式码的状况下,动态地为类别方法新增额外的权责;简单来说, AOP 主要的目的就是切入类别原有方法执行之前或之后,并安插我们想要执行的动作。

AOP和装饰者模式

其实一开始我以为 AOP 和 Decorator 模式在 PHP 上的实作方式是差不多的,不过实际上还有是些许的差别。
一般在 Decorator 模式中,具体类别和 Wrapper 类别都会有个共同的祖先,亦即一个抽象类别或介面,因此所产生出来的物件对 Client 程式来说,其抽象型态可以说是一样的。

但是在 AOP in PHP 中,我们必须透过一个代理类别来切入原有的类别方法裡,虽然这个代理类别也能够提供原有类别中的所有方法,但是实际上它却已经失去了与原有类别所拥有的抽象型态了。

用PHP实现AOP

首先先建一个类

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
<?php
/**
* Test class
*
*/
class TestClass
{
/**
* Method 1
*
* @param string $message
*/
public function method1($message)
{
echo "\n", __METHOD__, ":\n", $message, "\n";
}
/**
* Method 2
*
* @return int
*/
public function method2()
{
echo "\n", __METHOD__, ":\n";
return rand(1, 10);
}
/**
* Method 3
*
* @throws Exception
*/
public function method3()
{
echo "\n", __METHOD__, ":\n";
throw new Exception('Test Exception.');
}
}

这个类别提供了三个方法,其中 method1 和 method2 只是简单的显示资料而已,而 method3 则会丢出一个异常。

另外我们需要一个 Log 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
/**
* Log
*
*/
class Log
{
/**
* log message
*
* @param string $message
*/
public function save($message)
{
echo $message, "\n";
}
}

这个 Log 类别只提供一个 save() 方法,以显示 log 讯息。

现在我们要完成的目标如下:

在 method1 执行前呼叫 Log::save() 。

在 method2 执行后呼叫 Log::save() 。

在 method3 发生异常时呼叫 Log::save() 。

这里我用很简单的方式来做,那就是直接使用一个 Aspect 类:

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
<?php
/**
* Aspect
*
*/
class Aspect
{
/**
* Name of target class
*
* @var string
*/
private $_className = null;
/**
* Target object
*
* @var object
*/
private $_target = null;
/**
* Event callback
*
* @var array
*/
private $_eventCallbacks = array();
/**
* Add object
*
* @param object $target
* @return Aspect
*/
public static function addObject($target)
{
return new Aspect($target);
}
/**
* Contructor
*
* @param object $target
*/
public function __construct($target)
{
if (is_object($target)) {
$this->_target = $target;
$this->_className = get_class($this->_target);
}
}
/**
* Register event
*
* @param string $eventName
* @param string $methodName
* @param callback $callback
*/
private function _registerEvent($eventName, $methodName, $callback, $args)
{
if (!isset($this->_eventCallbacks[$methodName])) {
$this->_eventCallbacks[$methodName] = array();
}
if (!is_callable(array($this->_target, $methodName))) {
throw new Exception(get_class($this->_target) . '::' . $methodName . ' is not exists.');
}
if (is_callable($callback)) {
$this->_eventCallbacks[$methodName]($eventName) = array($callback, $args);
} else {
$callbackName = Aspect::getCallbackName($callback);
throw new Exception($callbackName . ' is not callable.');
}
}
/**
* Register 'before' handler
*
* @param string $methodName
* @param callback $callback
*/
public function before($methodName, $callback, $args = array())
{
$this->_registerEvent('before', $methodName, $callback, (array) $args);
}
/**
* Register 'after' handler
*
* @param string $methodName
* @param callback $callback
*/
public function after($methodName, $callback, $args = array())
{
$this->_registerEvent('after', $methodName, $callback, (array) $args);
}
/**
* Register 'on catch exception' handler
*
* @param string $methodName
* @param callback $callback
*/
public function onCatchException($methodName, $callback, $args = array())
{
$this->_registerEvent('onCatchException', $methodName, $callback, (array) $args);
}
/**
* Trigger event
*
* @param string $eventName
*/
private function _trigger($eventName, $methodName, $target)
{
if (isset($this->_eventCallbacks[$methodName]($eventName))) {
list($callback, $args) = $this->_eventCallbacks[$methodName]($eventName);
$args[] = $target;
call_user_func_array($callback, $args);
}
}
/**
* Execute method
*
* @param string $methodName
* @param array $args
* @return mixed
*/
public function __call($methodName, $args)
{
if (is_callable(array($this->_target, $methodName))) {
try {
$this->_trigger('before', $methodName, $this->_target);
$result = call_user_func_array(array($this->_target, $methodName), $args);
$this->_trigger('after', $methodName, $this->_target);
return $result ? $result : null;
} catch (Exception $e) {
$this->_trigger('onCatchException', $methodName, $e);
throw $e;
}
} else {
throw new Exception("Call to undefined method {$this->_className}::$methodName.");
}
}
/**
* Get name of callback
*
* @param callback $callback
* @return string
*/
public static function getCallbackName($callback)
{
$className = '';
$methodName = '';
if (is_array($callback) &amp;&amp; 2 == count($callback)) {
if (is_object($callback[0])) {
$className = get_class($callback[0]);
} else {
$className = (string) $callback[0];
}
$methodName = (string) $callback[1];
} elseif (is_string($callback)) {
$methodName = $callback;
}
return $className . (($className) ? '::' : '') . $methodName;
}
}

这个类别有点小长,简单说明如下:

我们利用 Aspect::addObject() 方法来指定要被切入的物件; addObject() 方法会回传一个透明的 Aspect 物件。
利用 beforeafteronCatchException 三个方法来指定切入的时机,它们会呼叫 _registerEvent() 方法来注册要执行的回呼函式 (callback)

执行原来被切入物件的方法,这时会触动 Aspect__call()方法,并在指定的切入时机呼叫 _trigger()` 方法来执行我们所切入的回呼函式。

我们利用 Aspect 类别来对 TestClass 物件的三个方法切入 Log::save() :

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
<?php
require_once 'Aspect.php';
require_once 'TestClass.php';
require_once 'Log.php';
$test = Aspect::addObject(new TestClass());
$logger = new Log();
$test->before('method1', array($logger, 'save'), 'Log saved (method1).');
$test->after('method2', array($logger, 'save'), 'Log saved (method2).');
$test->onCatchException('method3', array($logger, 'save'), 'Log saved (method3).');
/* @var $test TestClass */
echo "=======\n";
$test->method1('abc');
echo "=======\n";
echo $test->method2(), "\n";
echo "=======\n";
$test->method3();
echo "=======\n";
/* 執行結果:
=======
Log saved (method1).
TestClass::method1:
abc
=======
TestClass::method2:
Log saved (method2).
8
=======
TestClass::method3:
Log saved (method3).
Exception: Test Exception. in TestClass.php on line 38
*/

http://rango.swoole.com/archives/83
http://jaceju.net/2008-04-14-php-aop/