寫在前面
時不時地總會在各種地方看到 AOP,wiki 也查了不止一次,但每次都對那一堆陌生術語望而卻步,這次總算下決心要嘗試 AOP 了
最後發現,AOP 類似於設計模式,不同於 策略模式、模板方法模式、裝飾者模式。AOP 的近親是 代理模式,同樣能夠分離邏輯,核心也是攔截與細節隱藏
P.S. 搬出來這麼多名詞其實不是故意的,因為在理解 AOP 的過程中確實有對比思考過這幾個模式。然後,發現設計模式這種東西,嗯,怎麼說呢,有用嗎?沒有用嗎?額
一、術語(Glossary)
- 切面(Aspect)
在 AOP 中表示為「在哪裡做和做什麼的集合」
橫切關注點的模塊化,比如上邊提到的日誌組件。可以認為是增強、引入和切入點的組合。例如日誌、緩存、傳輸管理
- Join point(連接點)
在 AOP 中表示為「在哪裡做」
表示需要在程序中插入橫切關注點的擴展點,連接點可能是類初始化、方法執行、方法調用、字段調用或處理異常等等。表示執行期的一個點,例如方法執行或者屬性訪問
- 增強(Advice)
在 AOP 中表示為「做什麼」
或稱為增強在連接點上執行的行為,增強提供了在 AOP 中需要在切入點所選擇的連接點處進行擴展現有行為的手段。包括前置增強(before advice)、後置增強 (after advice)、環繞增強(around advice)。表示切面在特定連接點處的動作
- 切入點(Pointcut)
在 AOP 中表示為「在哪裡做的集合」
選擇一組相關連接點的模式,即可以認為連接點的集合,Spring 支持 perl5 正則表達式和 AspectJ 切入點模式,Spring 默認使用 AspectJ 語法。用來匹配連接點的正則表達式,增強都有相關的切入點表達式,在任何與之匹配的連接點處執行,例如,某個特定名稱的方法的執行
- 引入(Introduction)
在 AOP 中表示為「做什麼(新增什麼)」
也稱為內部類型聲明(inter-type declaration),為已有的類添��額外新的字段或方法
- Weaving(織入)
把切面和其它應用程序類型或者對象鏈接起來,以創建增強對象
織入是一個過程,是將切面應用到目標對象從而創建出 AOP 代理對象的過程,織入可以在編譯期、類裝載期、運行期進行
- 目標對象(Target Object)
在 AOP 中表示為「對誰做」
需要被織入橫切關注點的對象,即該對象是切入點選擇的對象,需要被增強的對象,從而也可稱為「被增強對象」
- AOP 代理(AOP Proxy)
AOP 框架使用代理模式創建的對象,從而在連接點處插入增強(即應用切面),就是通過代理來對目標對象應用切面
術語比較多,簡單分類:
抽象概念:切面、引入、織入、目標對象、AOP 代理
具體概念:連接點、增強、切入點
關係:切入點是連接點形成的集合,兩者都表示需要插入邏輯的目標位置,增強表示需要插入的具體動作
使用 AOP 時需要關注的是連接點和切入點,前者是「想在哪個位置插入邏輯」,後者是「想在哪塊區域插入邏輯(區域由位置組成)」,再切入並註冊 advice,添加前置後置邏輯
###Advice 類型
- 前置增強(Before advice)
在某連接點之前執行的增強,但這個增強不能阻止連接點前的執行(除非它拋出一個異常)
- 後置返回增強(After returning advice)
在某連接點正常完成後執行的增強:例如,一個方法沒有拋出任何異常,正常返回
- 後置異常增強(After throwing advice)
在方法拋出異常退出時執行的增強
- 後置最終增強(After (finally) advice)
當某連接點退出的時候執行的增強(不論是正常返回還是異常退出)
- 環繞增強(Around Advice)
圍繞一個連接點的增強,如方法調用。這是最強大的一種增強類型。環繞增強可以在方法調用前後完成自定義的行為。它也負責選擇是繼續執行連接點,還是直接返回它們自己的返回值或者拋出異常來結束執行
需要注意的是 AfterThrowing 與 AroundAdvice 的區別,業務邏輯發生異常後,會觸發前者,但拿不到異常對象,只知道關注的方法發生異常了,意義不大。而後者是把業務邏輯完全包裹起來,所以可以捕獲異常信息(暫不討論異步回調異常)。其它幾種 Advice 都是字面意思,很容易理解
二、作用
AOP 能夠將那些與業務無關,卻為業務模塊所共同調用的邏輯或責任(例如事務處理、日誌管理、權限控制等)封裝起來,便於減少系統的重複代碼,降低模塊間的耦合度,並有利於未來的可操作性和可維護性
感受一個例子,面向對象代碼很容易長成這樣:
/**
* OO style
*/
class OOUser {
public function add($fields) {
// check auth
if (!$this->isGranted('ADD_USER')) {
throw new Exception("Access Denied");
}
// log
$this->log('creating user');
// create user
try {
$user = array('id' => '003');
$user['name'] = 'user';
//...
// save
$this->insertUser($user);
} catch(Exception $e) {
$this->log('user create error: ' + $e);
// handleError($e);
}
// log
$this->log('user created');
}
}
緩存,日誌,異常處理,權限檢查等邏輯分散穿插於項目代碼各處(不止 User 類),無法分離出來。存在很多問題:
-
無法複用
-
難以理解類的最初職能,邏輯雜亂
-
很容易出錯,如果忘記寫這些樣板(biolerplate)代碼的話
-
有違 DRY 原則,每個邏輯塊都穿插著這些面熟的代碼
尤其是維護老項目,看到一塊塊的臉熟代碼很難受,想改又抽不出來,或者費了很大勁最後只是緩解了一點表面症狀(比如,考慮其它封裝方式,精簡了幾行業務代碼)
AOP 專門解決這個問題,它可以橫向切入對象內部進行內科手術,剝離核心業務邏輯,我們就可以專注於真正有用的那幾行代碼
三、PHP AOP 示例
發現了一個比較好用的 PHP AOP 框架:Go! AOP
P.S. 因為較好的 AOP 框架涉及反射與註解,以筆者目前的 PHP 能力不足完成,所以放棄了手動實現 AOP 機制的想法
考慮之前的 OO 代碼,對邏輯塊進行分類:
public function add() {
//=before advice
// check auth
if (!isGranted('ADD_USER')) {
throw new Exception("Access Denied");
}
//=before advice
// log
log('creating user');
//=business logic
// create user
try {
$user = array('id' => '003');
$user['name'] = 'user';
//...
// save
insertUser($user);
} catch(Exception $e) {
//=after throwing advice
log('user create error: ' + $e);
// handleError($e);
}
//=after advice
// log
log('user created');
}
發現業務邏輯只有幾行,但是,被其它不很關鍵的代碼深深地包起來了,更新維護時就將面對「在一大片代碼中修改某一小塊」的問題,定位到關鍵部分再小心翼翼地修改,然後還是很容易出錯(尤其是異常處理)
然後抽離業務邏輯,新的 User 類是這樣的:
/**
* AOP style
*/
class AOPUser {
public function add($fields) {
//=business logic
// create user
$user = array('id' => '003');
$user['name'] = 'user';
//...
// save
$this->insertUser($user);
// throw error
$this->badMethod();
throw new Exception("A Stange Error");
}
/**
* Insert user to database
*
* @Loggable
*
* @param Array $info Info
*/
public function insertUser($user) {
//...
$this->log('user inserted');
}
//...其它無法共享的依賴方法
}
我們把業務邏輯分離出來了,可共享的依賴方法(比如,log(), isGranted() 等)都被抽出來成為共享 lib,其它無法共享的依賴方法仍然作為類成員存在,此時 User 類的職責相對單一,不和諧的代碼都滾出去了,邏輯很清晰
接下來需要裝配(類似於裝飾者模式,但實現方式上差異較大),把滾出去的相關代碼再裝上,AOP 會幫我們動態組裝,我們只需要聲明關聯,告訴 AOP在哪裡 裝什麼(也就是術語「切面」的含義)
/**
* User aspect
*/
class UserAspect implements Aspect {
/**
* Pointcut for add method
*
* @Pointcut("execution(public App\App\AOPUser->add(*))")
*/
protected function UserAdd() {}
// 執行 $aopuser->add() 時切入
/**
* Check anth before add user
*
* @param MethodInvocation $invocation Invocation
* @Before("$this->UserAdd")
*/
protected function checkAuthBeforeAdd(MethodInvocation $invocation) {
/** @var $user \App\App\AOPUser */
$user = $invocation->getThis();
// check auth
if (!$isGranted('ADD_USER', $user->caller)) {
throw new Exception("Access Denied");
}
}
/**
* Handle Error after throwing
*
* @param MethodInvocation $invocation Invocation
* @AfterThrowing("$this->UserAdd")
*/
protected function handleErrorAfterThrowing(MethodInvocation $invocation) {
/** @var $user \App\App\AOPUser */
$user = $invocation->getThis();
// =after throwing advice
$log('user create error, handle error here');
// handleError();
//!!! avoid reporting error
set_exception_handler(function($e) {
echo "!!!Global Exception Handler: " . $e->getMessage();
});
}
/**
* Log after add user
*
* @param MethodInvocation $invocation Invocation
* @After("$this->UserAdd")
*/
protected function logAfterAdd(MethodInvocation $invocation) {
/** @var $user \App\App\AOPUser */
$user = $invocation->getThis();
// log
$log('user created');
}
//...其它 Advice
}
通過註解聲明增強(Advice)與目標對象的聯繫,告訴 AOP 在哪裡插入什麼邏輯,消除邏輯粘連
注意handleErrorAfterThrowing() 方法,為了避免全局異常報錯,我們使用了 set_exception_handler(),這樣做是因為 AfterThrowing 增強在切點發生異常時會觸發,但我們拿不到異常對象,也無法吃掉它,所以通過全局異常攔截來吃掉這個異常
如果需要精確操作某過程中的異常,應該使用 Around 增強,把目標過程完全包裹起來,再 try-catch 即可,如下:
/**
* Around advice to catch exception
* @param MethodInvocation $invocation Invocation
* @Around("execution(public App\App\AOPUser->badMethod(*))")
*/
protected function aroundBadMethod(MethodInvocation $invocation) {
try {
$invocation->proceed();
} catch (Exception $e) {
echo '!!!Around Advice Error Handler: ' . $e->getMessage() . "<br>\n";
}
}
P.S. 此處不討論異步回調中的異常,PHP 一般不關注這種情況,JS 的話,也不考慮註解方式實現 AOP(應該採用高階函數、binding 之類的方式進行邏輯注入),以後再說
P.S. GO! AOP 相當強大,也提供了切入系統方法及工具函數的方式,包括參數截獲,屬性訪問攔截等等
四、Demo
在線 Demo:http://www.ayqy.net/temp/aop/src/
源碼地址:https://github.com/ayqy/aop
注意:需要 PHP5.5+,因為 GO! AOP 框架內使用了 class 作為標識符
五、總結
AOP 是對 OOP 的補充,橫向切入對象並進行邏輯注入,確保類的職責單一
更貼切地說,AOP 是一種設計模式,也有比較激烈的看法:
AOP 是 OOP 的補丁,縱向 OOP 建立對象體系,繼承封裝多態;橫向 AOP 切入,縱橫合璧,天下無敵...
也沒錯,只是存在侵入程度的爭議,比如,如果想要 AOP 切入整個 OO 體系,勢必侵入程度很大(考慮繼承)。個人更喜歡侵入程度小的方案,靈活但不方便
怎麼說,學習 AOP 算是獲得了一種設計思路,類似於設計原則(複習一下):
-
封裝變化(把易於發生變化的部分抽出來,以減少其變化對其它部分的影響)
-
多用組合,少用繼承(組合比繼承更有彈性)
-
針對接口編程,不針對實現編程(使用接口可以避免直接依賴具體類)
-
為交互對象之間的鬆耦合設計而努力(更鬆的耦合意味著更多的彈性)
-
類應該對擴展開放,對修改關閉(open-close 原則)
-
依賴抽象,不要依賴具體類(減少對具體類的直接依賴)
-
只和朋友交談(密友原則)
-
別找我,我會找你(Don't call me, I will call you back. 安卓開發的大原則)
-
類應該只有一個改變的理由(單一責任原則)
-
橫向邏輯注入(AOP)
考慮問題時多一種選擇,仅此而已。在構建大型系統時 AOP 應該是必要的內置功能,但就應用場景而言,AOP 並不是萬能鑰匙,但 AOP 的思想(橫向邏輯注入)適用於任何場景
參考資料
-
Weaving aspects in PHP with the help of Go! AOP library:GO! AOP 作者的 PPT,適合入門(感性認知)
-
Introduction to aspect-oriented programming:對 AOP 術語的解釋
-
我對 AOP 的理解:後半部分挺好的
暫無評論,快來發表你的看法吧