我从来就没有太阳 所以不怕失去。
什么是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
class Test {
public function doSomething() { $logger = new Log(); $logger->save('before do something.'); $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
class TestClass {
public function method1($message) { echo "\n", __METHOD__, ":\n", $message, "\n"; }
public function method2() { echo "\n", __METHOD__, ":\n"; return rand(1, 10); }
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
class Log {
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
class Aspect {
private $_className = null;
private $_target = null;
private $_eventCallbacks = array();
public static function addObject($target) { return new Aspect($target); }
public function __construct($target) { if (is_object($target)) { $this->_target = $target; $this->_className = get_class($this->_target); } }
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.'); } }
public function before($methodName, $callback, $args = array()) { $this->_registerEvent('before', $methodName, $callback, (array) $args); }
public function after($methodName, $callback, $args = array()) { $this->_registerEvent('after', $methodName, $callback, (array) $args); }
public function onCatchException($methodName, $callback, $args = array()) { $this->_registerEvent('onCatchException', $methodName, $callback, (array) $args); }
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); } }
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."); } }
public static function getCallbackName($callback) { $className = ''; $methodName = ''; if (is_array($callback) && 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
物件。
利用 before
、 after
和 onCatchException
三个方法来指定切入的时机,它们会呼叫 _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).');
echo "=======\n"; $test->method1('abc'); echo "=======\n"; echo $test->method2(), "\n"; echo "=======\n"; $test->method3(); echo "=======\n";
|
http://rango.swoole.com/archives/83
http://jaceju.net/2008-04-14-php-aop/