NHN Cloud NHN Cloud Meetup!

AOP(Aspect Oriented Programming)とは何か?

JavaScriptの開発において、AOPは聞きなれないテーマです。通常、AOPの概念を説明するとき、よくcross cutting concerns(横断的関心事)という言葉を使います。もう少し簡単にいうと、「ログを残すところはここ、そこ、あそこである」「ユーザーが入力したデータを関連する部分でもデータの有効性を検証する必要がある」など、共通した関心事のように表現できるでしょう。

サンプルを見てみよう

class BookCollection {
    ...
    getByISBN(isbn) {
        return this.get({
            isbn: isbn
        }).then(book => book.name)
            .catch(error => null);
    }
    ...
}

AOPが適用できる事例を調べるため、BookCollectionからBookを取得するサンプルを書いてみました。ISBNで書籍名を取得する簡単なコードです。このコードにログを追加してみると以下のようになるでしょう。

class BookCollection {
    ...
    getNameByISBN(isbn) {
        return this.get({
            isbn: isbn
        }).then(book => {
                Logger.info(`Retrieving book ${isbn} - ${book.name} has been succeed`);
                return book.name;
            })
            .catch(error => {
                Logger.error(`Retrieving book ${isbn} has been failed. ${JSON.stringify(error)}`);
                return null;
            });
    }
    ...
}

さらにキャッシュや有効値の検証などを追加すると、コードはどのような形状になるでしょうか?then().then().then()のように追加できるでしょう。このようにコードが長くなってくると、本1冊を探したかっただけなのに、本末転倒になってしまいますね。クラスを分けて考えてみよう。

class BookCollection extends Collection {
    ...
    getNameByISBN(isbn) {
        return this.get({
            isbn: isbn
        }, {
            cache: true,
            onSuccess: 'name'
            onFail: null,
            log: {
                message: 'Retrieving Book {} - {} has been succeed',
                params: ['isbn', 'name'],
                level: 'info'
            },
            ...
        });
    }
    ...
}

もちろん、上のサンプルより優れた解決方法はあるでしょうが、最終的に長かったクラス名や、複雑な継承関係、可読性が少し改良されました。

AOPを使えば少しよくなる

以下のコードはaspect.jsというライブラリで使用される構文に沿って作成しました。おそらくJava開発者にとって、より身近な形であると考えられます。

class LoggerAspect {
    ...
    @afterMethod({
        methodNamePattern: /^getNameByISBN$/,
        classNamePattern: /^BookCollection$/
    })
    afterGetNameByISBN(meta) {
        let result = meta.method.result;
        Logger.info(`Retrieving ${result.isbn} - ${result.name} has been succeed`);
    }
    ...
}

@Wove
class BookCollection {
    ...
    getNameByISBN(id, article) {
        return this.get({
            isbn: isbn
        }, {
            cache: true,
            onSuccess: 'name'
            onFail: null
        });
    }
    ...
}

さらに良くなったように見えます。BookCollectionはデータの読み込みに集中しており、ログは完全に分離されたクラスで実行されています。今回もCacheAspectを追加してみよう。

class CacheAspect {
    ...
    @beforeMethod({
        methodNamePattern: /^get.*/,
        classNamePattern: /^[Book|User]Collection$/
    })
    beforeGet(meta, args) {
        let key = `${meta.name}:${args.join()}`;
        let method = meta.method;
        method.proceed = true;
        if (this.cache.hasOwnProperty(key)) {
            method.result = this.cache[key];
            method.proceed = false;
        }
    }
    ...
}

@Wove
class BookCollection {
    ...
    getNameByISBN(id, article) {
        return this.get({
            isbn: isbn
        }, {
            onSuccess: 'name'
            onFail: null
        });
    }
    ...
}

BookCollectionから役割とは関係ないコードを削除し、本来の役割が明確になりました。このような方法でAspectを増やしていくとよいでしょう。Aspectがどれだけ増えようと、BookCollectionには自分の役割となるコードだけが存在することになるでしょう。さらに、上記の名前のパターンを/^get.*/式に与えてさまざまなクラスやメソッドに適用できることに注視しよう。
Collectionクラス(あるいは共通の操作を実行したいより多くのクラス)が存在する場合でも、コードを増やさず、すべてに適用させることができます。Collectionクラスのcross-cutting concernsに対応するロギング、キャッシュなどを分離した状態と言えます。

しかし、Decoratorsなんて!

DecoratorsはES7標準を準備しています。すでに上記サンプルを提示しておきながら、当該ライブラリはまだ標準が決まっていないDecoratorsに依存しています(TC39 Notes, July 28, 2016Implement new decorator proposal when finalized)。Babel Legacy Decorator pluginを使ってサンプルを真似ることはできますが、実際のプロジェクトに適用する勇敢な人はいないでしょう。もちろん、aspect.js以外にすぐに使えるライブラリもありますが、説明が容易で、概念を忠実に実装したものから検討して、aspect.jsを選択しました。今すぐ何かに適用してみたいなら、meldや他のライブラリを参照することもできます。もちろんmeldを含めた他のライブラリもあります。

