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

アスペクトの利用方法

利用が期待されている分野

アスペクト指向プログラミングは,様々な場面で利用されるように なってきており,以下のような利用方法があると言われています. あくまで「期待されている」だけで,実際にそうかどうかは 未知の部分も多いです(ここに列挙された項目と, 実際のコード例として掲載している項目との違いを見ていただければ 分かるかと思います). 今後の開発経験の蓄積が待たれます.

  • アプリケーションの統合
  • アプリケーションサーバー等のインフラの補強
  • 特定スレッドからの呼び出しのみを受理するアクセス制限
  • リソースのプール処理
  • 負荷分散
  • 耐障害性の実現(オブジェクトの多重化,エラーからの回復)
  • 特定パターンの処理の最適化
  • トランザクション処理
  • オブジェクトの永続性(persistency)の実装
  • デザインパターンの置き換え
  • 例外処理
  • RMI などオブジェクトの分散化

アスペクト指向において, 「いつ(ポイントカット)何をする(アドバイス)」というルールを列挙していく 形式は,処理を実行するタイミングが何らかのポリシーで事前に与えられている場合, ポイントカットが記述しやすく,有効であろうと推測しています.

EJBコンテナなどがオブジェクトの性質を決定・拡張するのも, アスペクト指向的な要素と考えていいと思います.

一度アスペクトとしてモジュールが作成されると, それ以降は「うまく(特定のメソッド名や属性を持った)コードを書きさえすれば, あとはアスペクトが勝手に必要な機能(永続化など)を補充してくれる」 といったように,アプリケーション内部のオブジェクトに対する サービス的な側面を担うのではないかと考えられます.

アスペクトの分類

アスペクトの種別については,次の文献に記述があります.

Ivan Kiseleve: "Aspect-Oriented Programming with AspectJ", 
Sams Publishing, ISBN 0-672-32410-5

この文献では,アスペクトの種類を次のように分類しています.

  • Development Aspects
    • プログラムを開発する際,補助的に用いるアスペクトのことです. このアスペクトは,製品出荷時に取り除かれます. これに分類されるアスペクトとして, Logging, Tracing, Profiling がサンプルとして挙げられています. 今のところ,この使い方がもっともよく知られており, 逆にこの使い方しかないのではないか,との声も出ています.
  • Production Aspects
    • プログラムの中の機能を担当するアスペクトです. これに分類されるアスペクトは,システムを正常に機能させるために必要な存在です. セキュリティのチェックや例外処理などがサンプルとして挙げられます.
  • Runtime Aspects
    • 実行時の性能改善を行うアスペクトです. 取り除いてもシステムの機能自体には影響を与えません. バッファリング,オブジェクトのプールやキャッシングなどがこれに当たります.

上記のうち,Development Aspects とRuntime Aspects は, ある程度 Java に慣れたプログラマならば簡単に導入することが可能であるため, 各所で使用されているようです.逆に,用途が限定されるために, 「アスペクト指向はロギングしかできない」との声も聞かれます. アスペクト指向の真価が発揮されるであろう Production Aspects に関しては, 設計手法の確立なども含め,知識の蓄積と普及には時間がかかりそうです.

利用例いろいろ

Class.getName() をキャッシュする

Runtime Aspect の1つで,処理の遅いメソッドの性能改善です.

Java 一般の話として,Class.getName() メソッドは native メソッドで,オブジェクトが実際にどのクラスに属するかを 記録するために o.getClass().getName() の形式でよく呼び出されます. しかし,これは非常に重い処理なので,アスペクトの内部などで 使ってしまうと,プログラムがやたらと遅くなることがあります.

そこで,次のような記述を導入します.

static Hashtable h = new HashTable();
String around(Class c) : call(String c.getName()) {
  String s = (String)h.get(c);
  if (s == null) {
    s = c.getName();
    h.put(c, s);
  }
  return s;
}

このように簡単なキャッシュを入れるだけでも,かなり高速化できる場合 があります.この手のキャッシュは,thisJoinPoint.getSignature() や thisJoinPoint.getSourceLocation() のようなメソッドに対しても 有効な場合があります.

getName などで得られる文字列として結合して使用する場合には, 高価な文字列連結演算を含んでいるため, その結果をキャッシュすることで効果を発揮します. 必ずしも常に有効というわけではないので, プロファイラとあわせて使いましょう.

