はじめに
本稿では、モデリングとコード生成を「Ruby流」にサポートするRGenフレームワーク [1] を紹介します。私は、MDA/MDD [2] という意味で「モデリング」という用語を使用します(しかし、そうは言うものの、このアプローチに非常に厳密に従うわけではありません)。モデルはメタモデルのインスタンスであり、それは、言いかえればドメイン特化言語(DSL)と(密接に関わって)います。モデルの変換は、別のメタモデルのインスタンスにモデルを変換し、コード生成は、モデルをテキストの出力に変換する特殊な変換として利用されます。
RGenは、アプリケーションと非常によく似たスコープを持つJavaフレームワークであるopenArchitectureWare (oAW) [3] に強くインスパイアされています。RGenの背後にある主要なアイデアは、フレームワークの範囲内でアプリケーションロジックの実装として利用するだけでなく、メタモデルの定義、モデル変換、コード生成として利用するところにあります。RGenは、先に述べた側面のそれぞれに対して内部DSLを提供することにより、それらを促進します。いくつかのプロジェクトで示されるとおり、Rubyはこのアプローチにとても適しています。良く知られている例は、さまざまなRubyの内部DSLを含んだRuby on Rails [4] です。それとは対照的に、oAWはモデル変換とコード生成の定義のために外部DSLを使用します。
経験上、RGenアプローチは非常に軽量かつ柔軟で、それにより効率的な開発と単純な配備を可能にします。私は、コンサルティングをしているプロジェクトでこれが特に有用であることが分かりました。そこでは、ツールのサポートが不足しているもののツール開発が計画されいませんでした。RubyとRGenを使用することで、作業を進めるのに最小限の労力で必要なツール作成に着手しました。そして、そのツールを開発することで大きなメリットが得られるだろうということを人々に納得させました。
RGen やoAWのようなフレームワークのための典型的なアプリケーションは、コードジェネレーター(たとえば、組み込みデバイスのためのもの)やモデルを構築・操作するためのツールであり、通常はXML、独自テキスト、グラフィカル言語といったもので表わされます。本稿では、私は「UMLステートチャートから C++コードジェネレーター」を実例に使用します。現実に、私たちは自動車組み込みの電子制御ユニット(ECU)のためのモデル構築とコード生成のために、先に述べたプロジェクトで今なおRGenを使用しています。
モデルとメタモデル
モデリングフレームワークで最も基本的な側面は、モデルとメタモデルを表現する能力です。メタモデルは、特定の目的に対してモデルの見える様を記述します。それ故、ドメイン特化言語の抽象的な構文で定義します。一般的に、モデリングフレームワークのアプリケーションは、いくつかのメタモデルとそれらの間のモデル変換の利用を含みます。
metamodel は特定の目的のモデルが何のように見えるかもしれないかについて述べます。このように、領域に特有の言語の抽象構文を定めます。一般的に、モデルフレームワークのアプリケーションは、いくつかのmetamodelsの使用と彼ら間の典型的な変換を含みます。
RGen では、オブジェクト指向言語としてのRubyのモデルとメタモデルについて単純な表現を採用しています。オブジェクトはモデル要素を表現するのに使用され、クラスはメタモデルとして使用されます。モデル、メタモデルとそれらのRuby表現の関係は、図1で示されます。
ドメイン特化言語をサポートするために、モデリングフレームワークはカスタムメタモデルを可能にしなければなりません。RGenでは、メタモデル定義言語を提供することによりこれを促進しています。それは、(モデリングのドメインに対して)それ自身をドメイン特化言語として見ることができます。他のDSLについては、メタモデル定義言語の抽象的な構文がそのメタモデルによって定義されます。この場合は、メタメタモデルとみなされます。
図1: モデルとメタモデル、そしてそのRuby表現
メタモデルとは異なり、メタメタモデルはRGenで不変です。そのフレームワークには、Eclipse Modelling Framework(EMF)[5] のメタメタモデルであるECoreを使用します。図2はECoreメタモデルを単純化したものを表しています。ECoreでは、メタモデルは基本的にクラスで構成され、それは階層的なパッケージでまとめられ、属性や他のクラスへの参照を持っています。あるクラスは複数から派生するかもしれません。参照は一方向ですが、それらを双方向にするために、反対の参照と関連がある場合もあります。参照の対象はその型で、それはクラスでなければなりません。つまり、その対象のロールが、参照の名前です。属性は(クラスでない)データ型のインスタンスで、それはプリミティブ型か列挙型です。
図2: ECoreメタモデルの単純化したビュー
oAWのようなその他のフレームワークとは対照的に、RGenのメタモデル定義言語の具体的な構文はごく普通のRubyで、それを内部DSLとします。リスト1は、単純なステートマシンのメタモデルを表しています。そのコードは、モジュール1 、メタモデルパッケージを表わしているいくつかのクラス、個々のメタモデルクラスを定義するために通常のRubyのキーワードを使用します。通常の Rubyクラスとモジュールからこれらの要素を区別するために、いくつかの追加コードが必要となります。モジュールは、特別なRGen拡張モジュール (1) によって拡張され、クラスは、RGenメタモデルベースクラスの MMBase (2) から派生します。
メタモデルクラスのスーパークラスの関係は、Rubyクラスのインターフェース(3) によって表わされます。留意すべき点として、Rubyはもともと多重継承をサポートしていませんが、その柔軟性によって、特別なRGen命令によってその機能を提供することが可能です。このケースでは、個々のクラスはMMMultiple(
# リスト 1: ステートマシンメタモデルの例 module StatemachineMetamodel extend RGen::MetamodelBuilder::ModuleExtension # (1) class ModelElement < RGen::MetamodelBuilder::MMBase # (2) has_attr 'name', String end class Statemachine < ModelElement; end # (3) class State < ModelElement; end class SimpleState < State; end class CompositeState < State; end class Transition < ModelElement has_attr 'trigger', String # (4) has_attr 'action', String end Statemachine.contains_one_uni 'topState', State # (5) Statemachine.contains_many_uni 'transitions', Transition CompositeState.contains_many 'subStates', State, 'container' CompositeState.has_one 'initState', State State.one_to_many 'outgoingTransitions', Transition, 'sourceState' State.one_to_many 'incomingTransitions', Transition, 'targetState' end
メタモデルの属性と参照は、 MMBaseによって提供される特定のクラスメソッドを使用して指定されます。対応するコードが評価されるとRubyのクラス定義が解釈され、それが可能となります。クラス定義のスコープ内で、現在のオブジェクトはそのクラスのオブジェクトであり、クラスのオブジェクトが持つメソッドを直接呼び出すことができます2。
has_attrメソッドは、アトリビュートを定義するために使用されます。それは、アトリビュートの名前とプリミティブなRubyデータタイプを引数にとります3 (4)。RGenは、内部でRubyの型とECoreのプリミティブ型との対応づけをおこない、この場合は、EStringに対応づけます。参照を指定す るために、いくつかのメソッドが利用可能です。 contains_one_uniやcontains_many_uniは、一つあるいは複数の対象クラス のそれぞれに対して、一方向だけの関係を定義します。contains_oneやcontains_manyは、双方向です。これらのメソッドはもとのク ラスのオブジェクトから呼ばれ、第一引数に対象ロール、それに続き、対象クラス、双方向の場合はもとのロールをとります(5)。注意すべき点として、対象 クラスはそれを参照できるようにする前に定義する必要があります。ほとんどのRGenメタモデルでそうしなければならないので、参照はクラス定義の外と後 で定義されます。
モデルが実際に作成されると、メタモデルのRubyクラスがインスタンス化されます。アトリビュートの値と参照する対象は、それぞれのオブジェクトのインスタンス変数として格納されます。Rubyはインスタンス変数への直接アクセスを許していないので、アクセサメソッドが使用されます。Ruby流では、 getterメソッドは対応する変数と同等の名称で、setterメソッドは同じ名称の後ろに“=”を付けたものとなります。そのsetterメソッドを左に書いて使用します。
上述のクラスメソッドは、メタプログラミングを用いて動的に必要なアクセサメソッドを構築します。インスタンス変数への単純なアクセスに加えて、これらのメソッドが引数の型をチェックし、違反していた場合は例外を投げます。この種の実行時の型チェックはRuby上で構築されるので、Rubyではもともとこの種のチェックをおこないません4 。to-many参照の場合、getterはarrayを返し、参照したオブジェクトの追加や削除のためにsetterメソッドがあります。双方向の参照の場合は、アクセサメソッドが関係の先にある対応する参照を自動的に追加したり削除します。
リスト 2は、例を示しています。通常のRubyクラスのインスタンス化メカニズム(1) は、アトリビュートと参照を適切にセットするための特別なコンストラクタ(4) だけでなく、オブジェクトを生成するのにも使用されます。ここで注意すべき点は、パッケージ名はそのクラス名を適切なものとするために必要だということです。現在の名前空間にパッケージモジュールをインクルードすることで、繰り返しを避けることができます(3)。State s1のname アトリビュートに値がセットされ、getterメソッドの結果がチェックされます(2)。出力変換がs1 に追加され(to-many)、後方参照の自動生成(to-one)がチェックされます(5)。変換の対象の状態が、明確に値を割り当てられ(to-one)、s2 にセットされます。そして、配列の結果が要素t1を含んでいるか(to-many)がアサートされます(6)。第2の対象の状態が生成され、別の変換を使用しているもとの状態と関連を持ちます。最終的に、s1のすべての出力変換の対象とする状態が、s2とs3 でアサートされます(7)。注意すべき点は、targetState メソッドが配列であるoutgoingTransitionsの結果で呼ばれることです。RGenは、要素を含んだ未知のメソッドの呼び出しを中継し出力を単一セットにまとめる機能によって、RubyのArrayを拡張しているので、こういった簡潔な表記が可能となります。
# リスト 2: ステートマシンメタモデルのインスタンス化の例
s1 = StatemachineMetamodel::State.new # (1) s1.name = 'SourceState' # (2) assert_equal 'SourceState', s1.name include StatemachineMetamodel # (3) s2 = State.new(:name => 'TargetState1') # (4) t1 = Transition.new s1.addOutgoingTransitions(t1) # (5) assert_equal s1, t1.sourceState t1.targetState = s2 # (6) assert_equal [t1], s2.incomingTransitions s3 = State.new(:name => 'TargetState2') t2 = Transition.new(:sourceState => s1, :targetState => s3) # (7) assert_equal [s2,s3], s1.outgoingTransitions.targetState
上記で示すように、RGenのメタモデル定義言語は、Rubyでのメタモデルを表すのに必要とされるモジュール、クラス、メソッドを作成します。さらにその上で、メタモデルそれ自身は、通常のRGenモデルとして利用可能です。RGenが、自身のメタモデル定義言語を使用して表現されたECoreメタモデルのあるバージョンを含んでいるので、これが可能となります。メタモデルのRGenモデルは、Rubyクラスのecoreメソッドかメタモデル要素で表現されるモジュールを使用してアクセス可能です。
リスト3は、例を示しています。 EPackage のインスタンスとなるパッケージを表しているStatemachineMetamodelモジュールのecoreメソッドを呼び出し(1)、EClass のインスタンスとなるStateクラスのecoreメソッドを呼び出します(2)。両方で注意すべき点は、EPackageとEClassは同じモデルに属していることです。実際、「State」と名付けられたEClassは、「StatemachineMetamodel」と名付けられた EPackageの分類子の1つです(3)。メタモデルのRGenモデルは、単純にすべてのRGenモデルとして扱われます。サンプルコードでは、クラス「State」のスーパークラスが「name」という名称のアトリビュートを持っているかをアサートしています(4)。
# リスト 3: ECoreメタモデルへのアクセス smPackage = StatemachineMetamodel.ecore assert smPackage.is_a?(ECore::EPackage) # (1) assert_equal 'StatemachineMetamodel', smPackage.name stateClass = StatemachineMetamodel::State.ecore assert stateClass.is_a?(ECore::EClass) # (2) assert_equal 'State', stateClass.name assert smPackage.eClassifiers.include?(stateClass) # (3) assert stateClass.eSuperClasses.first.eAttributes.name.include?('name') # (4)
普通のモデルでは、メタモデルのモデルは全ての利用可能なシリアライザ/インスタンシエータによって、シリアライズやインスタンス化がおこなわれます。 RGenはXMIシリアライザやXMIインスタンシエータを内包していて、EMFでメタモデルを変換することができます。同様に、メタモデルのモデルは (たとえば、UMLクラスモデルから、またはモデルへ)RGenモデル変換の元や対象となります。モデル変換は、次のセクションでカバーします。最終的に、メタモデルのモデルは、RGenのメタモデルジェネレータを使用するRGenのメタモデルDSL表現にもどります。図3は、異なるメタモデルの表現やそれらの関係についてまとめています。
図3: RGenメタモデル表現の概要
RGen メタモデルのモデルは、EMFによく似たメタモデル上のリフレクション機能を提供します。手近にリフレクションがあることは、たとえば、カスタムのモデルシリアライザやインスタンシエータを実装する際など、さまざまな状況でプログラマにとって有益なものとなります。メタモデルがECoreであるという事実から、既存の多くのモデリングフレームワークとの交換可能性が保障されます。つまり、RGenメタモデルジェネレータを使用することで、すべての ECoreメタモデルがRGenの中で直接使用可能であるということです。
リスト4は、XMIへのメタモデルのシリアライゼーションを示しています(1)。さらに、RGen DSLの表現を再生成するためのメタモデルジェネレータの使用法を示しています(2)。注意すべき点は、いずれにせよ、メタモデルは StatemachineMetamodelのecoreメソッドによって返されたルートEPackageの要素によって参照されます。そのメタモデルから生成されたDSL表現は、ファイルから読み込まれ、評価されます(3)。オリジナルのクラスやモジュールと再ロードされたものとの間の名前の衝突をさけるために、名前空間として振る舞う、別のモジュールRegenerated のスコープ内で評価されます。「instanceClassName」アトリビュートの価値とは別に、それは再ロードされたバージョンでの追加的な名前空間を含んでいて、両方のモジュールは等価です(4)。
# リスト4: メタモデルのシリアライズ File.open("StatemachineMetamodel.ecore","w") do |f| ser = RGen::Serializer::XMI20Serializer.new ser.serialize(StatemachineMetamodel.ecore) # (1) f.write(ser.result) end include MMGen::MetamodelGenerator outfile = "StatemachineModel_regenerated.rb" generateMetamodel(StatemachineMetamodel.ecore, outfile) # (2) module Regenerated Inside = binding end File.open(outfile) do |f| eval(f.read, Regenerated::Inside) # (3) end include RGen::ModelComparator assert modelEqual?( StatemachineMetamodel.ecore, # (4) Regenerated::StatemachineMetamodel.ecore, ["instanceClassName"])
現時点では、RGenは実行時でのメタモデルの動的な変更に対しては、限られたサポートのみを提供しています。特に、ECoreメタモデルのモデルへの変更は、メモリ内のRubyメタモデルのクラスやモジュールに影響を及ぼしません。しかし、Rubyのメタモデル表現の動的バージョンに関して、作業は進行中です。この動的なメタモデルは、動的クラスやモジュールから成り、EClassやEPackageのECore要素と関連しています。ECore要素が修正されると、動的なクラスやモジュールは、たとえインスタンスが既に存在していたとしてもすぐにその振る舞いを変えます。これは、次のセクションで示すような、モデル変換での高度なテクニックを可能とします。
RGenアプローチの大きな利点の1つは、内部DSLの利用による柔軟性と、Ruby言語との密な結合です。例では、メタモデルクラスとモジュールの作成をプログラムでおこない、アトリビュートや参照を生成するためのクラスメソッドを呼んでいます。これを利用しているアプリケーションは、通常のXMLインスタンシエータです。そして、それは、 XMLタグ、アトリビュートの衝突、これらをメタモデル要素に対応付けているルールセットに直接依存した、対象のメタモデルを直接生成します。RGenのディストリビューションは、それらのインスタンシエータの原型バージョンを含んでいます。
内部のDSLアプローチによって可能となるもう一つの興味深い可能性は、メタモデルを通常のコードに組み込むことです。複雑なコードや内部で結合されたデータ構造を扱う必要があるとき、これは非常に役に立ちます。開発者は、(メタモデル)クラス、アトリビュートや参照に関して構造を考え、影響するホストクラスやモジュールの範囲内でそれらをプログラムします。メタモデルアプローチを使用することによって、実行時に自動的にチェックされたアトリビュートや参照型をもつことを開発者も判断します5。
モデル変換
多様な現実世界では、モデリングアプリケーションはいくつかのメタモデルを使うことでメリットを得ることができます。例として、いくつかの入力/出力の明確なメタモデルだけでなく、アプリケーションがある内部メタモデルを持ちます。図4で示されるように、モデル変換は、あるメタモデルのインスタンスを別のメタモデルのインスタンスへ変換するのに利用します。
図4: モデル変換
上記で説明しているステートマシンの例では、UML 1.3のステートチャートのモデルの入力が、モデル変換を用いて加えられます。RGenは、RGenのメタモデルDSLで表わされたUML 1.3のバージョンのメタモデルを内包しています。それは、XMIインスタンシエータを含んでいて、互換性のあるUMLツールによって格納されたXML ファイルからUMLメタモデルのインスタンスを直接生成することができます。図5では、[6]からstatechart.takenを入力する例を示しています。
図5: UMLのステートチャートの例
新しい対象のモデルを作るのは別として、あるモデル変換で、もとのモデルを代わりに修正するかもしれません。インプレイスなモデル変換では、変換がおこなわれる間、メタモデル要素が変更される可能性があります。RGenは、現時点でこれをサポートしていません。しかし、既に上記で述べたとおり、そういった動的なメタモデルをサポートするための作業が進行中です。
一例として、インプレイスなモデル変換は、下位互換の入力モデルについて以前のバージョンを読むことのできるツールが使用されることもあります。新しいバージョンのツールでの入力のメタモデルのあらゆる変更は、組み込みのインプレイスなモデル変換が付随されています。それらの変換それぞれは、一般的にメタモデルとモデルのほんの少しの変更だけからなります。しかし、それには大量のデータへの適用が必要になるかもしれません。同じソースのモデル上で、一連のインプレイスな変換のすべての操作を使用することで、入力モデルのマイグレーションは非常に効果的となります6。
先に説明したメタモデル定義のDSLと同様に、RGenはモデル変換の定義に内部DSLを提供しています。RGenのモデル変換の仕様は、もとのメタモデルの単一のメタモデルクラスに対する変換ルールで構成されます。そのルールは、対象のアトリビュートや参照を指定するだけでなく、対象のメタモデルクラスも指定します。参照の方は、一般的に適用する変換ルールの結果を組み込みます。
図6は、例として変換ルールからなるアプリケーションとその定義を示しています7。もとのメタモデルクラスAに対するルールは、対象のメタモデルクラスA’を特定し、複数参照のb’ と単一参照の割り当てのc’ を定義しています。b’の対象の値は、もとの参照bによって参照された要素の変換結果で定義されます。これは、メタモデルクラスB(下図参照)の対応するルールが適用されている (b’:=trans(b))ことを意味しています。RGenでは、配列要素の変換結果は、それぞれの要素の変換結果からなる配列となります。同様に、c’の値はcによって参照される要素の変換結果(c’:=trans(c))が割り当てられます。メタモデルクラスCの変換ルールにより、参照のb1’ とb2’ の値をもとの参照b1とb2によって参照される要素の変換結果となるように、順番に指定します (b1’:=trans(b1), b2’:=trans(b2))。メタモデルクラスBの変換は、この例では何の指定も必要ありません。
図6: 変換ルールからなる定義とアプリケーション
内部のDSLとして、モデル変換言語は具体的な構文にプレーンなRubyを使用します。それぞれのモデル変換は、特別なクラスのメソッドを用いて、RGen の Transformerクラスから派生したRubyクラス内で定義されます。最も重要なのは、引数としてもとと対象のメタモデルクラスのオブジェクトをとる、transformクラスのメソッドで変換ルールを定義することです。アトリビュートと参照の割り当ては、実際の対象オブジェクトのアトリビュートと参照の名称で対応づけられた、RubyのHashオブジェクトによって指定されます。そのオブジェクトは、もとのメタモデル要素のコンテキストで評価したtransformメソッド呼び出しと関連づけられたコードブロックによって生成されます。
注意すべき点として、変換ルールは別のルールを再帰的に利用することができます。RGenの変換メカニズムは、個々の変換の結果をキャッシュすることで全体の評価を止めるように注意しています。変換のコードブロックの実行は、再帰的に使用されたすべてのコードブロックが実行される前に、完全に終了します。この確定的なふるまいは、カスタムのコードがコードブロックに追加されたときに、非常に重要なものとなります。
リスト5は、UML 1.3メタモデルから上述したサンプルのステートチャートメタモデルへのモデル変換を示しています。新しいクラスUmlToStatemachineは RGenのTransformerクラスから派生し、対象のメタモデルモジュールは、対象クラスの名前を短くするために現在の名前空間にインクルードしています(1)。通常のRubyのインスタンスメソッド(このサンプルでは、transformという名前)は、変換のエントリポイントとしてふるまいます。それは、transの変換メソッドを呼び出し、入力モデルのすべてのステートマシン要素の変換をトリガーします 8 (2)。transメソッドは、もとのオブジェクトのクラスを始めとして、 transformクラスのメソッドの利用を定義した変換ルールを調べます。そして、ルールが見つからなければ継承関係を辿ります。UML13::StateMachineに対する変換ルールは存在し、それは、先の要素が Statemachineのインスタンスに変換されるように指定します(3)。共に、もとと対象のメタモデルクラスは、通常のRubyクラスのメソッドと Rubyの名前空間のメカニズムが使用されなければならない点に注意してください。transformの呼び出しに付随したコードブロックは、アトリビュート「name」、参照「transitions」と「topState」への値を割り当てた Hashオブジェクトを生成します。その値は、もとのモデル要素でアクセサメソッドを呼ぶによって計算されます。それは、自動的にコードブロックのコンテキストとなります。参照ターゲットの値に対して、 transメソッドは再帰的に呼ばれます。
# リスト5: ステートマシンモデルの変換例 class UmlToStatemachine < RGen::Transformer # (1) include StatemachineMetamodel def transform trans(:class => UML13::StateMachine) # (2) end transform UML13::StateMachine, :to => Statemachine do { :name => name, :transitions => trans(transitions), # (3) :topState => trans(top) } end transform UML13::Transition, :to => Transition do { :sourceState => trans(source), :targetState => trans(target), :trigger => trigger && trigger.name, :action => effect && effect.script.body } end transform UML13::CompositeState, :to => CompositeState do { :name => name, :subStates => trans(subvertex), :initState => trans(subvertex.find { |s| s.incoming.any?{ |t| t.source.is_a?(UML13::Pseudostate) && # (4) t.source.kind == :initial }})} end transform UML13::StateVertex, :to => :stateClass, :if => :transState do # (5) { :name => name, :outgoingTransitions => trans(outgoing), :incomingTransitions => trans(incoming) } end method :stateClass do (@current_object.is_a?(UML13::Pseudostate) && # (6) kind == :shallowHistory)? HistoryState : SimpleState end method :transState do !(@current_object.is_a?(UML13::Pseudostate) && kind == :initial) end end
ほぼすべてのRubyコードは、Hashオブジェクトを生成しているコードブロック内で使用することができるので、非常に強力な割り当てが可能となります。サンプルの対象メタモデル内の複合状態は、初期状態への明確な参照をもっています。それに対して、もとのメタモデルの初期状態は、「初期の」疑似状態からの変換によってマークされます。この変換は、入力の変換を持つサブ状態を見つけることで、Rubyに組み込まれているArrayメソッドを使用して実現されます。そして、transメソッドを使用してそれを変換します(4)。
対象クラスのオブジェクトの代わりに、transformメソッドはオプションでメソッドを受けることができ、対象クラスのオブジェクトを計算します。サンプルでは、UML13::StateVertexは、stateClass メソッドの結果によって、SimpleStateかHistoryStateかのどちらかに変換されます(5)。さらに追加のメソッド引数によって、ルールに条件をつけることができます。サンプルでは、そのルールは初期の疑似状態に対して適用されず、他のすべてのルールが適用されないため結果はnilとなります。通常のRubyメソッドに加えて、Transformerクラスは、その実態は、現在の変換のもとのオブジェクトのコンテキストを評価する、ある種のメソッドの定義を可能とするメソッドを提供します(6)。あいまいな場合は、現在の変換のもとのオブジェクトは @current_objectインスタンスの値を使用してアクセスすることもできます。
transform クラスのメソッドの呼び出しは通常のコードですが、変換定義の「スクリプト」を許可することで、より洗練された方法で呼び出すこともできます。リスト6で示されるTransformerクラス自身のcopy クラスメソッドの実装が良い例です。そのメソッドは、それらが同じであるか同じアトリビュートと参照をもっているかを前提として、もとのメタモデルとオプションで対象のメタモデルのクラスをとります。そして、メタモデルのリフレクションによってアトリビュートと参照を調べることで、与えられたすべてのもとのオブジェクトに対して割り当てたハッシュを自動的に生成するコードブロックで、 transformクラスメソッドを呼び出します9。
# リスト6: コピーコマンド変換器の実装 def self.copy(from, to=nil) transform(from, :to => to || from) do Hash[*@current_object.class.ecore.eAllStructuralFeatures.inject([]) {|l,a| l + [a.name.to_sym, trans(@current_object.send(a.name))] }] end end
copy クラスメソッドは、メタモデルのすべてのクラスに適用することもできます。これは、与えられたメタモデルに対するコピー変換を作るための一般的な方法であり、そのメタモデルのインスタンスに対するディープコピー(複製)をつくるのに利用することができます。リスト7は、UML 1.3のメタモデルに対するコピー変換器の例を示しています。
# リスト7: UML 1.3メタモデルのコピー変換器の例 class UML13CopyTransformer < RGen::Transformer include UML13 def transform trans(:class => UML13::Package) end UML13.ecore.eClassifiers.each do |c| copy c.instanceClass end end
変換器のメカニズムに関するもう1つの興味深い利用は、上述したメタモデルのリフレクションの実装です。ecoreメソッドが呼ばれると、受け側のクラスかモジュールは、組み込みの変換器にかけられます。そして、それはアトリビュートと参照の変換に対するルールを適用します。そのメカニズムは、「入力のメタモデル」として、メタモデルクラスの利用だけでなくプレーンなRubyクラスとして利用するのに十分な柔軟性を持っているので、それが可能となります。
コード生成
モデルの変換と変更とは別に、コード生成は、RGenフレームワークのもう一つの重要な活用法です。コード生成は、モデルからテキストベースのアウトプットへの特殊な変換とみなすことができます。
RGen フレームワークは、oAWのそれと非常に似ているテンプレートベースのジェネレーターメカニズムをもっています。RGenとoAWのソリューションは、両方とも、テンプレートの関係、テンプレートファイル、出力ファイルの点で、他のテンプレートベースのアプローチとは異なります。テンプレートは複数のテンプレートを含んでいて、複数の出力ファイルを生成し、出力ファイルの内容は複数のテンプレートによって生成されます。
図 7は、例を表わしています。「fileA.tpl」ファイルの中には、2つのテンプレート「tplA1」と「tplA2」が定義されていて、「fileC.tpl」ファイルの中には、テンプレート「tplC1」が定義されています(キーワードdefine)。テンプレート「tplA1」は出力ファイル「out.txt」を生成し(キーワードfile)、それにテキスト行を生成します。さらに、「tplA2」と「tplC1」の内容を同じ出力ファイルに展開します(キーワードexpand)。テンプレート「tplC1」は別のファイルにあるので、その名前をテンプレートファイルの相対パスで追加しなければなりません。
図7: RGenジェネレーターのテンプレート
RGen テンプレートが展開されると、それらの内容はコンテキストモデル要素のコンテキスト内で評価されます。すべてのテンプレートは、 :forアトリビュートを使用して、定義時点でメタモデルクラスと関連付けられ、該当タイプのコンテンツ要素に対してのみ展開されます。デフォルトでは、expandコマンドは現在のコンテキスト内でテンプレートを展開しますが、:for か :foreach アトリビュートを使用することで別のテンプレートを指定することもできます。後者の場合、配列が作られ、その配列のすべての要素に対してテンプレートが展開されます。テンプレートは別のコンテキストタイプを指定することでオーバーロードされ、expandが、自動的に与えられたコンテキスト要素に対して適切なテンプレートを選択します。
RGen テンプレートのメカニズムは、ERB(組み込みのRuby)上に構築されていて、Rubyに対して基本的なテンプレートのサポートを提供しています。 ERBは、標準的なRubyディストリビューションの一部で、<%, <%= and %>タグを使用して任意のテキストにRubyコードを埋め込むことができます。RGenのテンプレート言語は、ERB構文に通常のRubyメソッドとして実装された追加キーワードを加えたもので構成されます。そうすることで、テンプレート言語はRGen内で別の内部DSLを作ります。標準的なERB メカニズムで構築することで、その実装はとても軽量なものとなります。
コード生成における大きな問題の一つは、出力のフォーマットです。テンプレート自体が開発者に読みやすいものでなければならないので、後でアウトプットの読みやすさを妨げる付加的なスペースが追加されます。いくつかのアプローチは、きれいなプリンターに出力することでこの問題に対処することができます。しかし、それはより時間がかかり、特定種類の出力に利用できないかもしれない追加的なツールを必要とします。
RGen のテンプレート言語は、追加のツールを必要とせずに出力をフォーマットするための単純な手段を提供します。デフォルトでは、開始行と空行のスペースは除去されます。開発者は、インデントを制御して、明確なRGenコマンドによって空行を生成します。iinc や idecは現在のインデントレベルを設定するのに使用され、nl は空行を挿入するのに使用されます。フォーマットコマンドを追加するための苦労は許容範囲であることは、経験的に知っています。特に、特殊な出力フォーマットが必要な場合、このアプローチは非常に有効です。
リスト8は、ステートチャートの例からのコードテンプレートを示しています。それは、すべての複合状態に対して生成されたC++の抽象クラスのヘッダーファイルを生成するのに使用されます。ステートパターンと[6]に従い、ある状態クラスは全てのサブ状態クラスから派生されます。
# リスト8: ステートマシンのジェネレーターのテンプレート例 <% define 'Header', :for => CompositeState do %> # (1) <% file abstractSubstateClassName+".h" do %> <% expand '/Util::IfdefHeader', abstractSubstateClassName %> # (2) class <%= stateClassName %>; <%nl%> class <%= abstractSubstateClassName %> # (3) { public:<%iinc%> # (4) <%=abstractSubstateClassName%>(<%=stateClassName%> &cont, char* name); virtual ~<%= abstractSubstateClassName %>() {}; <%nl%> <%= stateClassName %> &getContext() {<%iinc%> return fContext;<%idec%> } <%nl%> char *getName() { return fName; }; <%nl%> virtual void entryAction() {}; virtual void exitAction() {}; <%nl%> <% for t in (outgoingTransitions + allSubstateTransitions).trigger %> # (5) virtual void <%= t %>() {}; <% end %> <%nl%><%idec%> private:<%iinc%> char* fName; <%= stateClassName %> &fContext;<%idec%> }; <% expand '/Util::IfdefFooter', abstractSubstateClassName %> <% end %> <% end %>
テンプレートは、その名前とコンテキストメタモデルクラスを定義し、新しい出力ファイルを指定することから始まります(1)。すべてのC/C++ヘッダーファイルは、重複のインクルードを避ける必要があり、それは、通常、すべて大文字のファイル名を含みます。テンプレート「IfdefHeader」は、与えられたファイル名のガードを提供します(2)。次の行では、クラスの定義が行われ(3)、「public」キーワードの後のインデントレベルがインクリメントされます(4)。いくつかの基盤となるメソッドに加え、すべてのサブ状態の出力変換に対する仮想メソッドが宣言されなければなりません。それは、関連する変換すべてに対し、単純なRubyのfor-loopの繰り返しによって実現されます(5)。for-loopの内部では、メソッド宣言が作成されます。アウトプットにトリガーアトリビュートの値を書くために、<% の代わりに<%= が使用されます。テンプレートの最後は、重複インクルードのガードに関するフッター部が追加されます。
一般に、逐語的にコピーされないすべてのテンプレート出力は、モデルによって表わされた情報から生成されます。コンテキストモデル要素のアトリビュートに対するアクセサメソッドは、直接読むことができ、他のモデル要素は参照のアクセサメソッドによって操作されます。この場合も、モデル要素の配列でメソッドを呼び出すことができることが、非常に有益なものとなります。
しかし、多くの場合、アウトプットを生成する前に、モデルからの情報が処理される必要があります。計算がより複雑であるか、異なるロケーションで使用される場合、それは、通常、別々のメソッドとして実装されるのが望まれます。上記の例では、stateClassName、 abstractSubstateClassName、allSubstateTransitionsは継承された属性/参照で、メタモデルクラスCompositeStateのメソッドとして利用可能です。
それらの継承した属性や参照は、メタモデルクラスの通常のRubyクラスとして実装されます。しかし、RGenはメタモデルクラスの多重継承をサポートしているので、特別な注意が払われなければなりません。ユーザー定義メソッドは、特別な「クラスモジュール」としてのみ追加され、それはすべてのRGenメタモデルクラスの一部であり、一定のClassModuleによってアクセスされます。
# リスト9: ステートマシンメタモデルの拡張例 require 'rgen/name_helper' module StatemachineMetamodel include RGen::NameHelper # (1) module CompositeState::ClassModule # (2) def stateClassName container ? firstToUpper(name)+"State" : firstToUpper(name) # (3) end def abstractSubstateClassName "Abstract"+firstToUpper(name)+"Substate" end def realSubStates subStates.reject{|s| s.is_a?(HistoryState)} end def allSubstateTransitions realSubStates.outgoingTransitions + # (4) realSubStates.allSubstateTransitions end end end
リスト9は、サンプルに関する派生した属性と参照の実装を示しています。初めに、StatemachineMetamodelパッケージモジュールがオープンされ、helperモジュールがミックスインされます(1)。パッケージモジュール内では、CompositeStateクラスのクラスモジュールがオープンされます(2)。そのメソッド実装は、通常のアクセサメソッドを使用しているクラスモジュール、別の派生属性や参照、あるいはミックスインモジュールのメソッド内でおこなわれます(3)。allSubstateTransitionsのメソッド実装は、配列で要素のメソッドを呼び出すことのできるRGenの機能から帰納的で強力な影響を受けています(4)。
注意すべき点として、リスト9のメソッドは、オリジナルのメタモデルクラスとして同じファイルで定義されていないことです。その代りに、Rubyの「オープンクラス」機能により、異なるファイルからのメソッドを提供しています。同じファイル内にメソッドを入れることは可能ですが、多くの場合、1つ以上のメタモデル拡張ファイルを利用するのが望ましいです。こうすることで、メタモデルが特定の目的だけにしばしば役立つヘルパーメソッドで「散らかされる」ことがありません。
サンプルでは、拡張ファイルは「statemachine_metamodel_ext.rb」という名前で、拡張メソッドはコード生成のために使用されます。実際はプロジェクトの規模に依存しますが、通常の拡張のために「statemachine_metamodel_ext.rb」を持ち、ジェネレータの特定の拡張のために「statemachine_metamodel_gen_ext.rb」を持つことは有効です。テンプレートファイルとして第2のファイルを保持することで、ジェネレータロジックを明確に分離することができます。
コード生成を始めるために、テンプレートファイルがロードされ、ルートテンプレートが拡張される必要があります。リスト10では、それがステートチャートのサンプル内でどのようにおこなわれるのかを示しています。初めに、DirectoryTemplateContainerのインスタンスが作られます (1)。コンテナは出力ディレクトリを知る必要があります。つまり、出力ファイルが作成されるディレクトリです(キーワードfileを使用します)。さらに、テンプレート内でメタモデルクラスへの参照のための名前空間として利用されるメタモデルを知る必要もあります。次に、テンプレートは指定したテンプレートディレクトリにロードされます(2)。サンプルでは、コード生成のために使用されるモデル要素は、先のモデル変換によってRGen環境に投入されます。ルートコンテキスト要素(つまり、メタモデルクラスStatemachineのインスタンス)は、その環境から取り出され(3)、コード生成はそれぞれに対するルートテンプレートを拡張することによって始まります(4)。
# リスト10: ジェネレータを開始する outdir = File.dirname(__FILE__)+"/../src" templatedir = File.dirname(__FILE__)+"/templates" tc = RGen::TemplateLanguage::DirectoryTemplateContainer.new( # (1) StatemachineMetamodel, outdir) tc.load(templatedir) # (2) stateMachine = envSM.find(:class => StatemachineMetamodel::Statemachine) # (3) tc.expand('Root::Root', :foreach => stateMachine) # (4)
リスト11は、最終的なジェネレータの出力部分を示しています。ファイル「AbstractOperatingSubstate.h」は、サンプルモデルの「動作している」状態として、リスト8で示されるテンプレートによって生成されます。この結果を成し遂げるために必要な追加の事後処理は何もないことに注意してください。
// リスト11: C++出力ファイルのサンプル "AbstractOperatingSubstate.h" #ifndef ABSTRACTOPERATINGSUBSTATE_H_ #define ABSTRACTOPERATINGSUBSTATE_H_ class OperatingState; class AbstractOperatingSubstate { public: AbstractOperatingSubstate(OperatingState &context, char* name); virtual ~AbstractOperatingSubstate() {}; OperatingState &getContext() { return fContext; } char *getName() { return fName; }; virtual void entryAction() {}; virtual void exitAction() {}; virtual void powerBut() {}; virtual void modeBut() {}; private: char* fName; OperatingState &fContext; }; #endif /* ABSTRACTOPERATINGSUBSTATE_H_ */
アプリケーション注釈
Ruby は、プログラマーが単純で簡潔な方向で自身の考えを表現することのできる言語です。それ故、十分に保守性のあるソフトウェアの効果的な開発を可能にします。RGenは、モデリングとコード生成のサポートをRubyに加えます。そして、開発者はRuby流の方法で、同じように単純に、モデルやコード生成を扱うことができます。
単純さの原則に従い、RGenフレームワークは、開発者に対して、サイズ、依存、規則という点で軽量です。単に、大きなアプリケーションだけでなく、日常のスクリプトのためのフレームワークとして利用することもでき、これにより大きな柔軟性が得られます。
動的でインタープリタ言語であるRubyは、開発の中で何らかの特有な差異をもたらします。最も突出したもののうちの1つは、型チェックに関するコンパイラサポートの欠如です。もう1つは、自動補完のようなエディタサポートが、一般的に最小レベルでの利用のみとなることです。Rubyの言語機能に完全に依存しているので、このことはRGenにも同様に当てはまります。特に、外部DSLを使用するoAWのようなフレームワークは、より優れたエディタサポートを提供します。
Python やSmalltalkのような別の動的型付け言語と共に、Ruby開発者はこれらのデメリットと非常にうまく協調しています。コンパイラチェックの欠如は、通常、より徹底して(単体)テストをすることで補完しています。それは、いずれにせよ良い戦略です。エディターサポートの欠如は、優れたエディタのサポートを最優先で作ることが困難であることよりも、これらの動的言語の機能を使用することで得られるメリットの方が部分的に存在しています。
一方で、特により大きなプロジェクトでは、これらの特徴による力が利用されなければなりません。これは、(実行時の)チェックを加えることでプログラマがおこなわなければなりません。しかし、言語自体は、プロジェクト特有のチェックDSLを定義することによって、このタスクをサポートします。
RGen のメタモデル定義言語は、そのようなDSLとみなすことができます。実行時にチェックされるアトリビュートや参照の型を定義するのに利用することができます。これは、RGenが巨大なRubyアプリケーションのスケルトンとして提供できることを意味しています。また、特に、ECoreやUMLに準拠した図を使用する、あるいは、グラフ視覚化ツールを使用する場合に、メタモデルは、開発者の共通理解を支援します。RGenによって追加された型チェックは、プログラム、モデルの核となるデータが一貫した状態であることを確かなものとするための第一歩となります。
RGen は、二年前にモデリングドメインとRuby言語をつなぐ実験として始まりました。導入で説明したように、それは自動車産業内でのコンサルティングプロジェクトでのプロトタイプツールとしてうまく利用されました。現在では、このプロトタイプツールが成熟し、自動車の電子制御部品(ECU)の通常開発に利用されています。
このツールの範囲内で、いくつかのメタモデルは、最大600程度のメタモデルクラスが使用されています。たとえば、私たちのモデルの一つは、約70000の要素から成り、それらがロードされ、変換され、最終的に、約90000行の出力コードが1分以内に変換されます。このドメイン特価なRGenベースのアプローチは、実行時やメモリ使用の観点の両方から、他のJavaやC# ベースのアプローチに難なく匹敵します。さらなるメリットとして、そのツールは、Rubyインタプリタを含んで2.5MBの実行形式で提供され、インストールせずにホストシステムで実行することができます10。
RGen 自体はRubyの内部DSLをベースにしていますが、新しいRubyの内部DSLの具体的な構文を作成する際のプログラマへの支援はまだです。パーサーや文法を生成するような外部DSLの具体的な構文に対するサポートもまだです。今のところ、カスタムのメタモデルのインスタンス(抽象的な構文)は、本稿で示したようにプログラムで作成するか、既存かカスタムのインスタンシエータによって作成する必要があります。この話題は将来の改善としてのテーマとなっています。
本稿で使用したサンプルアプリケーションの完成なソースコードは、RGenプロジェクトページ[1]から参照できます。
まとめ
Ruby をベースとしたRGenフレームワークは、モデルやメタモデルの処理、モデル変換の定義、テキストベースのアウトプットの生成をサポートしています。それが内部DSLを使用するものとして、Ruby言語と密に結合しています。Rubyの設計の原則に従い、それは軽量で柔軟性があり、簡潔で保守性のあるコードを書くための手段を提供することで、効果的な開発を提供します。
RGen は、単純なプロトタイプから発展したモデリングとコード生成のツールのための基盤として、自動車業界のプロジェクトでうまく利用されています。コンパイラチェックやエディタサポートが不足しているとして、Rubyに似た言語としてしばしば語られる不利な点をよそに、そのアプローチが有効であることを経験的に知っています。また、Rubyがインタプリタ言語であるにも関わらず、実行時やメモリ使用に関しての高パフォーマンスが達成されています。
RGenの生産的な利用の他に、そのフレームワークは今なお実験のために使用されています。現在開発中のある拡張では、インスタンスが既に存在する間、実行時に変更可能な動的メタモデルをサポートしています。
参考
[1] RGen http://ruby-gen.org (リンク)
[2] モデル駆動アーキテクチャ http://www.omg.org/mda (リンク)
[3] openArchitectureWare http://www.openarchitectureware.org (リンク)
[4] Ruby on Rails http://www.rubyonrails.org (リンク)
[5] Eclipse Modelling Framework http://www.eclipse.org/modeling/emf (リンク)
[6] Iftikhar Azim Niaz氏と田中 二郎氏による 「UMLのステートチャート図からのコード生成」。 (2003年、筑波大学 システム情報工学研究科)
[7] rubyscript2exe http://www.erikveen.dds.nl/rubyscript2exe (リンク)
1 ルビーのモジュールは名前空間として、そして、グループ化方法のために一般的に用いられます。そして、クラスにmix-insを許します。
2 これは、RubyでDSLをインプリメントするためのいくつかの方法の中の1つであり、Ruby on Railsでも使用されます。Railsアプリケーションでは、ActiveRecord::Baseのサブクラスは、RGenと似たような方法でメタモデルの定義に使用されます。ActiveRecordとは対照的に、RGenのメタモデルはデータベースと関連がなく、メタメタモデルとしてECoreを使用します。
3 Rubyでは、ほぼすべてのものがオブジェクトであるとして、クラスとデータタイプの間の違いはありません。String、Integer、Floatのようなクラスは、プリミティブ型として機能します。
4 Rubyはダックタイピングが可能です。それは、あるオブジェクトのタイプではなく、そのメソッドが特定の目的に対して正しいオブジェクトか間違ったオブジェクトになることを意味しています。
5 Rubyのような動的型づけ言語では、型チェックもまた重要です。しかし、静的型付け言語とは対照的に、どのチェックが必要か否かを開発者が判断します。
6 実際、Ruby on Railsのデータベースマイグレーションは、ほぼ同じことをします。それぞれのマイグレーションは、スキーマを変更するだけでなく、データベースの内容を修正します。一連のマイグレーションのステップは、データベースの内容にある特定のスキーマ(またはメタモデル)のバージョンを適用します。
7 例を単純化するために、ソースと対象のモデル/メタモデルの構造は同一とし、クラスと参照ロール名だけが違っています。典型的なアプリケーションでは、それが必ずしも必要というわけではありません。
8 transformer のインスタンスが生成されると、コンストラクタが、それぞれのモデルのすべての要素の配列となっているRGenのEnvironmentオブジェクトによって表わされた元と対象のモデルへの参照を取得します。
9 injectt配列のメソッドは、すべてのアトリビュートと参照とともに配列で呼び出されます(ECore の機能)。それは、直前の呼び出しのブロックの結果と共にブロックにそれぞれの要素を渡します。このようにして、機能の名称とその値をもった配列が構築され、最終的に、新しいハッシュオブジェクトを生成するhash []のクラスメソッドに渡されます。