アスペクト指向プログラミング入門

複雑なアプリケーションではロギング、トレーシング、メトリクスといったサポートの機能により、関数にすぐ負荷がかかってしまいます。これらのコードブロックはあらゆるコードベース上でそれぞれ少し変形して繰り返し使用されるのですが、これを横断的関心事(cross-cutting concerns)と言います。アスペクト指向プログラミング(AOP)は、アスペクトと呼ばれるモジュール内にコードブロックを引き入れて、関心の分離(separation of concerns)を手助けします。

AOPの実装

Phoneクラス1

不自然な例だというのは承知の上で、dialメソッド1つを使って簡単なPhoneクラスを構築してみました。

function Phone() {};
Phone.prototype.dial = function (friend) {
    var start = new Date().getTime();
    log('dial(): {}', friend.name);

    if (friend && friend.picksUp) {
        var end = new Date().getTime();
        log('Call lasted: {} milliseconds', end - start);
        return friend.message;
    } else {
        log('Error: No answer!');
        throw 'No answer!';
    }
};

dial関数は以下のような働きをします。

  1. nameメソッドとfriendのnameメソッドを含むトレース・メッセージのログを取る
  2. もしfriendがピックアップしたら
    1. 呼び出し時間のログを取る
    2. friendのメッセージを返す
  3. または
    1. friendがピックアップに失敗したことを示すログメッセージを作成する
    2. 例外の“No answer!”を送出する

AOPにはビジネスロジックと関心事を分離させる目的があることを思い出してください。ではPhoneクラスを見返してみましょう。実際に、ビジネスロジックはどれほどでしょうか。

  1. nameメソッドとfriendのnameメソッドでデバッグ・メッセージのログを取る
  2. もしfriendがピックアップしたら
    1. 呼び出し時間のログを取る
    2. friendのメッセージを返す
  3. または
    1. friendがピックアップに失敗したことを示すログメッセージを作成する
    2. 例外の“No answer!”を送出する

なんと関数のコードの半分近くが、関心事のために機能しているのです。
AOPライブラリのmeldを使って、このような関心事をアスペクトに分離させる方法を見ていきましょう。

呼び出し先のメタデータを追加する

最初に、トレーシングのステートメントを1つのアドバイスに引き出します。アドバイスは、別の関数上で作用する関数を参照します。ここにbeforeアドバイスを作成しました。このアドバイスは他の関数が実行する前に動作します。

meld.before(Phone.prototype, 'dial', function () {
    log(
        '{}: Calling {}... ',
        meld.joinpoint().method,
        meld.joinpoint().args[0].name
    );
});

meld.beforeメソッドを使って、Phone.dialが動作する前にlogアドバイスを適用します。AOPの専門用語では、phone.dialポイントカットbeforeジョインポイントでlogアドバイスを適用する、と言います。ジョインポイントとは、アドバイスが適用されて実行するポイントのことで、ポイントカットはアドバイスが適用される関数を定義します。

ジョインポイントは関連したポイントカットの情報を提供することに注目してください。このアスペクトではメソッドの名前と引数にアクセスするために、meld.joinpoint().methodmeld.joinpoint().args[0].nameにアクセスするのです。次に説明しますがジョインポイントは、また別の便利なメタ機能も提供してくれます。

トレーシングは極めて一般的なAOPの活用例です。開発者はクラスのコードを変更することなく、アプリケーションを通した値の変化を簡単に観察することができます。

呼び出しのタイミング

次は呼び出すタイミングを決める関数です。

meld.around(Phone.prototype, 'dial', function (joinPoint) {
    var start = new Date().getTime();
    var message = joinPoint.proceed();
    var end = new Date().getTime();
    log('Call lasted: {} milliseconds!', (end - start));
    return joinPoint.args[0].name + ': ' + message;
});

名前から分かるように、aroundアドバイスは関数が呼び出されている周囲で実行されます。これは関数の呼び出し前後に処理を入れるということです。この特殊なジョインポイントは.proceed関数をもっており、この関数はこのアスペクトが関連付けられた関数の実行命令をmeldに伝えます。joinPoint.proceed関数が実行されない限り、dial関数は決して実行されません。

// Calling fn2 from fn1, where fn2 has around advice
----------            -----------                   ---------
|        | --fn2()--> |         |                   |       |
| fn1()  |            | around  | --jp.proceed()--> | fn2() |
|        |            |  advice | <----return-------|       |
|        | <-return-- |         |                   |       |
----------            -----------                   ---------

aroundアドバイスでは関数の返り値の変更が可能です。joinPoint.proceedの返り値が関数呼び出しの返り値となります。呼び出し元へ値を返す前に自由に修正したり、全く違う値を返すことも可能です。aroundアドバイスは本質的に呼び出し元と呼び出し先との間にあるプロキシの役割を担っています。

呼び出される関数を制御するため、AOPにおいてaroundアドバイスは最も強力で堅牢なジョインポイトと言えます。

エラーハンドリング

最後に、ロギングの例外処理について見てみましょう。afterThrowingアドバイスは例外が投げられた後に動作します。

meld.afterThrowing(Phone.prototype, 'dial', function (error) {
    log('Error: {}', error);
});

ここでジョインポイントからafterThrowingアドバイスに例外を渡しています。afterThrowingは、アプリケーションのエラーの発生場所と発生理由についてのメトリクスを集めるときに特に有効です。多くの人がインターネット上で目にする“404 Not Found”エラーが表示されるURLデータを集めるウェブサーバのようなものだと考えられます。

さらに複雑なアプリケーションでは、ジョインポイントはトランジェントエラーが発生し得るコードをリトライするロジックの実行にも利用することができます。

meld.afterThrowing(SomeObject, 'someMethod', function (error) {
    if (isTransient(error)) { // Check whether error is recoverable
        setTimeout(function () {
            // Retry after a delay
            SomeObject.someMethod();
        }, delay);
    }
});

afterThrowingアドバイスによってロギングや例外処理が扱いやすくなります。

全体のまとめ

完成したコードがJSFiddleGitHubにあります。見てみると分かりますが、AOPを利用することでコードがたった5行のビジネスロジックと3つの再利用可能なアスペクトにまで短くなっています。

今回は全てのコードを1つのファイルで使用しましたが、さらに大きなコードベースを扱う開発者ならアスペクトを別々のファイルに分けて使用するでしょう。

考察

アスペクト指向プログラミングはアプリケーションのリファクタリングに有効です。既に紹介したアドバイスに加えて、AOPライブラリには以下のアドバイスも存在します。

  • afterアドバイス:成功するかどうかに関わらず、メソッドの呼び出し後に実行
  • afterReturningアドバイス:メソッドの呼び出し成功後に実行

チュートリアルを書くにあたって、広く使われていて便利であるということから今回はJavascriptを使用しましたが、他の多くの言語でAOPは使用可能です。例えば、JavaではSpring AOPAspectJといったものがあります。このライブラリは両方ともmeld.jsの機能に適応するのに加えて、ポイントカットの柔軟性や再利用をサポートしています。GitHub上の私のコードスニペットをまとめたリポジトリにSpring AOPのコードスニペットがありますので興味があれば見てみてください。

アスペクト指向プログラミングはビジネスロジックから関心事を分離す強力な技術を提供し、結果としてコードを分かりやすくまとめることができます。


参考文献

  1. Github上のオープンソース

  2. WikipediaのAOPに関する記述

  3. CSSを使用するときは毎回、アスペクト指向プログラミングを行っている

脚注


  1. 厳密にはJavascriptでは“クラス”ではなく“プロトタイプ”を使用する。