Apect Oriented Design

メソッド・インターセプター


例えばテスト用途にどんな引き数が渡されても特定の同じ値を返さなければならないとします。あるいはアジリティを重視した開発で、メソッド内のコードや利用データベースが用意できていない段階でも適当に用意した値を返す必要があるとします。

このような場合、通常はテスティングフレームワークを使いモックオブジェクトを生成して利用します。BEAR.SundayのRay.Diのモジュールでモックオブジェクトを用意して差し替える事もできます。しかしRay.Aopの提供するメソッドインターセプターを使えば更に簡単です。

メソッドインターセプターはメソッドの実行を横取りして(interceptして)代理実行します。モックオブジェクトは対象オブジェクトを入れ替えますが、インターセプターは対象オブジェクトとそれを利用するコンシュマークラスの間に割り込み(インターセプト)します。

まずは基本になるオリジナルのメソッド実行と同じ動作をするインタセプターのコードです。

class GreetingInterceptor implements MethodInterceptor {     /**     * (non-PHPdoc)     * @see Ray\Aop.MethodInterceptor::invoke()     */     public function invoke(MethodInvocation $invocation)     {         $result = $invocation->proceed();         return  $result;     } }

MethodInterceptorインターフェイスのinvoke(実行)というメソッドにはMethodInvocation(メソッド実行)オブジェクト$invocationが渡されます。

メソッド実行オブジェクトはメソッドの実行に必要な全ての知識(対象インスタンス、メソッド名、引き数等)を持っています。オリジナルのメソッドを実行するためには$invocation->proceed();を実行します。

この実行の前後に処理を記述したりすることで元も処理をまたぐ事ができます。引き数を操作したり変更したりすることもできます。1またインターセプターを同じメソッドに複数適用することもできます。

テスト用に常にここのメソッドが”HelloTest”を返す為には以下のように変更します。

class GreetingInterceptor implements MethodInterceptor
{
    /**
    * (non-PHPdoc)
    * @see Ray\Aop.MethodInterceptor::invoke()
    */
    public function invoke(MethodInvocation $invocation)
    {
        $result = $invocation->proceed();
        return  $result;
    }
}

このGreetingリソースに限らず、何のリソースのメソッドが”HelloTest”を返す為すためには以下のように変更します。

class GreetingInterceptor implements MethodInterceptor
{
    /**
    * (non-PHPdoc)
    * @see Ray\Aop.MethodInterceptor::invoke()
    */
    public function invoke(MethodInvocation $invocation)
    {
        // $result = $invocation->proceed();
        return  HelloTest;
    }
}

このインターセプターを特定のクラス、特定のメソッドにバインドするのも「モジュール」で行います。このバインドはsandbox\Resource\App\Greetingクラス(およびその継承したクラス)のどのメソッドにもMockInterceptorインターセプターを適用します。

class MockResourceInterceptor
{
    private $mock;
    public function __construct($mock)
    {
        $this->mock = $mock;
    }
    public function invoke(MethodInvocation $invocation)
    {
        $return $this->mock;
    } 
}

バインド対象を指定するためのマッチャーはアノテーションを指定したり、Callableオブジェクトを指定することもできます。

@Cacheアノテーション

BEAR.Sundayはいくつかのインターセプターを用意しています。その内、Cacheインターセプターは特に有用でしょう。このアノテーションはメソッド実行の結果を指定の秒数キャッシュします。


  /**
   * @Cache(60)
   */
  public function onGet($lang = en)
  {
      
  }

このインターセプターのソースを見てましょう。

public function __construct(Cache $cache) {
    $this->cache = $cache;
}

public function invoke(MethodInvocation $invocation) {
    $class = get_class($invocation->getThis());
    $args = $invocation->getArguments();
    $id = $this->getId($class, $args);
    $saved = $this->cache->fetch($id);
    if ($saved) {
        return unserialize($saved);
    }
    $data = $invocation->proceed();
    $annotation = $invocation->getAnnotation();
    $time = $annotation->time;
    $this->cache->save($id, serialize($data), $time);
    return $data;
    }