直接組み込んでみよう

この短い文章ですべてを説明することは難しいので、Proxy+DecoratorがどのようにAOPになれるのか、短くヒントになるコードだけ書いてみます。最初にAOPがProxyの関連テーマと言ったことを覚えているでしょうか?以下はAOP Advice(実動作するコード)が動作する方法をProxyを活用して真似たコードです。ProxyとClassは、現時点で最新ブラウザで実装されているので、以下のとおり、そのままコピー&ペーストしてもうまく動作します。(なお、IEでは動作しません。)

...
function Logger(target, pattern) {
    return new Proxy(target, {
        get: function(obj, prop) {
            var value, name;
            if (!Reflect.has(obj, prop)) {
                return;
            }
            name = target.name || target.constructor.name;
            value = Reflect.get(obj, prop);
            if (typeof value === 'function') {
                value = function() {
                    let result = Reflect.apply(obj[prop], obj, arguments);
                    if (pattern.exec(prop)) {
                        console.log(`Function ${prop} retrieved result ${JSON.stringify(result)}`);
                    }
                    return result;
                }.bind(obj);
            }
            return value;
        }
    });
}

class BookCollection {
    getNameByISBN(isbn) {
        return {
            isbn: isbn,
            name: 'Proxy + Decorators = AOP'
        };
    }
}
BookCollection.prototype = Logger(BookCollection.prototype, /^get.*/);
console.log(new BookCollection().getNameByISBN('sdaf'));
// Function getNameByISBN retrieved result {"isbn":"some-isbn","name":"Proxy + Decorator = AOP"}
// Object {isbn: "some-isbn", name: "Proxy + Decorator = AOP"}

上記のコードは、BookCollectionプロトタイプにProxyを作って与えられたパターンの関数が実行される場合にのみ、ログを一緒に実行する動作を行います。ProxyテーマのWeeklyを読んでいれば、十分に理解できるでしょう。では、Decoratorsを使ってLoggerをまとめる方法でコードを変えてみよう。

function wove(pattern) {
    return function (target) {
        target.prototype = Logger(target.prototype, pattern);
    };
}


@wove(/^get.*/)
class BookCollection {
    getNameByISBN(isbn) {
        return {
            isbn: isbn,
            name: 'Proxy + Decorators = AOP'
        };
    }
}

console.log(new BookCollection().getNameByISBN('sdaf'));
// Function getNameByISBN retrieved result {"isbn":"some-isbn","name":"Proxy + Decorator = AOP"}
// Object {isbn: "some-isbn", name: "Proxy + Decorator = AOP"}

@woveDecoratorsに慌てないで。上記の@woveは下のコードと完全に同じコードです。

wove(/^get.*/)(BookCollection);

もう少しDescriptorsについて理解したい場合は、DecoratorsDecorators and functionsを読んでみよう。DecoratorsはStage2 Draftの段階であり、現在の標準でも意見の相違が続いているだけに、今後変わる余地があることを勘案しておこう。もちろんaspect.jsの提供方法は上記のコードよりもはるかに複雑です。しかし、ここまでの原理を理解すればAOPで紹介したコードがどのように動作するか想像できるでしょう。

おわりに

Javaの世界でAOPを表現する言葉として、Black Magicというものがあります。ジェームズ・ゴスリンはeWeekとのインタビューで、「危険な」「問題の塊」そして「マニュアルなしでチェーンソーを握る行為」と言っています。これはOOPで入念に組み込まれたJavaの世界の大原則を無視して、AOPコードがカットイン(文字どおりcross-cutting)するように見えたからだろうと予想できます。では、JavaScriptのAOPも黒魔法のような存在になるでしょうか?
すでに組み込まれたコードにAspectを適用するのにAspectJがしなければならないことを考えると、次のJavaScriptコードはともて簡単で自然です。

let originalFunction = Collection.prototype.getNameByISBN;
Collection.prototype.getNameByISBN = function () {
    let result = originalFunction.apply(this, arguments);
    Logger.info(`Retrieving ${result.isbn} - ${result.name} has been succeed`);
    return result;
};

JavascriptがES6、ES7標準の方向やTypescriptの人気など、徐々にOOPで実装できるように形を整えつつあります。それによって自然とこれを補完できるAOPツールも改良され、これに対する話題も活発になってくるでしょう。

参考文

NHN Cloud Meetup 編集部

NHN Cloudの技術ナレッジやお得なイベント情報を発信していきます
pagetop