跳到主要內容
黯羽輕揚每天積累一點點

AOP(Aspect-Oriented Programming)

免費2016-07-16#Design_Pattern#AOP#Aspect-Oriented Programming#切面#Aspect#面向切面

AOP 是對 OOP 的補充,橫向切入對象並進行邏輯注入,確保類的職責單一

寫在前面

時不時地總會在各種地方看到 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 的思想(橫向邏輯注入)適用於任何場景

參考資料

評論

暫無評論,快來發表你的看法吧

提交評論