インターセプターはというMethodInvocationインターフェイスを実装します。2 引き数や対象メソッドをキーにキャッシュにを生成していて@Cacheアノテーションで指定された括弧内の秒数だけキャッシュデータを再利用するようになっています。 @Cacheアノテートされたされたリソースはアノテートがされただけでこれが実際にはどのインターセプターがバインドされるのかはこのクラスからは宣言していません。3 このアノテーションが実際にどのインターセプターが適用されるのか、あるいはそもそもインターセプターが適用されない(開発時など)といった構成知識に関わりがありません。Ray.Diの@Injectアノテーションと同じように構成は利用される側でなく利用する側が持ちます。

ランタイムインジェクター

BEAR.Sundayのオブジェクトの実行は最初の1リクエストでオブジェクトフラフのコンストラクションを完了するコンパイルと、以降のランタイムに分けられます。 コンパイルでリクエストをまたいで再利用可能なオブジェクトをつくるために、リクエスト毎にディペンデンシーを変えてインジェクトをしたりすることはできません。4 例えばDBオブジェクト5 のmaster / slaveをメソッドに応じて自動で選択するために、GET(読み込み)かそれ以外のメソッドでインジェクトを変えるという事はRay.Diのインジェクターではできません。 この場合インターセプターを使ってDBオブジェクトをメソッドにセットしてやることができます。 インターセプターはメソッド実行の情報が渡されるので、実行メソッド名をみてmaster/slaveのDBを選択することができます。master/slaveに限らずユーザーIDに応じたDB選択や、DBに応じた初期化などもインターセプターで記述できます。リソースをリクエストするクライントもそれを受けるappリソースも本来の仕事、つまり本質的関心(core concern)にのみ専念し、DBオブジェクトの準備というリソースをまたいだ横断的関心事(cross cutting concern)から関心を分離することができます。

Postsリソースクラス

/**
 * Posts
 *
 * @Db
 */
class Posts extends ResourceObject implements DbSetter
{
    /**
     * Table
     *
     * @var string
     */
    private $table = posts;
    /**
     * DB
     *
     * @var Doctrine\DBAL\Connection
     */
    private $db;

    /**
     * Set DB
     *
     * @param Connection $db
     *
     * @return void
     */
    public function setDb(Connection $db = null)
    {
        $this->db = $db;
    }

    /**
     * Get
     *
     * @return array
     */
    public function onGet()
    {
        $sql = SELECT id, title, body, created, modified FROM {$this->table};
        $stmt = $this->db->query($sql);
        $this->body = $stmt->fetchAll(PDO::FETCH_ASSOC);
        return $this;
    }

    /**
     * Post
     *
     * @param string   $title
     * @param string   $body
     * @param DateTime $created
     * @param DateTime $modified
     *
     * @return \sandbox\Resource\App\Posts
     */
    public function onPost($title, $body, $created = null, $modified = null)
    {
        $this->db->insert($this->table, ['title' => $title, 'body' => $body]);
        $this->code = 204;
        return $this;
    }
}

@Dbアノテーションクラス

/**
 * Db
 *
 * @Annotation
 * @Target(“CLASS”)
 *
 * @package    BEAR.Framework
 * @subpackage Annotation
 */
final class Db
{
}

モジュール内でDbインジェクターをバインド

$this->bindInterceptor(
            $this->matcher->annotatedWith(BEAR\Framework\Annotation\Db), // クラスに@Dbとアノテートされた全てのクラス
            $this->matcher->any(), // 全てのメソッド
            [$dbInjector] // 複数バインドできます
        );

Conclusion