オブジェクトの遅延初期化

簡単に言うと,オブジェクトの初期化を使われる瞬間まで遅らせるというものです. 実装としては簡単で,get が初めて呼ばれた瞬間に初期化コードを走らせます.

class TargetType {
  private String filename;
  private FileEntity file; // ファイルの中身を保存するクラス
  
  public TargetType(String filename_) {
    filename = filename_;
    // File はまだ読まない
  }
  public void initialize() {
    // 必要になってから File の中身を読む
    file = new FileEntity(filename);
  }
}

aspect InitLater {
  static Set uninitialized_objects = new HashSet();

  pointcut on_create(TargetType o) : call(* o.new(..));
  pointcut first_get (TargetType o) : 
    get(* o.*) && !get(* o.filename) && if (uninitialized_objects.contains(o));

  after(TargetType o) : on_create(o) {
    uninitialized_objects.add(o);
  }
 
  before(TargetType o): first_get(o) {
    o.initialize(); // o の初期化用メソッドを呼ぶ
    uninitialized_objects.remove(o); // 未初期化オブジェクト集合から除去
  }
}

適当に書いたコード片ですので,このままでは動かないと思いますが, コンセプトさえ理解できれば適宜書き換えて使えるでしょう.

また,上記の実装では,未初期化オブジェクトが HashSetに補完されるため,ガベージコレクションの 対象にならなくなります.メモリ消費が馬鹿にならないので, WeakHashMap を使ってオブジェクトを弱到達にしておくというのも手でしょう.

この遅延初期化のメカニズムが有効な場面は様々です. たとえば多数のファイルを扱う場合, 1ファイル1項目でファイルのリストを作っておいて, 実際に使うことになったものだけ中身を実際に読み込む,といった使い方があります.

Decorator としてのアスペクト

Decorator パターンは,java.io のストリームクラスなどで 使われているデザインパターンです. 同一のインタフェースを持つ Decorator(Wrapper) オブジェクトで 元のオブジェクトを包み,動作を変更するというものです.

例.FileOutputStream にバッファリング処理を付加します.
OutputStream out = new BufferedOutputStream(new FileOutputStream(f));

アスペクトでも,これと同様のことが簡単に実装できます. ただし,AspectJ はアスペクトをコンパイル時に結合するため, 何らかのフラグを与えて,オブジェクトごとに動的にON/OFF するための 仕組みが必要です.

しかし,動的な ON/OFF が不要な場面で,うまくオブジェクトを限定できれば (特定クラス内のメンバオブジェクトにのみアスペクトを結合するなど), コンパイル時の結合は,メソッドの Delegation による オーバーヘッドを持たないため,有効に働く場合も考えられます.

アスペクトで記述した Decorator は通常の Decorator と異なり, そのオブジェクトから外部へのメソッド呼び出しも書き換えることができます. これをうまく使えば,オブジェクトを「完全に」包むことが可能になります. これは Composition Filter (合成フィルタ)のアプローチですが, オブジェクトのテストベンチとしてのアスペクトなど,様々な用途が考えられます.

「ワームホール」コンテキスト渡し

コンテキスト渡し(別名「ワームホール」イディオム)は, AspectJ Tutorial で紹介されているものです.

class Caller { 
 void doSomething() { worker.doSomething(); }
}
class Worker {
 void doSomething() {
   // 子供がいればそれに delegate,いないなら自分で処理
   if (hasSubWorker()) worker.doSomething();
   else doHoge();
 }
}

Caller と Worker というクラスが存在し, Caller は Worker に対してメソッド呼び出しを行っている状態を仮定します. Worker はメソッドを子 Worker へ渡していき, 最終的に末端のオブジェクトが実際の動作を行います.

さて,ここで,末端の Worker のひとつで, Caller に応じて処理を切り替えたいという要求が起きることがあります. 通常ならば,doSomething の引数を増やすのが妥当な選択ですが, 他の worker はまったく Caller の情報を使用しないのに 引数で渡すというのも,なんだか無駄な気がします.

