読者です 読者をやめる 読者になる 読者になる

はむはむエンジニアぶろぐ

このブログのコンセプトは"ハッキングの為なら愛する家族を傷つけることをいとわない" 自分にとってエンジニアリングは "手段ではなく生きる目的" である

【AOP】ProxyパターンとDIコンテナを使ったアスペクト指向プログラミング

デザインパターン Proxy DI PHP アスペクト指向

f:id:secret_hamuhamu:20150816222337j:plain
ProxyパターンとDIコンテナを使うことで、アスペクト指向プログラミングを実現できます。
アスペクト指向は、オブジェクト指向で解決しにくい問題を解決しましょうというパラダイムです。
今回のアスペクト指向プログラミングの対象は、ロギングです。

言語はPHPで、DIコンテナはPimpleを使います。

環境構成

  • PHP 5.6
  • Pimple 3.0


アスペクト指向とは?

アスペクト指向プログラミングを略して、AOPと言います。
アスペクト指向プログラミング wikipediaより

アスペクト指向プログラミング(アスペクトしこうプログラミング、Aspect Oriented Programming、AOP)は、オブジェクト指向ではうまく分離できない特徴(クラス間を横断 (cross-cutting) するような機能)を「アスペクト」とみなし、アスペクト記述言語をもちいて分離して記述することでプログラムに柔軟性をもたせようとする試み。アスペクトの例としては、データ転送帯域の制限や例外の処理などがある


オブジェクト指向とは直交する概念である。

オブジェクト指向とアスペクト指向は直交している。
つまり、全く異なる概念である。

オブジェクト指向は、オブジェクトを用いて「関心ごとを分離」する。
各々のオブジェクトは、凝集された関心ごとの塊である。(のが理想)

それに対し、アスペクト指向は オブジェクト指向を用いても解決できない「横断的な関心ごと」を取り扱う考え方である。
「横断的な関心ごと」とは、このようなものである。

  • ロギング
  • トランザクション
  • エラーハンドリング
  • キャッシュ

etc...

オブジェクト指向 < アスペクト指向 ということではなく、オブジェクト指向の考え方で補えないところを アスペクト指向で補完するという位置づけになる。
つまり、「横断的な関心ごと」の問題は オブジェクト指向 + アスペクト指向で立ち向かう。


アスペクト指向で解決できること

自分がコーディングしたコードを見返してみて、似たようなコードをいろんなクラスで行っているのであれば、アスペクト指向で解決できるかもしれない。
アスペクト指向プログラミングがされていれば、「横断的関心ごと」を1箇所で管理できる。

例えばログの基本フォーマットを変えたい場合など、アスペクト指向プログラミングされている1箇所変更するだけで済む。
つまり、オブジェクト指向プログラミングのコードに修正を加えずに変更できる。

「横断的関心ごと」というのは、オブジェクト指向において凝集度を下げる要因になる。
「横断的関心ごと」を取り除くことで、高凝集を保つことができる。

さらにアスペクト指向プログラミングは、統一性をもたらします。
例えば、メソッドを呼び出す前後でロギングをするというチーム内のルールがあったとします。

新しくチームに入った人が、そのルールを知らなかったり、そもそもロギングを忘れていたなんてことは全然ありえます。
それを防ぐためには、ロギングを意識せずよしなにやってくれる人が必要です。

そのよしなにやってくれる人が、アスペクト指向プログラミングである。


Proxyパターンとは?

Proxyパターンとは、GoFのデザインパターンの一つ。
Proxyは、代理人のことで、ある対象オブジェクトへの操作を肩代わりして、対象オブジェクトの負担を減らすというものです。
対象オブジェクトがやるには、ふさわしくない処理を受け持つと考えていいでしょう。

Proxyパターンは、クラスのインターフェースを変更せずに委譲を用いて機能追加をするパターンです。
インターフェースを変更せずに機能追加をするパターンとして、似たようなものにDecoratorパターンがあります。


両者の違いは難しいのですが、Proxyパターンは委譲を使って転送するだけで出力に影響を与えません。


サンプル実装してみた。
HogeService が、ロギング機能を追加したいクラスで、ロギング機能を LoggingProxy に任せる。

echo してるところをログとってるんだと読みかえてください。

<?php
class HogeService
{
    public function execute($a, $b, $c)
    {
        return $a + $b + $c;
    }
}

class LoggingProxy
{
    private $object;
    private $logger;