Ray.Diのインジェクションシステムはコンシュマーとディペンデンシーの関係を疎にしアプリケーション構成を柔軟にしますがコンパイルされた関係性は再利用されオブジェクトとオブジェクトの結びつき(オブジェクトグラフ)はリクエストをまたいでも変わりません。つまりRay.Diでは早期束縛の依存ののみを扱います。 対してRay.Aopのメソッドインターセプターはコンシュマーとメソッドを動的に束縛します。横断的関心事メソッドをコールしてもそれが実際にオリジナルなメソッドをコールしたかにコンシュマーは関心を払いません。メソッド内ではDBデータを読み込んでるのに、バインドされたインターセプターはmemcacheからデータを読みその値を返し、オリジナルのメソッドはデータ更新の際の最初の1度しか呼ばれないかもしれません。関係性は外部で構成され、その束縛はリクエストの実行時に決定されます。つまり遅延束縛です。 このようにRay.AopのインセプターはAOPとしてメソッドの振る舞いを返るだけでなく、ランタイムでの依存解決にも使われます。例えばDBオブジェクトは実際のメソッドリクエストがあるまで、master/slave/partioningどのDB接続をするべきかは決定することができません。インターセプターとして束縛されたDBインジェクターが依存を動的に注入します。 現代的なPHPフレームワークの多くは、アプリケーションコントローラーの役割を様々な関心をアスペクトととらえそれぞれの実装がなされています。例えばフィルターチェーンであったり、シグナルスロット、イベンドディスパッチャー、イベントサブスクライバー実装パターンや呼び方が違っても問題をアスペクトとしてそれぞれの解決をしようとしているのは同じように思えます。 Ray.AopでのAOPはコンシュマーにもサービスにもAOPフレームワークの依存がなく利用するためのサービスクラスには、イベント通知などイベントハンドリングのための仕事をする必要はありません。Ray.Diで生成されるアスペクトが織り込まれるサービスクラスは、イベントハンドリングをするサービスに含まれた状態で渡されます。該当メソッドの適用インターセプター知識をそれぞれが保持していて、イベントハンドリングがそれぞれのサービス6内で行われます。7 Ray.Aopを使ったアプリケーションコントローラー8 フレームワークやアプリケーションがコンシュマーとサービスの利用の関係をダイナミックにします。これを完全に外側から構成できる拡張性、関心の分離の促進によるソフトウエア品質の向上には期待をしています。9 v0.1.0alphaリリースを機にBEAR.ResourceRay.Di、Ray.AopとBEAR.Sundayのオブジェクトフレームワークというべきものについて記事を一つ一つかいてきました。Ray.Diはオブジェクトの生成を、Ray.Aopはそのオブジェクトのメソッドの利用にこれまでにない拡張性と機能性を与えます。そうやってできたオブジェクトにRESTという制約を被せ、オブジェクトの関係を(データではなく)DSLによって記述される関係性で結合しようとするのがBEAR.Resourceです。Ray.Diはオブジェクトグラフを、BEAR.Resourceはリソースグラフを構成しようとし、それぞれのリソースクラスは自らを構成しようとします。

はてなブックマーク - Object Framework – Ray.Aop


<ol class="footnotes">

  • BEAR.SaturdayではAroundアドバイスとして実装されていものと同様のものです。 []
  • これはJavaのAOPアライアンスMethodInterceptorを元にしたもので、Google Guice, Spring, SeasorのAOPもこのAOPアラインアンスのインターフェイス群を実装しています。 []
  • 前バージョンのBEAR.Saturdayでは実クラスを指定していて、アスペクトという関心の分離と適用はできたのですがそれが固定化されていました。 []
  • またPDOなどのPHP標準組み込みオブジェクトもインジェクトできません。 []
  • PDOと違って組み込みオブジェクトではないので@Injectでコンパイル時にインジェクトすることは可能です []
  • 詳しくはサービスを含んだプロキシー []
  • この仕組みはBEAR.Resourceでリソースそれぞれがレンダラーを持っているのと似ています。サービス(レンダラー、イベントハンドラー)にデータ(テンプレート、イベントシグナル)を渡すのではなく、オブジェクトがサービスを内包しているのです。 []
  • フォームや認証、セキュリティ、ログ []
  • 一方このパターンを採用する事で発生するデメリットにも注意深く対処していかなければなりません。 []
  • </ol>