// 普通の拡張
class Caller { 
 void doSomething() { worker.doSomething(this); }
}
class Worker {
 void doSomething(Caller c) {
   // 子供がいればそれに delegate,いないなら自分で処理
   if (hasSubWorker()) worker.doSomething(c);
   else doHoge(c);
 }
}

さらにここで,worker のサブクラスを作成し, サブクラスごとに異なる追加パラメータが必要になったらどうするでしょうか?

// パラメータが増えてしまったシグネチャの例
void doSomething(Caller c, int flag_for_FooWorker, double flag_for_BarWorker);

// コンテキストを表現するオブジェクトを用いる例
void doSomething(WorkerContext c);

// コンテキスト情報を持った visitor に worker を巡回させる
visitor.visit(worker);

様々な方法は考えられますが,どれも「他のWorkerでは必要だけど 自分には必要ない」余計な情報にアクセスできるようになってしまう, という欠点は解消できていません. これに対して,アスペクトでコンテキスト情報を渡そうという発想があります.

aspect WormHole {
  static Caller caller;
  before (Caller caller_) : this(caller_) && call(void Worker.doSomething()) {
   caller = caller_;
  }
  around (SomeWorker worker) : call(void worker.doSomething()) &&
                               if (!worker.hasSubWorker()) {
    worker.doFoo(caller);
  }
} 

最初の worker 呼び出しの時点で caller を記憶し,末端の必要な Worker に対してのみ届けるというアスペクトを実装できます. このアスペクトの利点は,パラメータを順番に渡していく必要がないため, 他の worker に余計なパラメータを与えません.

途中の引数を渡していく部分が (たとえばフレームワークの一部であるなどの理由で) 変更できない場合などに有効です.

しかし,欠点もあります.それは,「プログラマがクラスだけを見ると, caller がどこから飛んできているのか分からない」というものです.

アスペクトはクラスに依存していることを明示的に記述できますが, クラスはアスペクトに依存していることを明示することはできません. クラスをビルディングブロックとして,アスペクトを糊付けとして用いる場合, クラスがアスペクトに依存するのは避けたいという要望はありますが, それは一般のプログラマにとっては理解しがたいコードとなってしまいます. AspectJ IDE のような,アスペクトがクラスのどこに連結するかを 示すツールがあれば少しは楽になりますが,それでも完全な解決とは言いがたいです.

このようなコードを良いと見るか悪いと見るかは,意見の分かれるところです.

トランザクションをアスペクトで実現する

以下の文献には,アスペクトを用いた分散処理の記述が説明されています. ここでは,その中から,トランザクションを記述する抽象アスペクトの コード例を紹介しておきます.

S. Soares, E. Laureano, P. Borba:
Implementing Distribution and Persistence Aspects with AspectJ,
In Proc. of OOPSLA 2002, pages 174-190, November 2002
abstract aspect AbstractTransactionControl {
  abstract pointcut transactionalMethods();
  abstract IPersistenceMechanism getPm();   // 子アスペクトで実装する

  before(): transactionalMethods() {
    getPm().beginTransaction();
  }
  after() returning: transactionalMethods() {
    getPm().commitTransaction();
  }
  after() throwing: transactionalMethods() {
    getPm().rollbackTransaction();
  }
}

デザインパターンをアスペクトで書くと?

オブジェクト指向プログラミングで用いられるデザインパターンについては, 数多くの雑誌やサイトで取り上げられており, 設計者やプログラマにとっての大事な教養のひとつという感が出てきました. しかし,デザインパターンはアスペクト指向でも通用するのでしょうか?

デザインパターンはオブジェクト間の連携方法を説明したものですから, それが対処しようとしている問題も横断要素の一種と考えられます. GoF のデザインパターンをアスペクトで記述した場合, コードの局所性,再利用性,合成・分離の容易性といった アスペクト指向がうたっている特徴が現れるのか, という疑問に対して,OOPSLA 2002 で論文が投稿されていました.

オブジェクト間の循環依存性の解決,パターンコードの再利用が可能になるなど, Observer, Singleton などのパターンについては有効性が認められたのですが, Facade や Abstract Factory などのパターンでは特に効果がない, という興味深い結果となりました. この論文は PDF ファイルが web 上でも公開されており, その中に表が掲載されていますので,興味のある方はご覧ください. 著者らのサイトからは, Java と AspectJ のサンプルコードもダウンロードできるようです.

