#はじめに
ゼミで自分の気になる分野の研究を発表する機会がありました。
色々調べても面白いと思える論文がなく、唯一面白いと思えたのがアスペクト指向でした。
とても古い技術なので既に知っている人や興味のない人は多いと思いますが、面白いと思ったので紹介したいと思います。
この記事を読んで少しでもアスペクト指向に興味を持ってくれる人がいたら嬉しいです。
※アスペクト指向ではオブジェクト指向の時のような専門用語?が多くあります。
その用語を説明するには他の用語と密接に関わりがあったりするので、この用語はなんだ?ってことが多くあると思います。
ですが、そんな言葉があるんだ程度に流してもらっていいと思います。
#アスペクト指向とは?
オブジェクト指向の次に来る技術で、ポストオブジェクト指向であると紹介される
だがそれは違くて、アスペクト指向はオブジェクト指向の限界を補い、さらに発展させるための技術である
#ソフトウェア工学でのそもそもな矛盾
ソフトウェアでは凝集度は高く、結合度は低くことが良いとされている
だが、これはそもそも真逆の方向を向いている
凝集度が高いプログラムが良ければ、機能ごとに関数を作りまくれば良い
結合度が低いプログラムが良ければ、関数を一つにまとめてしまえば良い
関数型、オブジェクト指向のようにプログラミングパラダイムが変わってきたのは、これらの中間点をどこに置くかが変わってきたということとつながっていると思う
#アスペクト指向はどこに置いた?
オブジェクト指向は凝集度が高くしたパラダイムだと思う
一つのモジュールをクラスとして、モジュールをたくさん作り、あるモジュールからあるモジュールを呼び出す方法を取っている
これでは結合度は高くなってしまう
この結合度を低くするための一つの技法がGoFのデザインパターン
アスペクト指向は凝集度は高く、結合度を低くするために、記述する際はモジュールごとに分割し、コンパイルする際に一つのモジュールにしてしまえば凝集度は高く、結合度は低く保てるという考え方から始まった
アスペクト指向の最初の考え方
#アスペクト指向が有効な理由
ソフトウェアを作る時にある、特定の品質欲求を満足するためのコードは、プログラムの基本的なモジュール構成に対して横断的に散らばることが多い
例としてロギングがよくあげられ、テストやセキュリティのためにログを吐くコードは色々なクラス(モジュール)に書かれている
これは横断的関心事であり、モジュール化せずに横断的なままにしておくと、品質欲求は満足できるがプログラム全体の見通しが悪くなり、開発効率や保守性が低下してしまう可能性がある
横断的関心事のモジュール化に有効な技術の一つがアスペクト指向プログラミングである
#関心事の分離とは?
関心事(Concern)とは、コンピュータ科学の用語で処理(メソッド)をひとまとまりにまとめたクラスは一つの関心事である
プログラムを関心事ごとに分離して異なるモジュールにする原則のことを、関心事の分離(Separation of Concerns)という
分離とはそれぞれの関心事に関連するコードを集めて独立したモジュールとして、他のコードから分離する
関心事ごとに異なるモジュールになっていると、プログラムの機能拡張や修正が楽になる
アスペクト指向はオブジェクト指向ができなかった複数の視点からの関心事の分離を可能にした
#AspectJ
Java言語にアスペクト指向プログラミングのパラダイムを追加し拡張した言語
主のアスペクト指向プログラミングとは少し違く、クラスとアスペクトが同居している
今までの視点でモジュール(クラス)構成を行い、横断的関心事が存在すればアスペクトでモジュール化させる方式
ジョインポイントを「場所」と考えるのを静的ジョインポイント・モデル
「とき」と考えるのを動的ジョインポイント・モデルという
AspectJは動的ジョインポイント・モデルを採用している
#アスペクト
Javaでのクラスのような存在
Javaではクラスがメンバやメソッドなどをまとめて記述するが、
AspectJではアスペクトが『アドバイス』『ポイントカット』『インタータイプ宣言』などをまとめて記述する
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Before
@Aspect
public class Aspect {
@before("execution(* hoge.*.*(..))")
public void before() {
System.out.println("before!");
}
}
public aspect GreetingAspect {
pointcut message():
call(* Messenger.printMessage(..));
before(): message() {
System.out.print("Hello, ");
}
}
#ポイントカット
アドバイスは設定したメソッドが呼び出された時にアドバイスを呼び出すように設定する
メソッドが呼び出された時に発生したイベントとアドバイスが紐づいているイベントが一致しているかを判断してくれるのがポイントカットである
(※本当はイベントが発生しているわけではありません)
ボタンがクリックされた時などのEventListenerへの登録のような感じと思ってください
##ポイントカットの種類
ポイントカットには動的ポイントカットと静的ポイントカットが存在する
###メソッド関連のポイントカット
メソッド呼び出しに関係するポイントカットにはcallとexecutionの2つがある
引数にメソッドパターンを取り、メソッドが呼び出されて実行された「とき」を表す
call(void Main.init())
execution(void Main.init())
// 上記ともMainクラスのinitメソッドが呼び出されたときを表す
call
プログラムの実行スレッドが、該当するメソッド呼び出し式に達してから、呼ばれたメソッドの本体を実行し、ふたたび呼び出し式の側に戻ってくるまでの間の一連の処理
execution
呼ばれたメソッドの本体の中にプログラムの実行スレッドが移った後、メソッド本体の実行が始まってから終了するまでの間の一連の処理
実行スレッドが呼ばれたメソッドの側に間になされる処理
一般的には
【callはメソッドを呼び出した『とき』を選択し、executionは呼ばれたメソッドが実行される『とき』を選択する】
と覚えておくとよい
####callとexecutionの違い
superを使った非staticメソッドの呼び出しはexecutionには選択されるが、callには選択されない
callポイントカットは、呼び出される表面的な型を基準に選択するが、executionポイントカットは、呼び出されるオブジェクトの本当の型を基準に選択する
/*クラス部分*/
interface Main {
public abstract void init();
}
class Sub implements Main {
public Sub() {
}
public void init() {
}
}
/******************* call ***********************/
/*アスペクト部分*/
call(void Sub.init())
/*実行部分*/
Sub sub = new Sub();
Main s = sub;
sub.init(); //選択される
s.init(); //選択されない
((Main)sub).init(); //選択されない
/************* execution ***********************/
/*アスペクト部分*/
execution(void Sub.init())
/*実行部分*/
sub.init(); //選択される
s.init(); //選択される
((Main)sub).init(); //選択される
###フィールド関連のポイントカット
フィールドに関連するポイントカットにはgetとsetの2つがある
引数にフィールドパターンを取り、フィールドを読み書きした「とき」を表す
// Personクラスのint型でxという名のフィールドの値を参照したとき
get(int Point.x)
// Personクラスの非privateフィールドの型がString型の任意の名前のフィールに値を代入した時
set(!private String Person.*)
// 任意の型で任意のクラス・インターフェースに宣言されている任意の名前のフィールドに値を参照した時
get(* *)
get
フィールドを参照した(呼んだ)とき
set
フィールドに値を代入したとき
###コンストラクタ関連のポイントカット
フィールドに関連するポイントカットにはcallとexecutionとinitializationとpreinitializationの4つがある
引数にコンストラクタパターンを取り、合致するコンストラクタでオブジェクトが生成される「とき」を表す
call(public Point.new(int, int))
initialization(*.new(..))
call
new演算子を実行している間
プログラムの実行スレッドが、new演算子を実行している側から、コンストラクタの本体へ移り、実行終了後に再び元の側へ戻るまでの間
execution
superやthisによる他のコンストラクタ呼び出しが終了した後、最初に呼ばれたコンストラクタが実際に実行されている間
superやthisによって呼び出された他のコンストラクタが実行されている間は含まない
initialization
superによるコンストラクタ呼び出しが終了した後から、呼ばれたコンストラクタが終了するまでの間
executionと異なり、thisによって他のコンストラクタを呼び出して実行している間を含む
preinitialization
呼ばれたコンストラクタの時刻が始まってから、superによるコンストラクタ呼び出しを実行する直前までの間
途中で、thisによって別のコンストラクタを読んだり、thisやsuperによる呼び出しの実引数を計算したりする時間を含む
###catch節関連のポイントカット
プログラムの実行中に例外が投げられると、実行中のメソッドはすべて途中で中止される
これを避けるためにtry-catch分を使って例外からの回復処理をプログラム中に書くことができる
catch節に関連するポイントカットにはhandlerが用意されている
引数にタイプパターンを取り、catch節が実行される「とき」を表す
// クラス名を直接書くか、ワイルドカードを使って書く
handler(java.io.IOException+)
###その他ポイントカット
他にもポイントカットはあるのですが、長くなるので別でまとめました
もっと細かく指定したいと思ったら見てください
より細かいポイントカットを行うためのポイントカット一覧
#アドバイス
アスペクト内の関数であり、クラスでいうメソッドとようなものである
ポイントカットで選択されたジョインポイントにアドバイスが挿入(織り込む:weave)される
ジョインポイントは何らかの処理が実行された「とき」を表すが、その「とき」は一瞬ではなく期間である
その期間の中でどのタイミングでアドバイスを実行するかを選ぶことができる
上記の図のように、ジョインポイントにも期間があり、アドバイスを織り込む場所は複数存在する
##アドバイスの種類
上記で説明したように、アドバイスの種類によって織り込む場所が決まる
###beforeアドバイス
選択されたジョインポイントが表す期間の直前(あるいは開始時)に実行する処理のこと
beforeアドバイスの宣言の例
before(): loggedMethods() {
System.out.println("** execute doIt() ...");
}
beforeアドバイスは、キーワードbeforeから始まる
before()の括弧内にはアドバイスの引数を書く
before():のコロンと{}の波括弧の間には任意のポイントカットを書く
beforeアドバイスの使用例
pointcut pickup(BookSession bs, int i):
call(String String.substring(int)) && this(bs) && args(i);
before(BookSession bs, int i): pickup(bs, i) {
System.out.println("** substring(" + i + ")");
}
pickupポイントカットが引数をとるので、アドバイスも引数をとる
before(BookSession bs, int i)と書くことで、その引数をポイントカットに渡すことができる
pickupポイントカットは選択したジョインポイントごとに、引数にしかるべき値を代入する
代入された値は、アドバイスの本体の中で、普通の変数と同様に扱うことができる
###afterアドバイス
選択されたジョインポイントが表す機関の直後(あるいは終了時)に実行する処理のこと
afterアドバイスの宣言の例
after(): loggedMethods() {
System.out.println("** execute doIt() ...");
}
###after returningアドバイス
選択されたジョインポイントが表す期間の処理が、正常に終了した時に実行される処理のこと
途中で例外が投げられて異常終了した時は実行されません
after returningアドバイスの宣言例
after() returning (String s):
call(* *(..)) && within(BookSession)
{
System.out.println("Returned Normally with " + s);
}
after returningアドバイスは正常終了した時の戻り値を取り出して、アドバイスの本体の中で使うことができる
###after throwingアドバイス
選択されたジョインポイントが表す期間の処理が例外を投げて異常終了した時に実行される処理のこと
after throwingアドバイスの宣言例
after() throwing (IOExecption e):
call(* *(..)) && within(BookSession)
{
System.out.println("Threw an exception " + e);
}
throwingの引数eは投げられた例外の値で、アドバイス本体の中で自由に利用できる
###aroundアドバイス
選択されたジョインポイントの期間全体の処理の代わりに実行される
ジョインポイントがメソッドを呼び出した時だとすると、aroundアドバイスだけが実行され、元のメソッド呼び出しは全く実行されない
記述の仕方は戻り値の型 around (引数例)
と書く
戻り値の型はジョインポイントの戻り値の型と一致しなければならない
aroundアドバイスの使用例
aspect Overrider {
int around(): call(int Hello.say()) {
System.out.println("Hi");
return 1;
}
}
public class Hello {
public static int say() {
System.out.println("Hello");
return 0;
}
public static void main(String[] args) {
int i = say();
System.out.println(i);
}
}
実行結果
アラウンドアドバイスが存在するとき
Hi
1
アラウンドアドバイスが存在しないとき
Hello
0
#ジョインポイント
何らかの処理が実行された「とき」を表す
ポイントカットによって選択された、元の処理が実行され、アドバイスを織り込む場所のこと
結合点と訳されることもある
プログラムの実行中に起こるイベントのようなものと考えてよい
#インタータイプ宣言
プログラムの静的な構造を変更するアスペクトの機能
他のクラスやインタフェースのメソッドやフィールを宣言したり、名前を変更したり、新しいインタフェースを追加で実装するように他のクラスを変更したりできる
##インタータイプ・メンバー宣言
他のクラスやインタフェースのメンバを、アスペクトの中で宣言すること
【インター(inter)】とは【~の間】という意味
クラスやインターフェースとアスペクトの間にまたがるまたがる宣言
だからインタータイプ宣言(inter-type)といわれる
###インタータイプ・メソッド宣言
他のクラスやインターフェイスのメソッドをアスペクト内で宣言することができる
普通のメソッド宣言の書き方と違うところは、メソッド名の前にそのメソッドが宣言されているクラス名、あるいはインタフェース名を書くこと
それ以外はほぼ同じで、thisを利用することもできる
インタータイプ・メソッド宣言の使用例
aspect interTypeMethod {
public void Hello.say() {
System.out.println("Hi, I'm " + name);
}
}
public class Hello {
String name;
Public Hello(String name) {
this.name = name;
}
Public static void main(String[] args) {
Hello h = new Hello("Taro");
h.say();
}
}
実行例
Hi, I'm Taro
例でわかるように、Helloクラスに宣言したようにsay()メソッドを呼び出すことが可能
say()メソッド内ではHelloクラスのnameフィールドまで利用している
コンパイル時にHelloクラスにWeavingされていることが理解できていれば、あまり不思議なことではない
インタフェースへのインタータイプ・メソッド宣言
正規のJava言語では、インタフェースにはabstractメソッドしか宣言できない
しかし、インタータイプ宣言では、インタフェース用に非abstractメソッドを宣言することができる
aspect interTypeMethod {
public void Greeting.say() {
System.out.println("Hi, I'm " + name);
}
}
interface Greeting {
}
public class Hello implements Greeting {
String name;
Public Hello(String name) {
this.name = name;
}
Public static void main(String[] args) {
Hello h = new Hello("Taro");
h.say();
}
}
interTypeMethodアスペクトでGreetingインタフェースにsay()メソッドを宣言していることがわかる
だが、Greetingインタフェースを実装しているHelloクラスにはsay()メソッドをオーバライドしていない
これからわかるように、アスペクトでインタフェースにメソッドを宣言すると、非staticメソッドを宣言していることになる
###インタータイプ・フィールド宣言
クラスやインターフェースのフィールドを宣言することができる
正規のJava言語では、インタフェースに非finalフィールドを宣言することはできないが、
インタータイプ宣言では、finalでないフィールドを宣言することが可能
aspect interTypeField {
private String Hello.name;
}
public class Hello {
Public Hello(String name) {
this.name = name;
}
public void say() {
System.out.println("Hi, I'm " + name);
}
Public static void main(String[] args) {
Hello h = new Hello("Taro");
h.say();
}
}
###インタータイプ・コンストラクタ宣言
クラスやインタフェースのコンストラクタを宣言することができる
さすがのインタータイプ宣言でも、インタフェースのコンストラクタを宣言することはできない
インタータイプ・コンストラクタ宣言内でthisやsuper()を使うことは可能
aspect interTypeField {
Public Hello.new(String name) {
this.name = name;
}
}
public class Hello {
String name;
public void say() {
System.out.println("Hi, I'm " + name);
}
Public static void main(String[] args) {
Hello h = new Hello("Taro");
h.say();
}
}
#最後に
最後まで見て頂きありがとうございます。
僕はアスペクト指向を専門にしているわけではないので間違っていことも多くあったと思います。
調べた内容も古いものなので、今の現状とは間違っている部分もあると思います。
今回の選んだ内容はすべて僕の独断と偏見です。
他にもAspectJには素晴らしい機能がたくさんあります。
インタータイプ宣言にもアクセス修飾子やクラスの継承先の変更など多くあります。
詳しいことはまた違う記事で書いていきたいと思います。
最後まで見て頂き本当にありがとうございます。この記事を読んで少しでもアスペクト指向に興味を持ってもらえたならうれしいです。
ミスや間違いが多く存在すると思いますがご了承ください。
#参考