    public function __construct(HogeService $object)
    {
        $this->object = $object;
        $this->logger = new Logger();
    }

    public function execute($a, $b, $c)
    {
        $this->logger->debug("calling execute.\r\n");
        $returned = $this->object->execute($a, $b, $c);
        $this->logger->debug("called execute.\r\n");

        return $returned;
    }
}

class Logger
{
    public function debug($message)
    {
        echo sprintf('debug : %s', $message);
    }
}


$proxy = new LoggingProxy(new HogeService());
$proxy->execute(1, 2, 3);

実行結果

debug : 1回目の呼び出し
debug : calling execute.
debug : called execute.


こんな感じで、LoggingProxyを介することで、処理の前後にロギングをはさむ事ができた。

ProxyパターンとDIコンテナを使ったアスペクト指向プログラミング

本題に入ります。
オブジェクトのメソッドを呼び出す際に毎回ロギングしたい場合、Proxyパターンを使えば可能です。
しかし、ロギングしたいオブジェクトが色んな所で利用される予定だとProxy通すためコードを追加するのは、冗長で変更に弱いです。

そこで、インスタンス生成を一元管理できるDIコンテナを使用します。

DIコンテナ

DIコンテナは、Pimpleを使用します。
HogeServiceContainer.php

<?php

use Pimple\Container;
$container = new Container();

$container['logger'] = function ($c) {
    return new Logger();
};

$container['hoge_service'] = $container->factory(function ($c) {
    //return new HogeService();
    return $c['loggable_hoge_service'];
});

$container['loggable_hoge_service'] = $container->factory(function ($c) {
    return new LoggingProxy(new HogeService(), $c['logger']);
});

return $container;

Proxyパターン

先ほどの LoggingProxy を汎用的に変更しました。
マジックメソッドの __call を使っているので、 HogeService 以外のクラスでもロギングすることができます。

<?php

class HogeService
{
    public function execute($a, $b, $c)
    {
        return $a + $b + $c;
    }
}

class LoggingProxy
{
    private $object;
    private $logger;

    public function __construct($object, Logger $logger)
    {
        $this->object = $object;
        $this->logger = $logger;
    }

    public function __call($methodName, array $args)
    {
        $className = get_class($this->object);

        $calling = function() use ($className, $methodName, $args) {
            return sprintf("%s_%s has calling. args %s.\r\n",    $className, $methodName, implode(',', $args));
        };
        $called = function($returned) use ($className, $methodName, $args) {
            return sprintf("%s %s had called. returned %s.\r\n", $className, $methodName, $returned);
        };

        $this->logger->debug($calling());
        $returned = call_user_func_array([$this->object, $methodName], $args);
        $this->logger->debug($called($returned));

        return $returned;
    }
}

class Logger
{
    public function debug($message)
    {
        echo sprintf('debug : %s', $message);
    }
}

利用側

<?php
$container = require_once 'HogeServiceContainer.php';

$hogeService = $container['hoge_service'];
$hogeService->execute(1, 2, 3);

実行結果

こんな感じで、ログを取ることができます

debug : HogeService_execute has calling. args 1,2,3.
debug : HogeService execute had called. returned 6.

まとめ

ProxyパターンとDIコンテナを使えば、アスペクト指向プログラミングできる。
PHPにもアスペクト指向のライブラリがあるので、それを使ったりコードを読んで学習できます。

最近読んだ本

アスペクト指向入門 -Java ・ オブジェクト指向から AspectJプログラミングへ

アスペクト指向入門 -Java ・ オブジェクト指向から AspectJプログラミングへ

ユースケースによるアスペクト指向ソフトウェア開発 (Object Oriented Selectionシリーズ)

ユースケースによるアスペクト指向ソフトウェア開発 (Object Oriented Selectionシリーズ)

  • 作者: Ivar Jacobson,Pan-Wei Ng,鷲崎弘宜,太田健一郎,鹿糠秀行,立堀道昭
  • 出版社/メーカー: 翔泳社
  • 発売日: 2006/03/23
  • メディア: 大型本
  • 購入: 1人 クリック: 28回
  • この商品を含むブログ (26件) を見る

実践テスト駆動開発 テストに導かれてオブジェクト指向ソフトウェアを育てる (Object Oriented SELECTION)

実践テスト駆動開発 テストに導かれてオブジェクト指向ソフトウェアを育てる (Object Oriented SELECTION)