http://www.cs.ubc.ca/~jan/AODPs/

Jan Hannemann, Gregor Kiczales:
Design Pattern Implementation in Java and AspectJ, 
Proceedings of OOPSLA 2002, pp.161-173 (2002).

コーディング規則の強制

AspectJを使用したコーディング規則の強制については,以下の記事に サンプルコード付きで紹介されています.

R. Dale Asberry: Using AspectJ to implement and enforce coding standards
http://www.daleasberry.com/newsletters/200210/20021002.shtml
// フィールドへの外部からの直接代入の禁止
pointcut directMemberAssignment(): set(* *.*) && !withincode(* set*(..));
declare error: directMemberAssignment():
     "Coding standards require the use of a setter for all " + 
     "member variable assignments, even within the class itself.";

 この記事を読んだ Wes Isberg による補足:
 上記の pointcut では,コンストラクタ内部ではメンバの初期化ができず,
 コンストラクタも setter を呼ばなければならなくなる.
 setter がオブジェクトが初期化されていることを前提としている可能性がある.

 さらに補足:
 setter がサブクラスでオーバーライドされる可能性もあるため,
 コンストラクタから setter を呼び出すことは危険なものとなっています.
 もちろん,setter を final 宣言すれば使用可能ですが.

declare error 宣言は,AspectJ のマニュアルでは,static crosscutting として 説明されているもので,あるポイントカットがマッチすることを コンパイルエラーとして定義します.これを用いることで, いくつかのコーディング規則を定義することができます. この記事の筆者は,「従来はレビューなどによってコーディング規則に 従っているかどうかをチェックしていたが, コンパイラにその仕事を任せたほうが素早くフィードバックを 得ることができるため有用である」としています.

次の例は,特定クラスへのアクセスを あるパッケージのクラス群からに制限するものです.

pointcut restrictedCall(): call(* java.sql.*.*(..)) || call(java.sql.*.new(..));

pointcut illegalSource(): within(com.foo..*) && !within(com.foo.sqlAccess.*);

declare error: restrictedCall() && illegalSource():
   "java.sql package can only be accessed from com.foo.sqlAccess";

Java にはパッケージ単位でのアクセス制限機構は存在しないので, 自分専用に作った(頻繁に変更する予定の)作業用クラスを チームのメンバーに勝手に利用されないように, このような宣言を使用することもできます.

この記事では,さらに実行時にもコーディング規則をチェックするためのアスペクトも 紹介されていますが,ここでは扱いません.興味のある方は原文を読んでください.

AspectJ のポイントカット定義はシンプルで書きやすいことが有利な点ですが, pointcut として書けない,構文的なコーディング規則は扱えません. そこまで取り扱いたいのであれば(そこまでして扱うべきかどうかは分かりませんが),次のようなプリプロセッサなどを利用するのが良いかもしれません.

EPP: A Extensible Pre-Processor Kit
http://staff.aist.go.jp/y-ichisugi/epp/j-index.html

複数インタフェースの継承を禁止するアスペクト

AspectJ-users ML で,Eric Simmerman が 「複数のインタフェースを実装するような クラスを実装することを禁止したい.たとえば EJB のクラスが Remote 呼び出し用インタフェースを実装することを止めたいのだが, こんな感じでいける?」と質問して, Wes Isberg が「こう書けばいけるね」と直したソースです. とても面白い使い方だと思います.

interface A {}
interface B {}

class Both implements A, B {}
class One implements A {}
class Two implements B {}
class Three extends One implements B {}

aspect NotBoth {
    declare error : staticinitialization(*) && within(A+) && within(B+):
                    "can't implement both A and B";
}

パッチとしてのアスペクト

オブジェクト指向シンポジウム2003の懇親会での雑談中に 何となく思いついたので,ここに書いておきます.

プログラミング作業中には,微妙な状況が発生することがあります.

  • ライブラリにバグがある.バグの回避方法は分かっている. バグ修正版が近々リリースされるかどうかは分からない. ライブラリは複数箇所で使用されており,使用している場所すべてを 正しく修正することは難しい.また,修正版がリリースされたら コードを取り除かなければならない.
  • 他人のコードにバグを見つけた.修正する権限は(管理上の都合などから)ない. しかしコードは動かさなければならない.
  • 自分のコードにバグを見つけた.とりあえず修正方針をいくつかに絞った. ソースコード管理システムがあるから変更の取り消しは可能だが, ちょっとずつ色々な修正方法を実験するには面倒くさい.

