{t-isio, kamiya, kusumoto, inoue}@ist.osaka-u.ac.jp アスペクトを用いた表明の記述 石尾 隆†,神谷 年洋‡, 楠本 真二† ,井上 克郎† †大阪大学 大学院情報科学研究科 ‡ 科学技術振興機構 さきがけ {t-isio, kamiya, kusumoto, inoue}@ist.osaka-u.ac.jp 発表 25 分 (18~20)
概要 表明とは アスペクト指向プログラミング アスペクトを用いた表明の記述 今後の課題 本発表では,まず表明とは何か,ということについて,また,その利点と弱点について説明します.次に,アスペクト指向プログラミングについて簡単に触れた後,提案手法であるアスペクトを用いた表明の記述について説明します.最後に,研究の現状と,今後の課題について説明します.
表明とは 表明(assertion) 特定の言語要素が「何をするのか」 その言語要素が実行される時点で(または直後に) 成立していなくてはならない条件 実現方法とは無関係 例: 「sort 関数の終了時,配列の中身は昇順で整列」 「この関数は quick sort を使う」とは書かない 対応する実行時点ごとに異なる呼び名 メソッド実行の前後: 事前条件,事後条件 すべてのメソッドの実行前後: クラス不変条件 ループ文実行中: ループ不変条件 それではまず,表明とは何か,ということについて説明します.表明とは,関数あるいはメソッド,プログラム文など,ある言語要素に対して記述され,その言語要素が何をするのかを記述したものです.一般的には,その言語要素の実行直前あるいは実行直後に成立しているべき条件を,プログラム中に記述するという形になります. 表明は,何をするか,にだけ注目しており,実現方法については言及しません.たとえば,sort 関数に対して,表明として「この関数の終了時には,配列の中身は整列している」と記述することはあっても,「この関数は quick sort で配列の中身を整列させる」といった具体的な実現方法について記述することはありません.そのため,手続きやメソッドなどの機能を抽象的に表現している,といえます. 表明は,対応する言語要素に応じて,異なる呼び名が付いています.たとえば,メソッド実行の前後で成立している条件を記述する表明を事前条件,事後条件と呼んだり,すべてのメソッドの前後において成立する条件をクラス不変条件と呼んだりします.その他にも,ループの実行中,通常はその先頭で成り立っている条件を記述するループ不変条件などがあります.
表明の用途 早期の故障検出 責任の分担 表明の違反=プログラムが想定外の状態である 障害が他の場所に波及する前に検出する 障害範囲の切り分けによる原因の早期解明 責任の分担 ある一定範囲を表明で区切って責任を分担 曖昧さのない分担が可能 契約による設計 手続きの事前条件を満たすのは利用側の責任 手続きの事後条件を満たすのは手続き側の責任 表明の第一の用途は,早期の故障検出です.表明に記述された条件に違反している,ということは,プログラムが想定外の状態となっていることを示しています.その状態のままプログラムの実行を続けることで影響が他の場所へ波及する前に,故障として検出することで,原因の早期解明にもつながります. また,その他の用途としては,責任の分担があります.一定範囲を表明で区切ることで,曖昧さのない形で開発の責任を分担を行うことができます.契約による設計では,メソッド実行の前後で成立しているべき事前条件,事後条件を記述しておくことで,手続きの事前条件を成立させるのは利用側の責任,その事前条件をもとに事後条件を満たすのが手続き側の責任,というように分担を行います.
利用可能な記述手段 表明文 (assert 文) 事前条件,事後条件,クラス不変条件 条件検査を行うプログラム文 条件式 = true なら何もしない, false ならプログラム停止 Java や C++ など,一般的な言語で利用可能 事前条件,事後条件,クラス不変条件 Eiffel ではすべてサポート Java,C++ では JML, Larch などの支援ツール 表明は,既に,様々なプログラミング言語で利用可能となっています.たとえば C言語やJavaでは,assert文という,条件の検査を行う文が利用できます.違反した場合は,その時点で,プログラムが停止されます. また,メソッド前後で条件を記述する事前条件,事後条件などの記述をサポートした言語としてはEiffelがあります.また,同様の記述をJavaやC++で行うための,JML,Larch といった,プリプロセッサ等の形式で扱う拡張言語と支援ツールも存在しています.
表明を用いる際の問題 安全性と再利用性のバランス 表明として記述できる範囲の制限 実際に表明を使用する場合,次のような2つの問題が出てきます. ひとつは,安全性と再利用性のバランス,もうひとつは,表明として記述できる範囲の制限に関する問題です. これから,順番に説明していきます.
安全性と再利用性 表明は安全性を高める コンポーネントの2種類の制約 安全のためには書きたい 条件を書きすぎてしまうと再利用しにくい コンポーネント本来の制約 例: このList には「任意の null でないオブジェクトを格納する」 一般的なオブジェクトに適用できるので,再利用が容易 アプリケーションが必要とする「最小限の」機能 例: このListには「長さ1以上の文字列を格納する」 誤ったオブジェクトを格納する可能性はなくなる まず,表明の安全性と再利用性の問題について説明します. コンポーネントに対して,表明の記述を追加すればするほど,その時点で多くの条件が成立していることになるため,その時点で許される状態の自由度が減少し,予測可能な範囲に収まります.その結果,コンポーネントは安全となっていきます.しかし,その分,その表明が想定している状態以外はすべて違反とされてしまうため,再利用性の低下を招いてしまいます. このような問題の例として,ここでは,コンポーネントの持つ2種類の制約を挙げます.2種類の制約のうち1つは,コンポーネントが本来持っている制約です.たとえば,ある List クラスについて,null 参照でさえなければ,どんなオブジェクトでも格納できる,というように実装できたとします.すると,格納するオブジェクトは null でない,ということがそのコンポーネントの外すことのできない制約になります. 2種類のうちもう1つは,アプリケーションが必要とする最小限の機能に関わる制約です.たとえば,List クラスを作っていた動機が,長さ1以上の文字列を管理したい,というものだった場合,長さ1以上の文字列を格納できるということがList側の制約となります. ここで,長さ1以上の文字列だけを格納する,という制約を採用すると,その List に対して誤って他のオブジェクトを格納してしまう危険性はなくなりますが,今後 List クラスを再利用していくとき,他のオブジェクトを格納したい,といった用件に対して,実際には格納できることは分かっているにも関わらず,再利用できなくなってしまいます.
「安全な再利用」のための制限 Behavioral Subtyping あるオブジェクトが,別のオブジェクトの代替となる条件 Strong (拡張ではない) Specialized Implementation require (事前条件) Original Component Extension この問題の背景には,Behavioral Subtyping という条件があります. Behavioral Subtyping とは,あるオブジェクトが,別のオブジェクトの代替となるための条件です. コンポーネントを拡張する際には,具体的には,事前条件は同じか弱くなること,つまりオブジェクトが,元のオブジェクトが動く状況では必ず動くことが要求されます.また,事後条件は同じか強くなること,つまり元のオブジェクトが保証している条件は少なくとも同じように保証すること,が求められます. しかし,先ほどの例であれば,任意のオブジェクトを格納できるList を,文字列だけを格納する List として使う場合は,事前条件,事後条件ともにより強くなってしまうので,Behavioral Subtyping の関係にはならず,相互に再利用することができません.安全性のために,そのコンテキストでのみ事前条件を強めて使用したいという要求がある場合,また,そもそも表明が記述されていないようなコンポーネントにおいて事前条件がまったくないものを再利用する場合などには,後から条件を追加・削除することが重要となってきます. Behavioral Subtyping Simple Implementation Generalization Weak Weak ensure (事後条件) Strong
アプリケーション制約の分離 アプリケーション制約とコンポーネントの制約を分離 既存の手法 アスペクトによる制約の分離 アプリケーションの制約を後から追加できるようにする 既存の手法 総称型 (generics) : 型に対する制約 (C++, Eiffel) 部分範囲型 : 値の範囲を制限する (Pascalなど) アスペクトによる制約の分離 アスペクトの追加,削除によって制約を管理 汎用的なコンポーネント定義+アスペクトによる表明 汎用的なコンポーネントの再利用性を損なわない コンテキストに応じた安全性を付加する この問題を解決するためには,アプリケーションの制約と,コンポーネント側の制約とを分離して,後からアプリケーション側が,その利用コンテキストに応じた制約を追加できるようにすることが考えられます.総称型あるいは型パラメータなどと呼ばれる,使用する型を後からパラメータとして決定する方式や,Pascalの部分範囲型のように実際に用いる値を制限する方式などがあります. しかし,現状では,型による制約などだけでは,先にあげた「長さ1以上の文字列」といった,属性にまで踏み込んだ制約を記述することができません. そこで,アスペクトとして,制約の条件判定を後から付加することを提案します. アスペクトとしてアプリケーション制約の表明を記述し,オブジェクトに付加することで,再利用が容易な,汎用的なコンポーネントを開発者が作成した場合には,それをアプリケーションごとのコンテキストに応じた安全性を重視した表明の中で取り扱います.提案手法の詳細については,後で述べることとし,もう1つの問題点について先に説明します.
表明が記述できる範囲の制限 表明で言及できるもの 表明で言及できないもの その言語要素から可視である範囲 その言語要素から不可視の範囲 assert 文が記述されているメソッドのローカル変数 そのオブジェクト自身のフィールド 所有しているオブジェクトの公開フィールド 表明で言及できないもの その言語要素から不可視の範囲 assert 文が記述されているメソッドを呼び出したオブジェクト 所有しているオブジェクトの非公開フィールド 言及できないものは通常コメントなどとして記述される 例: 「メソッドXは,メソッドYの作業用で,Yからのみ使用する」 もう1つの問題点は,表明で記述できる範囲の制限です. Assert 文などは,通常のプログラム文として記述されるため,その文から可視である範囲の情報にしかアクセスできません.アクセスできる範囲の例をあげると,その assert 文が書かれているメソッドのローカル変数,そのメソッドが宣言されているクラスのメンバ変数や,また,そのオブジェクトが参照しているオブジェクトの,可視であるようなメンバ変数などです. 逆にアクセスできないものとしては,そのメソッドを呼び出すオブジェクトや,所有しているオブジェクトの非公開メンバ変数などがあります.多くの場合,表明として言及できないものは,コメントとして,たとえば「メソッドX は,メソッド Y の作業用で,Yからのみ使用する」といったように記述されますが,強制力を持つことはありません.
表明で記述したいもの 複数のオブジェクトを横断した表明 複数のオブジェクトに対して記述を行う場合: 一連の相互作用の事前条件・事後条件 例: ファイルから読み込まれた一連のオブジェクト群の状態 メソッドを呼び出すオブジェクトに対する条件 例: ラッパーオブジェクト以外からのアクセスを禁止する Façade デザインパターンなどで起こる 複数のオブジェクトに対して記述を行う場合: カプセル化が破壊される モジュール間の依存関係が増加する 1. アスペクトとしてオブジェクトとは別個に記述する 2. 制御フローに対する記述を可能とする 現在の表明機構では記述できないが,記述したいものとして,複数のオブジェクトを横断した表明があります.具体的には,オブジェクト間の一連の相互作用の結果として得られるような成果に対する表明や,メソッドを呼び出したオブジェクトに対する条件があります. オブジェクト間の相互作用では,メソッド単体で条件が成立するのではなく,複数のオブジェクト,複数のメソッドが呼ばれることによって成立する条件,といったものが存在します.たとえば,ファイルから一連のオブジェクト群の情報を読み込む場合,読み込み途中ではそれぞれのオブジェクトに関する表明がすべて成立するとは限りませんが,終了した時点では,何らかの関係が成立している,ということが起こります. また,メソッドを呼び出したオブジェクトに対する条件としては,そのメソッドを呼べるのはラッパーオブジェクトのみであり,それ以外のオブジェクトからのアクセスであれば禁止する,といったものがあります.この条件は,Façade デザインパターンなどで発生する場合があります. しかし,これらは,先に述べたような,オブジェクトの可視性に従っている現状の表明では記述することができません.かといって,オブジェクトから他のオブジェクトに対する参照を追加したり,制御フロー情報を表現するような引数やフラグ変数を作成してしまうと,本来のモジュールによるカプセル化を破壊してしまったり,モジュール間の依存関係を増加させてしまうことにつながります. これに対して,アスペクトとして,オブジェクトとは別個に記述することで,カプセル化の破壊などを起こさずに,目的を達成することができると考えられます.また,制御フローに関する情報を可視化することで,フラグ変数などを介さずに,直接的な表明の記述が可能となると考えます.
アスペクトを用いた表明の記述 アプリケーションと,コンポーネントの制約を分離したい 複数のオブジェクトを横断した制約を記述したい アスペクトとして表明をオブジェクトから分離する 呼び出し側に関する表明を記述したい 制御フローに関する表明の記述方法を追加する 現状の問題点と,提案手法とを整理すると,このようになります. アプリケーションとコンポーネントの制約を分離したい,また,複数のオブジェクトを横断した制約を記述したい,という要望から,アスペクトとして表明をオブジェクトから分離することを提案します. また,呼び出し側に関する表明を記述したいということから,制御フローに関する表明の記述方法の追加を行います.
アスペクト指向プログラミング 目的:モジュールを横断した関心事を別モジュールに分離 AspectJ が用いる実現手段: Join Point Model ある実行時点 (join point) に処理(advice)を連動させる メソッド呼び出し,メソッド実行,フィールド参照,フィールド代入,例外送出 連動した advice は,その実行時点の情報にアクセスできる 例: 「String クラスへのメソッド呼び出し」の直前に,そのメソッド名を記録する aspect LoggingStringAccess { before(): call (* String.*(..)) { Logger.logs(thisJoinPoint.getSignature()); } ここで,アスペクト指向プログラミングについて,簡単に説明します.アスペクト指向プログラミングは,ベースとなるモジュール機構において,オブジェクト指向プログラミングであればオブジェクトという単位において,複数のモジュールを横断したような関心事を別モジュールとして分解することで,モジュール性を向上しよう,という考えに基づいたプログラミング手法です. その実現手段として,代表的なアスペクト指向プログラミング言語である AspectJ が用いている Join Point Model があります.これは,プログラム中の実行時点のことを Join Point と呼び,その中から選んだ集合に対して,アドバイスと呼ばれるメソッドに類似した処理単位を関連付けるというものです.ここで,実行時点としてはメソッド呼び出しやフィールド参照などを使用します. 連動したアドバイスは,その実行時点の情報にアクセスしながら,処理を行うことができます.たとえば,String クラスへのメソッド呼び出しの直前に,そのメソッド名を記録する,というコードは,AspectJ ではこのように記述します. 実行時点情報
アスペクトとしての表明の記述方法 表明=プログラムの特定の実行時点で,条件を検査する 表明文(assert文) の記述方法だけを拡張 assert 文を,開発者が自由に配置できる Join Point と考える 現在の AspectJ などでは,直接的に表現できない 表明文(assert文) の記述方法だけを拡張 事前条件および事後条件は既存の言語要素で記述 例: AspectJ を用いた事前条件および事後条件の記述 before(): メソッド実行時 { assert (条件式); } after(): メソッド実行時 { 表明は,プログラム特定の実行時点で条件を検査する処理であることから,assert 文は開発者が自由に配置できる Join Point であると考えて,assert 文の記述方法を変更します.本研究では,assert 文の拡張だけに集中します.これは,事前条件や事後条件といった他の記述法については,たとえばAspectJ などの言語処理系と併用することで記述できると考えられるためです.
表明の記述方法の拡張 表明の宣言に用いる関数と実装を分離する assert ( FileIsOpened(f) ); // 文の書き方は同じ // 定義はアスペクト側に記述する ASSERT FileIsValid(File f) { return f.exists(); // boolean 値を返す } // 本体の定義は複数あってもよい: 複数ある場合は AND をとる ASSERT FIleIsValid(File f) { return f.canWrite(); 本研究で提案する表明の記述法を説明します. 簡単には,表明文の条件式で用いる関数の宣言と,関数本体の定義とを分離することです.特別な点は,定義の本体が複数存在することを許すことにあります.複数の定義が存在する場合には,それらすべての定義で表明が成立していなければならない,と考えます. これによって,アスペクトごとに,特定の表明関数に対して,新たな定義を追加していくことで,条件を厳しくすることが可能となります.現在の AspectJ などを用いても,この処理と同様の記述は可能となっていますが,これらの判定関数が表明用である,という事を直接的に記述できるようにするために,このような方法を選択しました.
制御フローに関する表明 AspectJ における cflow pointcut を論理式用に用いる cflow ( object.method ) 指定された object のメソッド method が実行中(呼び出しスタック上に存在している)なら true を返す クラスで指定された場合はそのクラスの任意のインスタンスが対象 例: writeFile 処理のときのみ,指定ファイルが書き込み可能であることを前提とする ASSERT FileIsValid(File f) { return !cflow(DB.writeFile())||f.canWrite(); } 提案手法のもう1つの要素である,制御フローに関する表明の導入について説明します. 制御フローに関して記述する既存の言語要素として,AspectJ が持つ cflow pointcut があります.これは,指定されたメソッドのシグネチャが呼び出しスタック上に存在しているかどうかを判定するための言語要素です. この表記と同様の記法を,表明のための論理式として使用することで,制御フローに関する記述を行います.cflow キーワードに引数としてオブジェクト参照とメソッドシグネチャを渡し,それが呼び出しスタック上に存在しているかどうかで,真偽値を返すようなリフレクション関数とします. このような呼び出しのコンテキストに応じた記述は,AspectJ の cflow ポイントカットでも可能ですが,論理式として記述することで,より簡潔な記述を行うことができます.
表明記述の AspectJ 記述との比較 提案手法による記述例 ASSERT FileIsValid(File f) { return ! cflow ( DB.writeFile() ) || f.canWrite(); } AspectJ で記述した例 pointcut FileIsValid_for_WriteFile (File f): cflow(execution(void DB.writeFile())) && args(f) && execution(boolean AssertionAspect.FileIsValid(File)); Object around FileIsValid (File f): FileIsValid_for_WriteFile(f) { Boolean b = (Boolean)proceed(f)).booleanValue(); return f.canWrite() && b; 実際に,今述べたような cflow の記述を,実際に AspectJ で記述しようとすると,このようになります. コンテキストに対応する実行時点を選び,実際の判定処理に加えて,他の表明関数の動作を損なわないように,記述を追加しています.この記述では,DBクラスのインスタンスが何であるか,ということに注意を払っていませんが,特定のインスタンスが指定された場合には,さらにどのDBオブジェクトに対してメソッドが呼ばれたか,という判定を行うためのフラグ変数の導入などの記述が必要となります.
提案手法の利点 表明をアスペクトとして記述する アプリケーションに応じた表明の追加が可能 コンテキストに応じた表明の記述 安全性の向上 再利用性の阻害を抑えられる コンテキストに応じた表明の記述 cflow の使用 表明の文そのものはプログラム文として通常の記述 いつ実行されるかは明瞭(「何を実行するか」だけを分離) 理解容易性への影響は少ない 提案手法の利点を説明します. 表明をアスペクトとして記述することにより,アプリケーションに応じた表明の追加が可能となります.これは,安全性の向上につながります.また,アスペクトはオブジェクトとは分離して管理できるため,クラスだけを再利用して新しい制約を別途作成しなおすことで,再利用性の阻害を抑えられます. また,cflow 述語の導入により,特定のメソッドが実行中である,といったコンテキストに応じた表明の記述が可能となります. 一方で,表明の文そのものはプログラム文として記述していることから,いつどのような判定処理が実行されるか,といった,理解容易性への影響は少なくなっています.
研究の現状 提案した言語要素に対するプリプロセッサの開発中 現在の課題 提案言語 AspectJ コード生成 表明の持つ副作用 現在,この提案した言語要素に対するプリプロセッサの開発を行っています. 提案言語から,AspectJ の,先ほどお見せしたようなコード記述を生成するようなプリプロセッサとなる予定です. 現在の課題として,表明に用いる関数の持つ副作用の問題があります.これについて,今から説明します.
副作用の問題 表明が副作用を持つと,理解容易性が低下する 例: 配列がソート済みでないならソートする ASSERT isSorted(Array array) { if (! testSorted(array)) Arrays.sort(array); return true; } 利用者側のコード例: array = getArray_Not_Sorted(); // 整列していない配列を受け取る assert ( isSorted ( array )); // ここで停止するはず doSomethingUsingSortedArray( array ); 表明関数自体が副作用を持つ場合以外にも, 他のアスペクトの作用で副作用が発生する場合もある C++ における const のような「副作用がない」ことを言明する記述を用いた安全性の向上が重要 表明が用いる関数が副作用を持つかどうか,というのは,プログラムの理解容易性に関わる問題だと考えられます. たとえば,この例では,isSorted という表明の実装として,配列がソート済みである場合はそのまま true を返し,ソートされていない場合はソートしてから true を返す,という処理を行っています. このような記述を行ってしまうと,利用者側のコードにおいて,たとえばソートされていない配列を用意して,それに対してソートしているかどうかを判定する表明を宣言した場合,一般的にはここで停止する,と思われるにも関わらず,実際には配列をソートしてから先へ進んでしまう,ということが発生します. この問題に対しては,C++言語におけるconst 宣言のような,その関数に副作用がないことを開発者が言明し,副作用がない関数からは副作用がない関数しか呼べないようにする,といった安全性の向上が重要となってくる,と考えています.
まとめと今後の課題 表明の利点と弱点 アスペクトを用いた表明の記述 今後の課題 故障の早期検出,責任分担 表明が記述できる範囲には制限がある 横断的な表明の記述が可能になる アプリケーション依存の制約を分離できる 今後の課題 提案する言語要素の洗練 プリプロセッサ形式による言語の実装 適用実験 最後にまとめます.表明の利点と弱点として,表明が故障の早期検出や責任分担として役立つこと,そして表明が記述できる範囲には制限がある,ということを説明しました. これに対して,アスペクトを用いた表明の記述を提案し,横断的な表明をアスペクトとして記述すること,また,アプリケーションに依存した制約などもアスペクトとしてオブジェクトから分離することで再利用に適した形での汎用化が可能であると考えられます. 今後,プリプロセッサ形式として提案した言語の実装を行い,従来の表明などを置き換えることで,ソフトウェアの複雑さや,コンポーネント単位での再利用性などがどのように変化するか,といった適用実験を行っていきたいと考えています.
既存手法との差異 “判定用関数” との違い AspectJ による実装との違い 後から表明を追加可能 cflow による違い アドバイスは任意の位置に書けるわけではない 複数ある場合は(自動で) AND をとる → 開発者が記述する手間を省略
例題 表計算プログラム Sheet は Cell の集合を管理 Expression は Sheet 上の他の Cell を参照する has-a Sheet Cell Empty Expression IntValue Count Sum
例題に対する表明 Expression が参照している Sheet は,Expression 自身を含んでいる. 注:この例は櫻井さんの研究と競合? Sheet Expression 参照範囲を要求
Observer/Subject による表明検査 FileIsValidAssertion オブジェクト 検査関数を実装したものを add していく
表明の記述例 提案手法による記述例 ASSERT FileIsValid(File f) { return ! cflow ( DB.writeFile() ) || f.canWrite(); } AspectJ で記述した例 pointcut FileIsValid_for_WriteFile (File f): cflow(DB.writeFile()) && args(f) && execution(boolean AssertionAspect.FileIsValid(File)); Object around FileIsValid (File f): FileIsValid_for_WriteFile(f) { boolean b = (Boolean) proceed(f); return f.canWrite() && b; 今述べた cflow の記述を,実際に AspectJ で記述しようとすると,このようになります. 防ぎたい実行時点を選び,フラグ変数を導入し,実際の判定処理を追加する必要があり,このような場面が複数出現した場合は,さらに手間を支払うことになります.