ビジネス要求をソフトウェア機能に変換するサイクルが早い中では、アスペクト指向は頼りになる強力な原理です。基本的な設計の視点を伝統的なオブジェクト指向プログラミング(OOP)が持つ階層型の考え方から少し距離を取ってAOPとその設計原則に従うことにより、ソフトウェア設計者はオブジェクト指向に対して水平的、しかも補完的な設計を考えることができるようになるのです。
この記事ではAOPの機能のうち、一番利用されていない機能の一つをどのように実装するかを学びます。横断(crosscutting)は設計もプログラミング手法も比較的単純ですが、特に疎結合で拡張可能なエンタープライズ・システムを構築するという点では強力な機能が満載されています。実行時にオブジェクトの振る舞いが変更できる動的な横断(dynamic crosscutting)はAOPの基本の一つと考えられていますが、静的横断(static crosscutting)の方はほとんど知られていない手法と言えます。この記事がその状況を変えるのに役立つと思います。ここでは最初に動的横断と静的横断の両方の概要を説明し、すぐに静的横断手法を実証する実装シナリオに移ります。エンタープライズ・アプリケーションで最も一般的な課題の一つに対して、静的横断でいかに手軽に対処できるか、つまりサードパーティーのコードを生かしながらアプリケーションのコードベースを柔軟に保つ方法を、自分の目で確かめることができるでしょう。
ここではアスペクト指向プログラミングを簡単に、概念的なところから説明しますが、この記事はAOPの入門ではないので注意してください。AOPの入門記事のリストに関しては、参考文献を参照してください。
オブジェクト指向設計の基本的に良いところは、実世界領域の実体とそれに対応した振る舞いを、抽象的なオブジェクトとしてモデル化できると言う点にあります。オブジェクト指向で設計されたシステムはPersonやAccount、OrderそれにEventなどといった効果的なビジネス・オブジェクトを生み出します。ただし欠点として、そうしたビジネス・オブジェクトが、元々の意図と一致しないプロパティや操作と混ぜ合わされて、混乱したものになってしまう事があるのです。
アスペクト指向プログラミングは動的・静的横断を使って、目立たずきれいで、モジュラー化した形でオブジェクトに振る舞いを追加できるようにする事で、この問題に効果的に対応しています。
横断はアスペクト指向プログラミング特有の用語で、与えられたプログラミング・モデルの中で、確立された、責任(ロギングやパフォーマンス最適化のような)の区画を通り抜けることを指しています。横断の世界には動的横断と静的横断という2つのタイプがあります。この記事では両者を簡単に説明しますが、静的横断に重点を置いて説明します。
動的横断はポイントカット(pointcuts)やジョイン・ポイント(join points)によってアスペクトに振る舞いを生成する過程であり、これが後で実行時に、既存のオブジェクトに側面から追加されるのです。動的横断はオブジェクトの階層構造にある各種のメソッドにロギングや認証を容易に追加できるようにするためによく使われます。動的横断で使われるいくつかの概念について、ちょっと勉強してみましょう。
- アスペクトはJavaプログラミング言語でのクラスに似ています。アスペクトはポイントカットやアドバイスを定義し、横断(動的と静的の両方)を既存オブジェクト群の中に織り込むために、AspectJのようなアスペクト・コンパイラーでコンパイルされます。
-
ジョインポイントは、(プログラム中の)正確な実行点です(例えばクラス中にあるメソッド)。例えばオブジェクト
Fooにあるメソッドbar()はジョイン・ポイントになり得ます。ジョイン・ポイントは抽象的な概念です。ジョイン・ポイントを積極的に定義する事はできません。 -
ポイントカットは端的に言うと、ジョイン・ポイントを捉えるための構成です。例えばポイントカットを定義して、オブジェクト
Fooにあるメソッドbar()へのコールをどれでも捉えることができます。ジョイン・ポイントと対照的に、ポイントカットはアスペクトで定義されます。 -
アドバイスはポイントカットに対する実行コードです。よく定義されるアドバイスにはロギング機構の追加があります。このロギング機構ではポイントカットはオブジェクト
Fooにあるメソッドbar()への全てのコールを捉え、アドバイスは(例えばbar()のパラメーターを捉える、というような)ロギング機能を動的に追加します。
こうした概念は動的横断の中心的なものですが、これから見るように静的横断にはその全てが必要なわけではありません。動的横断に関してさらに詳しくは参考文献を見てください。
静的横断は、与えられたオブジェクトの実行時の振る舞いを変えないと言う点で動的横断とは異なっています。静的横断の方は追加的なメソッドやフィールド、プロパティを導入する事で、オブジェクトの構造を変更できるようになるのです。さらに、静的横断でオブジェクトの基本的な構造に拡張や実装を付け足す事もできるようになります。
静的横断の一般的な使い方として取り上げるべきものは今のところ無いのですが(驚くほど強力なものでありながら、AOPの機能としてあまり知られていないようです)、この手法の意味するところは非常に大きいと言えます。静的横断を使う事で、複雑なシステムを真にオブジェクト指向の手法でモデル化できるのです。静的横断によって、一般的な振る舞いをプラグインとしてシステムのどこにでも、深い階層構造を作り上げる必要もなく追加できるようなるのです。つまり、より優雅に、かつ現実世界の構造により近い形でオブジェクト・モデルを構成できるということです。
これから先は静的横断の手法やアプリケーションに焦点を当てていきます。
静的横断を作成するための構文は、ポイントカットやアドバイスが無いという点で動的横断を作成する構文と大きく異なっています。(下で定義するFooのような)与えられたオブジェクトに対して静的横断を使う事で、新しいメソッドを作成したり、コンストラクタをさらに追加したり、継承階層を修正することまで簡単になるのです。既存のクラスで静的横断をどのように実装するか、例を挙げてよく見てみましょう。リスト1は簡単な、アスペクト化されていないFooを示しています。
リスト1. アスペクト化されていないFoo
public class Foo {
public Foo() {
super();
}
}
|
リスト2で示すように、オブジェクトに新しいメソッドを追加するのは、アスペクト内に新しいメソッドを定義するのと同じぐらい簡単です。
リスト2. Fooに新しいメソッドを追加する
public aspect FooBar {
void Foo.bar() { System.out.println("in Foo.bar()");
}
}
|
newというキーワードが必要となる点で、コンストラクタはわずかに異なります(リスト3)。
リスト3. Fooに新しいコンストラクタを追加する
public aspect FooNew {
public Foo.new(String parm1){
super();
System.out.println("in Foo(string parm1)");
}
}
|
オブジェクトの継承階層の変更には、declare parentsタグが必要になります。例えばFooをマルチスレッドにするにはRunnableを実装するかThreadを継承する必要があります。リスト4はdeclare parentsタグをどのように使ってFooの継承階層を変更するかを示しています。
リスト4. Fooの継承階層を変更する
public aspect FooRunnable {
declare parents: Foo implements Runnable;
public void Foo.run() {
System.out.println("in Foo.run()");
}
}
|
この時点で、(特に疎結合で高い拡張性を持つシステムを構築するに当たって)静的横断が意味するところを想像できるのではないでしょうか。これから先は、静的横断を使う事でエンタープライズ・アプリケーションの柔軟性を容易に向上できるという実世界の設計・実装のシナリオを、順を追ってお見せしようと思います。
エンタープライズ・システムではサードパーティーの製品やライブラリを利用できるように設計されている場合がよくあります。アーキテクチャー全体をそうした製品に結びつけないようにするためによく行われるのは、アプリケーションの中に外部のベンダー・コードとインターフェースするように設計された抽象レイヤーを含めることです。この抽象レイヤーは非常に柔軟性の高い構成を持っており、システムの連続性に与える影響を最小に止めつつ、別のベンダーの実装や、場合によっては自前のコードもプラグインできるようになっています。
この実装の例として、ある操作が行われると、全く異なる通信経路から顧客に通知するシステムを考えてください。このシステムではEmailオブジェクトを使って、直接電子メール通信のインスタンスを表します。リスト5に示すように、Emailオブジェクトは送信者のアドレス、受信者のアドレス、標題、メール本体などのプロパティを含んでいます。
リスト5. Emailオブジェクトの例
public class Email implements Sendable {
private String body;
private String toAddress;
private String fromAddress;
private String subject;
public String getBody() {
return body;
}
public String getFromAddress() {
return fromAddress;
}
public String getSubject() {
return subject;
}
public String getToAddress() {
return toAddress;
}
public void setBody(String string) {
body = string;
}
public void setFromAddress(String string) {
fromAddress = string;
}
public void setSubject(String string) {
subject = string;
}
public void setToAddress(String string) {
toAddress = string;
}
}
|
アーキテクチャー・チームは(電子メール、ファックス、SMSメッセージなどを送る)カスタムの通信システムを構築するのではなく、任意のオブジェクトに基づくメッセージを(特定の規約に従って)送信できるベンダー製品を組込むことに決めるとします。このベンダー製品は非常に柔軟性に富み、XMLでのマッピング機構があり、カスタムのクライアント・オブジェクトをベンダー固有の経路実装にマップできるようになっています。ベンダーのシステムは通常のJavaオブジェクトを扱う上で、このマッピング・ファイルとJavaプラットフォームの反映能力に大きく依存しています。
アーキテクチャー・チームは柔軟性を大いに利用して、Sendableインターフェースをモデル化します(リスト6)。
リスト6. Sendableインターフェースの例
public interface Sendable {
String getBody();
String getToAddress();
}
|
図1はEmailオブジェクトとSendableインターフェースのクラス図を示しています。
図1. EmailクラスとSendableクラスのクラス図
このベンダーは各種のメッセージ・フォーマットを全く異なる経路で送れるという機能に加えて、提供したインターフェースを通して受信者のアドレス検証もできるようになるフックも提供しています。ベンダーの資料によると、このインターフェースを実装すると見られるオブジェクトはどれでも、あらかじめ定義したライフサイクルに従います。これは結果としての振る舞いに対応してvalidateAddress()メソッドが呼ばれ、正しく処理されることを保証します。もしvalidateAddress()がfalseを返すと、ベンダーの通信システムは対応する通信を送ろうとはしません。リスト7はこのベンダーのValidatableインターフェースを示します。
リスト7. Sendableインターフェースのアドレス検証
package com.acme.validate;
public interface Validatable {
boolean validateAddress();
}
|
アーキテクチャー・チームは基本的なオブジェクト指向設計の原則を適用し、Sendableインターフェースを更新してベンダーのValidatableインターフェースを継承しようと決定します。ところがそう決めてしまうと、ベンダーのコードに直接依存・結合してしまう事になるのです。このチームが将来、他のベンダーのツールを使おうとすると、コード・ベースをリファクターし、Sendableインターフェースのextends文と、オブジェクト階層構造に実装された振る舞いを削除しなければならないのです。
もっとスマートで、しかも究極的により柔軟な解決方法は、静的横断を使って対象とするオブジェクトに振る舞いを追加することです。
このチームはアスペクト指向の原則を適用して、EmailオブジェクトがベンダーのValidatableインターフェースを実装すると宣言するアスペクトを作成します。さらにこのアスペクトの中に、validateAddress()メソッドに必要となる振る舞いをコード化します。こうすることでEmailオブジェクトはベンダーのパッケージからのインポートを何も含まず、またvalidateAddress()メソッドを定義する事もないので、コードをうまく切り離す事ができます。Emailオブジェクトは自分がvalidatableのタイプである事に全く気が付かないのです! リスト8はその結果としてのアスペクトで、EmailオブジェクトはベンダーのValidatableインターフェースを実装するように、静的に拡張されています。
リスト8. Email validatableアスペクト
import com.acme.validate.Validatable;
public aspect EmailValidateAspect {
declare parents: Email implements Validatable;
public boolean Email.validateAddress(){
if(this.getToAddress() != null){
return true;
}else{
return false;
}
}
}
|
JUnitを利用するとEmailValidateAspectが確かにEmailオブジェクトを修正したことを確認する事ができます。JUnitテスト・スイートを使う事で、Emailオブジェクトがデフォルト値で作成されること、また一連のテストケースでEmailが確かにValidatableのインスタンスである事が検証できます。さらにテストケースによって、toAddressがnullの場合には、validateAddress()へのコールではfalseが返されることも分かります。テストケースはその上、非nulltoAddressがvalidateAddress()にtrueを返させることも検証できるのです。
まず、単純な値を持つEmailオブジェクトのインスタンスを構成するフィクスチャー(fixture)を作成することから始めます。リスト9で、このインスタンスが、有効な(つまりnullではない)toAddress値を確かに持っている事に注意してください。
リスト9. JUnit setUp()
import com.acme.validate.Validatable;
public class EmailTest extends TestCase {
private Email email;
protected void setUp() throws Exception {
//set up an email instance
this.email = new Email();
this.email.setBody("body");
this.email.setFromAddress("dev@dev.com");
this.email.setSubject("validate me");
this.email.setToAddress("ag@ag.com");
}
protected void tearDown() throws Exception {
this.email = null;
}
//EmailTest continued...
}
|
有効なEmailがあるので、testEmailValidateInstanceof()はこのインスタンスがValidatableのタイプである事を保証します(リスト10)。
リスト10. Junitのinstanceofチェック
public void testEmailValidateInstanceof() throws Exception{
TestCase.assertEquals("Email object should be of type Validatable",
true, this.email instanceof Validatable);
}
|
次のテストケースでは、意図的にtoAddressフィールドをnullに設定しておいてから、validateAddress()へのコールがfalseを返す事を検証しています(リスト11)。
リスト11. JnitのtoAddressのnullチェック
public void testEmailAddressValidateNull() throws Exception{
//force a false
this.email.setToAddress(null);
Validatable validtr = (Validatable)this.email;
TestCase.assertEquals("validateAddress should return false",
false, validtr.validateAddress());
}
|
最後のステップは、念のためです。testEmailAddressValidateTrue()テストケースはEmailインスタンスの初期値(toAddressフィールドはag@ag.comに設定されています)でvalidateAddress()を呼びます。
リスト12. JUnitのtoAddress非nullチェック
public void testEmailAddressValidateTrue() throws Exception{
Validatable validtr = (Validatable)this.email;
TestCase.assertEquals("validateAddress should return true",
true, validtr.validateAddress());
}
|
アーキテクチャー・チームはSendableインターフェースで通信実装を抽象化しようと懸命に努力したのですが、最初の試みではこのインターフェースを無視してしまったようです。このチームはEmailオブジェクトの静的横断で得られた教訓から、約束の振る舞いを上の階層に移動してSendableベース・インターフェースに入れる事で、さらに改善を加えます。
新しいアスペクトは、ベンダーのValidatableインターフェースを備えたSendableインターフェースの継承を作成します。そうするとさらに、実装された振る舞いがアスペクト内に作成されます。今度は別の通信オブジェクトFax、に対してvalidateAddress()メソッドが定義されます。これをリスト13に示します。
リスト13. 改善されたアスペクト
import com.acme.validate.Validatable;
public aspect SendableValidateAspect {
declare parents: Sendable extends Validatable;
public boolean Email.validateAddress(){
if(this.getToAddress() != null){
return true;
}else{
return false;
}
}
public boolean Fax.validateAddress(){
if(this.getToAddress() != null
&& this.getToAddress().length() >= 11){
return true;
}else{
return false;
}
}
}
|
このAPI例はちょっと不自然かも知れませんが、エンタープライズ・アーキテクチャーに静的横断を応用する事がどれほど簡単かを説明する事はできたのではないかと思います。静的横断はここで説明したような形式のシナリオ(目立たずに、オブジェクトの振る舞いや定義まで修正するのにも使用できる)では特に効果的ですが、他にも多くの使い方があります。例えばPOJOs (plain old Java objects)を展開時に「EJB化」するのに静的横断を使ったり、ビジネス・オブジェクトに使ってHibernateのようなパーシスタンス・フレームワークのライフサイクル・インターフェースを機能強化したりする事もできます(参考文献)。
どんなアプリケーションであれ、静的横断を使う事で、エンタープライズ・アプリケーション・コードの効果を妨げるような障害が優雅に解決できるようになります。この記事ではその手法の基礎と、最も基本的な使用法を学びました。アスペクト指向プログラミングと他の横断技法については参考文献の資料を参照してみてください。
- この記事で使用しているソースコードをダウンロードしてください。
-
eclipse.org/aspectjから、AspectJおよび関連するツールをダウンロードすることができます。サイトにはFAQやメーリングリスト、優れた資料もあり、他のAOPの参考文献へのリンクもあります。さらに調査をするのに最適です。
-
AspectWerkzはJavaプラットフォーム用の、動的で軽量な高性能の AOP/AOSDフレームワークです。
-
EclipseIDEではAspectJ プラグインをとり上げています。
- アスペクト指向のソフトウェア開発に関する包括的な情報源としてAOSD.netを見てみてください。
-
HibernateはJavaプラットフォーム用の、強力で超高性能なオブジェクト/リレーショナル・パーシスタンス照会サービスです。
-
Codehausは興味深いオープンソース・プロジェクトの宝庫であり、AspectWerkzやNanning(Javaプラットフォーム用として別のアスペクト実装)などがあります。
- developerWorks のJava technologyゾーンにはJavaプログラミングのあらゆる面に関する記事が豊富に用意されています。
- Java technologyゾーンのチュートリアル・ページにはdeveloperWorksで紹介されたJava関連のチュートリアルの完全なリストがありますので見てみてください。