このようなときに,その変更をアスペクトとして記述することで, 問題を軽減できる可能性ができます.

  • ライブラリのバグ回避アスペクトは,ライブラリを改変することができる, ただし,ライブラリを改変する場合は注意が必要なので, ライブラリを使用している部分だけを書き換えるアスペクトを定義してもよい. 新しいライブラリでバグが直ったら,アスペクトを単純に取り除けばよい.
  • 他人のコードのバグを修正するアスペクトを書く. もちろん,バグ回避コードでもよい. バグを修正するアスペクトを作ったら,そのコードを所有している相手に 送りつけるという手もある.アスペクトが,コードの修正そのものを含んでいれば, 相手側はアスペクトのコードを参考にバグを直すことができる.
  • 一時的なパッチとしてアスペクトを書く.アスペクトの中で Inter-type Declaration (メンバの追加導入; Introduction のこと) を 用いてクラスごとのメンバ宣言などを行っておき, プログラムが動作した時点で,それらをクラス中に展開する. もちろん,アスペクトが横断的関心事を記述していることが分かったなら, そのままアスペクトとしてプログラムに残せばよい.

単純に「パッチ用アスペクトだらけ」な環境で放っておくと 管理コストが上がりそうなので,横断的関心事でないものは 適度にクラス内に展開していくということも必要だと思います. さすがにそれを自動化するツールはありませんが….

共有オブジェクトへのアクセスの監視

あるクラス C のインスタンスのフィールドへのアクセスを解析したい,という 要望がある場合があります.しかし,フィールドがオブジェクトであるとき, そのオブジェクトへの参照が別のオブジェクトによって共有されている場合, 単純な set, get で監視することはできません.

// 例: children オブジェクトへのアクセスを監視したい
class C {
  List chidren;
  public void setChildren(List c) { children = c; }
  public List getChildren() { return children; }
}

C c = getC();
List l = c.getChildren();
l.add(obj);   // -- このアクセスを監視したい!

これに対する解決策のひとつは, 「あるクラスから参照されている」オブジェクトかどうかを常に保存しておき, 参照ごとにそれが該当オブジェクトかどうかを調べる方法です.

aspect FieldCheckAspect {
  Set referred_by_c = new HashSet();
  before(C c): target(c) && set(C.children) {
    referred_by_c.remove(c.children);
  }
  after(C c): target(c) && set(C.children) {
    referred_by_c.add(c.children);
  }
  before(List l): target(l) && call(* List.*(..))
     && if(referred_by_c.contains(l)) {
    System.err.println("C.chilren is accessed !");
  }
}

この手法は,どんな List オブジェクトへの参照であっても C.children への参照かどうかを調べるため,オーバーヘッドが高くなります. また,List の中身のオブジェクトや,List.iterator などによって オブジェクトの要素へ直接アクセスしている場合も検知することはできません. しかし,List のかわりに独自クラスを使う, within 指定子によって特定クラスからのアクセスだけを監視する, といった方法によって,オーバーヘッドを抑えることは可能です.

補足: 実際に使用するときは,WeakHashMap などを使って 弱参照でオブジェクトを格納しないと, いつまでたってもオブジェクトが解放されないので注意してください.

例外処理アスペクト

AspectJ では,アスペクトも例外を throw することができますが, オブジェクト側にどのような例外ハンドラが用意されているか 分からないことも多いため,通常はアスペクト側で例外ハンドラも用意します. オブジェクトが発生させた例外をアスペクトが受け取ったり, アスペクトが発生させた例外をオブジェクトが受け取ることもできますが, 以下のようなコードを用いることで,アスペクトが発生させた例外を, オブジェクトの例外ハンドラを素通りさせて 同一のアスペクトにある例外ハンドラに捕まえさせることができます.

アイディアは簡単で,好ましくない例外ハンドラに捕まえられた場合は 例外を投げ直すようなコードを記述します.

