メインコンテンツへ移動

AOP(アスペクト指向プログラミング)

無料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 を使用する時に注目するのはジョインポイントとポイントカットです。前者は「どの位置にロジックを挿入したいか」、後者は「どの領域にロジックを挿入したいか(領域は位置で構成される)」、そして介入してアドバイスを登録し、前置後置ロジックを追加します

###アドバイスタイプ

  • 前置アドバイス(Before advice)

あるジョインポイントの前に実行されるアドバイス。ただしこのアドバイスはジョインポイント前の実行を阻止できません(例外をスローしない限り)

  • 後置戻り値アドバイス(After returning advice)

あるジョインポイントが正常に完了した後に実行されるアドバイス:例えば、メソッドが例外をスローせずに正常に戻ります

  • 後置例外アドバイス(After throwing advice)

メソッドが例外をスローして終了する時に実行されるアドバイス

  • 後置最終アドバイス(After (finally) advice)

あるジョインポイントが終了する時に実行されるアドバイス(正常戻りまたは例外終了のいずれか)

  • ラウンドアドバイス(Around Advice)

ジョインポイントを囲むアドバイス。メソッド呼び出しなど。これは最も強力なアドバイスタイプです。ラウンドアドバイスはメソッド呼び出し前後にカスタム動作を完了できます。またジョインポイントの実行を継続するか、それとも独自の戻り値を直接返すか、例外をスローして実行を終了するかを選択する責任もあります

注意が必要なのは AfterThrowing と AroundAdvice の違いです。ビジネスロジックが例外を発生した後、前者はトリガーされますが、例外オブジェクトを取得できず、注目しているメソッドが例外を発生したことしかわからず、意味は大きくありません。後者はビジネスロジックを完全にラップするため、例外情報をキャプチャできます(非同期コールバック例外については暫く議論しません)。其它数種のアドバイスは文字通りの意味で、非常に理解しやすいです

二.役割

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 クラスだけではない)に分散して散在し、分離できません。多くの問題が存在します:

  • 再利用できない

  • クラスの本来の職能を理解しにくい。ロジックが雑然としている

  • 非常にエラーが発生しやすい。これらのボイラープレート(boilerplate)コードを書くのを忘れた場合

  • 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 auth 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');
    }

    //...其它アドバイス
}

アノテーションを通じてアドバイスとターゲットオブジェクトの関連を宣言し、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 の思想(横方向ロジック注入)は任意のシーンに適用可能

参考資料

コメント

コメントはまだありません

コメントを書く