before(): precondition_check() {
  // 事前条件違反なので例外を送出
  if (..) throw new PreconditionViolationException();
}
around(): a_method_call() {
  try {
    proceed();
  } catch (PreconditionViolationException e) {
    // 例外を記録して復帰
  }
}
// 余計な場所で例外を捕まえられないように投げ直す
// 註: aosd-jp ML に投稿したときに比べると instanceof 判定が増えてます
before(RuntimeException e): 
    handler(RuntimeException) && args(e) && !within(PreconditionCheckAspect) {
  if (e instanceof PreconditionViolationException) throw e;
}

上記のような「例外を投げ直す」ようなコードは, 他にある「すべてのオブジェクトを捕まえて,何らかの処理をしてから再 throw する例外ハンドラ」なども無視してしまうので,事前の設計が重要となります.

本来は宣言的に例外の扱いを記述できるべきなのかもしれません. このような例外の取り扱いは,他の Separation of Concerns を謳った言語処理系でも あまり考えられていないようです.

また,アスペクトで例外処理を書くことができる,ということと, 書くことが望ましい,ということはまったく別問題なので, 設計・実装経験の蓄積が望まれます.

表明をアスペクトで記述する

表明とは,簡単には,プログラムのある時点で 「成立していなければならない条件」のことです. 古くからある有名なテクニックの一つで, 表明,アサーション(assertion),契約による設計(Design by Contract, DbC), などをキーワードに検索すると解説記事がたくさんあります.

で,アスペクト指向との関連ですが, 通常,メソッドの先頭などに記述される assert 文(あるいは JML などの 仕様記述言語)ですが,アスペクトとして注入することもできます.

AspectJ であれば,

before(Object obj): call(* List.add(..)) && args(obj) { assert(obj != null); }

と記述することで,メソッド List.add の呼び出し前に,obj が null でないことをチェックすることができます.

役に立つか立たないか,についてはまだ議論の余地が残っていますが, いくつか面白い記述ができそうである,ということで,既に提案されています.

  • 複数のメソッドで共通の性質を,まとめて記述できる.
    • 単に記述の手間を省略するだけでなく,これによって C++ における const メソッドのような,「副作用がないメソッド群」などの性質を容易に知ることができるようになります.
    • ただし,それと引き換えに,あるメソッドの説明を読んでも,どのような事前・事後条件があるかは分からなくなります.
  • 複数のオブジェクトにまたがった制約を記述できる.
    • たとえばメソッド呼び出し時点であれば呼び出し側と呼び出し先の両方について制約を記述できます.
    • また,AspectJ における cflow 述語や,別途フラグ管理をアスペクトとして付加することで,呼び出し経路など,より高度な情報も利用できます.
    • 複数のオブジェクトを横断した表明を,通常の表明機構だけで実現すると, 「表明検査のための余分なインタフェース,フラグ管理コード」が発生することがあります.これらをアスペクトとして明確に分離することができます.
  • モジュールの外部から制約を付加できる.
    • クラス本体の記述には影響を与えないため,クラスの再利用が,制約に左右されないという利点があります.汎用的なクラスに対して,厳しい制約を外部から付加することで安全に使う,といった用途が考えられます.
    • 上の項目と関連しますが,特定の利用コンテキストに応じて制約を特化できます.上記の例をさらに拡張すると,特定の List インスタンスにだけ制約を付加するといったことが可能です.
    • あるクラスの利用コンテキストごとに表明を分割して記述できるため,表明が混ざり合うことによる混乱を避けることができます.

これらが本当にうれしいかどうか,効果がどのくらいかといったところは, まだ経験の蓄積を待たなければなりませんが, 簡単に始められるアスペクト指向の活用のひとつだと思います.

イベント/プログラミングおよびプログラミング言語ワークショップ において発表したところ,そこそこ好評でした.このときの資料は以下から取得することができます.実際に表明をアスペクトとして記述したらどうなるのか,という例ベースの話が載っています. http://sel.ist.osaka-u.ac.jp/~lab-db/betuzuri/contents.ja/526.html

設定ファイルの情報をセットするアスペクト

これは,AspectJ Cookbook に記述されていたレシピの概要です.

アプリケーションの設定ファイル(Windowsなら .ini ファイルの内容など)を読み込むために1つのクラスを作る場合,他のコンポーネントは IniFile.getInstance().getX() のように,その設定ファイルのオブジェクトをどうにかして獲得し,情報を取り出してくる必要があります.

このようにコンポーネント群がその設定ファイルクラスに依存すると,コンポーネントの設定ファイルとは独立した再利用が困難となります.また,単体テストをする際にも,たとえば,設定ファイルクラスをモックに差し替えるとか,あるいは設定ファイルを複数用意するといった手間が必要になります.

このアスペクトは,「各コンポーネントが必要な情報を設定ファイルから読み出す」呼び出し関係を,「設定ファイルの情報を必要とするオブジェクトが生成されたとき,必要な値を設定しにいく」アドバイスに置き換えるものです.これによって,コンポーネントから設定ファイルへの依存関係を切り離します.

設定ファイルの読み出しは,アプリケーション起動時などに行います.たとえば,次のポイントカットを使います.

pointcut application_startup(): staticinitialization(Main);

設定ファイルの内容を使うオブジェクトが生成されたとき,値をセットしにいきます.

// 適当なコード例
after(MyClass obj): execution(public MyClass.new(..)) && this(obj) {
  obj.setX(config.getX()); // 必要な値をセット
}

これによって,設定ファイルの内容を使用するオブジェクトから,設定を格納したオブジェクトへの依存関係を削ることができます.

実際に適用する場合には,設定値の変更を set(int Main.someProperty) のようなポイントカットで監視して設定ファイルオブジェクトと同期させるとかいった工夫が必要です. また,アスペクトによって設定される予定の変数であることを何らかの形で明記しておかないと,開発者が混乱する可能性があります.

「単一の共有オブジェクトへの依存関係を断ち切る」という点では,ロギング用オブジェクトへのメソッド呼び出しをアスペクトにまとめるという形式と似ています.

アスペクトの活用へ向けての課題

アスペクトをどのようにテストするべきか?

aspectj-users ML で,Neil Hart という人が "Testing with JUnit" という サブジェクトで,「アスペクトをどうやってテストするのがいいんだろう?」という 発言をしていました.これに対して, Wes Isberg は以下のように答えていました.

何をどうテストするのかによって方法は異なってくるので, まず,static pointcut と dynamic pointcut に区分けして扱います.

Static Pointcut が働いているかどうかをチェックするには: declare warning を使って正しい位置でコンパイラの 警告メッセージが出ることを確認します.

dynamic なテストは調べたいことに依存しますが, 「イベントが期待通りに起こっているかどうか」を 次のようにチェックすることができます.

// コードは Wes Isberg の記述したもの; コメントは紹介の都合上,追記したものです
public class TestEvents {
    public void gotEvent(String s) { ... }
    public void expectEvents(String[] expected) { ... }
    public void checkAllEvents() throws ... { ... }
}

aspect TestStuff {
   private static final TestEvents testEvents;
   // 起こってほしいイベントを列挙しておく
   static {
        testEvents = new TestEvents();
        testEvents.expectEvents(new String[]
           {  "before [...] - target one"),
              "before [...] - target two")
           });
   }

   // 適当なタイミングで起こったイベントを記録していく
   before() : ... {
       testEvents.gotEvent("before [...] - target " +
                             thisJoinPoint.getTarget());
       // Object.toString の標準実装のような,
       // 実行ごとに変わる可能性がある文字列表現を使わないように注意.

   }
   ...

   // すべて終わった後に,イベントの確認
   // 起こらなかったらテスト失敗
   after() returning : testInvocation() {
       testEvents.checkAllEvents();
   }
}

参考資料として,CVS ツリーの AspectJ のテストなどを利用するといいかも, ということで Wes Isberg のメールは終わっています.

アスペクトは,横断対象のクラスが揃わないと テストしにくい(単体ではそもそもテストが不可能に近い) という問題があります. アスペクトを結合する前のクラスをテストするべきか, アスペクトを接続した状態でクラスをテストするべきか,という問題と, いつアスペクトを書くか,いつテストを開始するか, アスペクトだけをテストするようなイベント列を生成する テストケースは書けるのか,といった様々な議論がありそうです.


トップ   差分 バックアップ リロード   一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2006-07-28 (金) 15:14:38 (4493d)