![](http://nesteruk.org/pix/0/85702fef-9deb-4ea9-90ff-8b5062398ef7.jpg)
注:英語の記事とソースコードはこちら
はじめに
一般的に、ドメイン固有言語(ドメイン固有言語、略してDSL)には3つの形式があります。 最初の種類はテキストDSLで、これはテキストと構造によってのみ定義され、このテキストをコードに変換する特定のプロセスに関連付けられています。 2番目の種類は構造DSLで、コンテンツはツリーまたはグラフのようなエディターを使用して定義されます。 3番目のタイプであるグラフィックDSLについて説明します。開発者はグラフィックエディターを使用して、後でコードに変換できる視覚的な構造を作成します。
この記事では、エンドユーザーが非同期操作を定義できる単純なグラフィカルDSLを作成します。非同期操作は、Pulse&Waitメカニズムを使用して編成されます。 添付の例をコンパイルするには、Visual Studio 2008とVisual Studio SDKが必要です。 Microsoft DSLツール(SDKに含まれています)を使用してDSLを作成します。
問題の説明
Pulse&Waitを使用するのは難しいため、Pulse&Waitメカニズムを使用して整理できる操作のシーケンスを決定できるグラフィカルなDSLを作成します。 特に、エディターで非同期ブロックをドラッグアンドドロップできるようにするとともに、ブロック間の関係を定義して、非同期の依存実行のルールを作成できるようにしたいと考えています。
DSL作成
始める前に、DSLツールを使用する際の最も重要なポイントの概要を説明します。
- DSLツールでは、グラフィックDSL自体がグラフィックDSLを使用して作成されます。 これは一見わかりにくいかもしれませんが、原則として、非同期DSL(ここではAsyncDslと呼びます)のほとんどはプログラミング言語ではなく視覚要素を使用して開発されることを理解する必要があります。 もちろん、舞台裏には多くのコードがありますが、頻繁に遭遇することはありません。
- DSLツールは、T4テクノロジーを幅広く利用しています。 グラフィカルDSLは実際にはXMLの視覚的表現にすぎず、T4はこのXMLをコードに変換します。 そうすれば、DSLツールで視覚要素を編集するとき、実際にはXMLを編集していることになります。
- DSLはまだC#を使用して作成され、コンパイルされます。 部分クラスなどを使用して拡張できます。これにより、DSLが特定の方法で動作できるようになります。 この記事では、このようなことはしません。
- DSLツールを使用したDSLの作成は、視覚的な部分(ユーザーがXMLモデルを視覚的に作成できる部分)にのみ適用されます。 それをプレーンテキストコードに変換する部分は、後で会う別の問題です。
Visual StudioでDSLプロジェクトを作成するには、[新規プロジェクト]を選択し、[その他のプロジェクトタイプ]→[拡張性]→[ドメイン固有の言語デザイナー]を選択します。
![](https://habrastorage.org/getpro/habr/post_images/a3f/eee/821/a3feee82110fd651ccd83b6483b07601.jpg)
[OK]をクリックすると、作成したDSLの機能の一部を特定できるウィザードが表示されます。
- 最初のページでは、言語の名前を決定することに加えて、初期テンプレートを選択することもできます。 このテンプレートは、DSLの初期機能を決定します。たとえば、タスクフローを選択することにより、初期DSL要素がフローチャートのような構造に属することを決定します。 この段階で選択するテンプレートに関係なく、最初に生成された要素を削除することにより、DSLの動作をいつでも再定義できます。
- 2番目のページでは、DSLのファイル拡張子(拡張子)を選択できます。 この拡張機能は、DSLを自分のプロジェクトに埋め込む場所に表示されます。 拡張機能に加えて、ウィザードはアイコンを生成します。
- 3番目のページでは、DSLが属する製品の名前など、DSLを定義するいくつかの行を指定できます。
- 4ページ目では、既存のアセンブリまたは新しいキーのいずれかを使用して、アセンブリに実際に署名する必要があります。
ウィザードでの作業が完了すると、DSL定義フレームワークが取得されます。 Visual StudioでDSL機能を使用したことがない場合、スクリーンショットは少しショックを受けるかもしれません。
![](https://habrastorage.org/getpro/habr/post_images/3a7/c21/8b6/3a7c218b6d5614956be54f29875f28e2.jpg)
次の要素がDSL編集プロセスに関係しています。
- Dsl Designerツールボックス。 このパネルには、DSLの設計時に使用するすべての要素が含まれています。 これらの要素は、たとえばWinFormsの場合と同じ方法で使用されます-要素を取り、それをエディターウィンドウ(つまり、奇妙なボックスのある中央のウィンドウ)にドラッグします。
- DSLデザイナー自身。 実際、これは拡張子が
.dsl
ファイルですが、.dsl
、エディターはメガビジュアルです-前述したように、DSLは単独で他のDSLを使用して構築されます。 このDSLには2つの部分があります-左側にはクラスとクラス間の関係があり、右側には視覚要素があります。 エンドユーザーが使用するDSLの概念の視覚的な反映。 したがって、DSLは次のように想像できます。右側は視覚化、左側は論理です。
- ソリューションエクスプローラー。 DSLを作成すると、 2つのプロジェクトが作成されます。1つは作成するDSLを定義し、もう1つはDSL関連コンポーネントのエディターを定義します。 これについては後で詳しく説明します。今のところ、すべてのテンプレートを変換するボタンを1つだけマークすることが重要です。
これは非常に重要なボタンです。 前にも言ったように、DSLはコードに変換されるXML仕様です。 つまり、DSLの定義を更新するには(定義もDSLです)、C#のすべてのテンプレートを変換する必要があります。 上のボタンはまさにそれを行います。 したがって、最終的なDSLの変更が突然反映されたと確信している場合は、このボタンをクリックするのを忘れていました。
- DSLエクスプローラー。 これはスタジオの新しいパネルで、DSLをツリー形式で表示し、次のようになります。
このツリーは、DSLの構造的な側面の多くをカプセル化します。 ツリーの一部のノードには独自のプロパティセットがあり、F4キーを押すと表示されることに注意してください。
- プロパティページを編集するためのページは、DSL Explorerツリーの要素とDSLエディターの視覚要素の両方に存在します。 一部のDSL要素は「エディターで」直接編集できます。たとえば、プロパティパネルを開かずに、2つの要素間の関係の形式(1対多、多対多など)を定義できます。 それがどのように便利であるかはあなた次第です。
それでは、DSLを作成するプロセスを詳しく見ていきましょう。
視覚要素の配置
既に書いたように、ツールボックスには作業する必要があるすべての要素が含まれています。 これらの要素は、「論理」と「視覚」の2つのグループに分けられます。 ロジック要素は、DSLの構造(ドメイン)を定義するものです。 視覚要素は、ユーザーがDSLを操作するときに操作するこれらの長方形、線、およびゆるい要素を反映します。
DSL論理構造の中心概念は、ドメインクラスです。 このクラスは、作業しているサブジェクト領域に応じて、 必要なものを表すことができます 。 非同期操作で作業するため、ドメインクラスの1つは
Operation
と呼ばれます。
![](https://habrastorage.org/getpro/habr/post_images/363/0fa/890/3630fa890301410948c27f7083eb7626.jpg)
ドメインクラスはプロパティを持つことができます 。 ユーザーが設定できる値。
Operation
クラスには、エンドユーザーが
Operation
オブジェクトのインスタンスをモデルにドラッグした後に決定できる
Timeout
、
Name
および
Description
プロパティがあります。
小さな問題があります-実際、ユーザーはドメインクラスをモデルに直接ドラッグしません。 代わりに、
OperationShape
モデルに自分自身をドラッグします。これは、
Operation
視覚的な反映です。 このクラスは、
GeometryShape
(同じツールボックスから取得)から形成されます。
![](https://habrastorage.org/getpro/habr/post_images/7a4/1da/59b/7a41da59bc0f558250357f0ad00313b5.jpg)
Operation
ドメインクラスと
Operation
視覚表現を定義したら、それらをバインドする必要があります(そのまま実行すると、何も機能しません)。 これを行うには、ダイアグラム要素マップ要素を使用します。 実際、これは2つの要素を接続し、それらの間の関連付けを定義する線です。 ただし、追加しても、何も機能しません。
要素間の関係
DSLのツールボックスコントロールの作成を始める前に(これは楽しいです)、要素間の関係について話す必要があります。 関係には、埋め込み関係と参照関係の2つのタイプがあります。 埋め込み関係を使用する場合、要素Aは要素Bで完全に囲まれます。たとえば、スイムレーン(大きな水平方向の視覚空間)があり、それにクラス全体を挿入する必要がある場合、埋め込み関係を使用するのは理にかなっています。 コメントを添付する必要のあるブロックだけがあれば、参照関係になります。
特定のタスクに要素を使用する方法を見てみましょう。 缶の「ルート」に
ExampleModel
要素があります。 この要素の名前も変更しません。なぜなら、 最終的なDSLには表示されません。 モデルにプロセスとコメントが含まれていることを判断するために、それぞれのクラス間の埋め込み関係の線を引き、次の図を取得します。
![](https://habrastorage.org/getpro/habr/post_images/c5b/d67/a23/c5bd67a23beb6a4d88c57255f52c0ed4.jpg)
オレンジ色のボックスは関係を象徴し、両側に名前と基本的な関係があります。 カーディナリティは、DSLデザイナーによって後から規制されるため、エンドユーザーはこれに違反することはできません。 リレーションについては、これらのオレンジ色のボックスの意味は、既に完成したDSLを編集するときに、異なるドメインクラスをバインドできることです。
注: DSLデザイナーは多くのルールをあなたの言語に適用しますが、その1つはすべての要素が何かの一部であることを要求します。 これは、すべての要素を1つの「コンテナ」コンテナに減らす必要があることを意味します。 (DSL == XMLであることを思い出せば、この要件の理由は明らかです。)
埋め込み関係を使用して、DSLにプロセスとコメントの両方がモデル全体の一部であることを伝えました。 これで、参照関係を使用して、プロセスにコメントを付けることができ、これら2つの要素をリンクできることを確認できます。
![](https://habrastorage.org/getpro/habr/post_images/c8a/b37/7d9/c8ab377d9860cf49ce37a4615fdc8792.jpg)
上記の破線は、参照関係、つまり この場合、操作はコメントを参照するだけで、コメントは含まれません。 もちろん、この関係には独自の視覚要素(操作とコメントを結ぶ線)があります。これについては、これから説明します。
最後にツールボックス
DSLの論理的および視覚的な部分を受け取ったら、ユーザーがこのDSLからデザイナーにアイテムをドラッグできるようにする必要があります。 ここから開始します-DSL Explorerのエディターノードから:
![](https://habrastorage.org/getpro/habr/post_images/624/95b/9a9/62495b9a9c39b68654be8606aaae0f7a.jpg)
ツールボックスの新しい要素を作成するには、DSL全体を右クリックします。 次のメニューが表示されます。
![](https://habrastorage.org/getpro/habr/post_images/738/d93/52d/738d9352d7bcf007dc300e8df4d0bb75.jpg)
コネクタと要素の2つのオプションがあります。 コネクターは、要素を接続する線です(矢印が付いている場合もあります)。 要素はブロック型の構造です。
新しい要素を作成した後、F4を押すと、この要素のプロパティが表示されます。
![](https://habrastorage.org/getpro/habr/post_images/0d6/9e2/5cc/0d69e25cc13d1ed21a6fc4dc85209dc9.jpg)
ここで重要なのは、これらのプロパティのいくつかを入力する必要があることです。そうしないと、DSLは起動しません。 明らかに定義する必要があるもののうち、要素を反映するドメインクラスの定義、およびアイコンの定義。 (いくつかのデフォルトのアイコンがすでに提供されているため、自分で作成するのが面倒な場合は、既製のアイコンを使用できます。)
発射!
DSLを作成するプロセスを要約します。
- ウィザードを使用して基本的なDSLを作成しました
- processなど、必要な概念を表すドメインクラスを追加しました。
- ドメインクラス間の関係を追加しました。この例では、操作が一般モデルに属し、コメントがあることを確認しました。 また、操作間の遷移操作、および開始と終了の要素が追加されました。
- DSLが使用する視覚要素を特定しました。
- 視覚要素をドメインクラスに関連付けます。
- ツールボックスコントロールを作成し、対応するクラスに関連付けました。
DSLの準備は半分整っています。視覚的な部分のみを定義しています。 すべてのテンプレートを変換し、言語を起動したら、ついにDSLで遊ぶことができます。
![](https://habrastorage.org/getpro/habr/post_images/eee/f8e/7b7/eeef8e7b7eb6413340992c3f236f0aa4.jpg)
コンセプトの
非同期DSLについて、次のイディオムを特定しました。
- 運営
これは、「お茶を作る」などの作業単位です。 操作が失敗することなく実行できることを意味します。
- 処理
プロセスは、グラフ内の一連の操作です。 この要素を追加した唯一の理由は、同じクラスに複数の操作グラフを保持できるようにするためです。
- 開始と終了
プロセスはどこかで開始および終了する必要があるため、開始と終了の状態をマークする2つの要素を作成しました。
- 終了から開始への移行
この遷移は、別の操作が完了した後にのみ操作を開始できることを決定します。
- 開始から開始への移行
この遷移により、操作は別の操作が開始されたときにのみ開始でき、それ以前には開始できないことが決定されます。
実際の例を見てみましょう:朝食を食べるプロセス(私は知っているが、あまり賢くない)。 朝食を作るには、やかんを入れて、トースターにパンを入れる必要があります-任意の順序で。 すべてが準備されている間、ジャムを取得したいのですが、トースターをすでにオンにしている場合のみです。 完成したパンとジャムを手に入れたら、サンドイッチを作ることができます。 そして、サンドイッチとお茶の両方が準備できたときだけ、私は朝食を食べ始めることができます。
DSLを使用すると、プロセス全体を次のように定義できます。
![](https://habrastorage.org/getpro/habr/post_images/e55/2fe/d00/e552fed00aaa7f39f1a7e0491819e67f.jpg)
ご想像のとおり、太線は開始から終了までを表し、破線は開始から開始までを表します。
T4を使用してモデルを変換する
視覚的な朝食モデルはDSLとしてのみ存在するため、T4を使用して本格的なコードに変換する必要があります。 幸いなことに、変換を行う必要があるときまでに、モデルは既にXML形式に変換されています。残りは、それをバイパスして必要なものを生成することだけです。
T4での最終結果の生成は、
WriteLine()
(宛先ファイルに行を書き込む)や
Push/PopIndent()
(スタック上のインデントの数を保持する
Push/PopIndent()
などのいくつかの方法で移動します。
ここではT4変換コードを紹介しません。上記のリンクからダウンロードできます。 代わりに、朝食の定義からDSLが生成するものを示します。
namespace Debugging<br/>
{<br/>
using System.Threading;<br/>
partial class Breakfast<br/>
{<br/>
private readonly object MakeSandwichLock = new object ();<br/>
private readonly object EatBreakfastLock = new object ();<br/>
private readonly object GetJamLock = new object ();<br/>
private bool MakeTeaIsDone;<br/>
private bool ToastBreadIsDone;<br/>
private bool GetJamIsDone;<br/>
private bool MakeSandwichIsDone;<br/>
private bool MakeTeaStarted;<br/>
private bool ToastBreadStarted;<br/>
private bool GetJamStarted;<br/>
private bool MakeSandwichStarted;<br/>
protected internal void MakeTea()<br/>
{<br/>
MakeTeaImpl();<br/>
lock (EatBreakfastLock)<br/>
{<br/>
MakeTeaIsDone = true ;<br/>
Monitor.PulseAll(EatBreakfastLock);<br/>
}<br/>
}<br/>
protected internal void ToastBread()<br/>
{<br/>
lock (GetJamLock)<br/>
{<br/>
ToastBreadIsDone = true ;<br/>
Monitor.PulseAll(GetJamLock);<br/>
}<br/>
ToastBreadImpl();<br/>
lock (MakeSandwichLock)<br/>
{<br/>
ToastBreadIsDone = true ;<br/>
Monitor.PulseAll(MakeSandwichLock);<br/>
}<br/>
}<br/>
protected internal void GetJam()<br/>
{<br/>
lock (GetJamLock)<br/>
if (!(ToastBreadStarted))<br/>
Monitor.Wait(GetJamLock);<br/>
GetJamImpl();<br/>
lock (MakeSandwichLock)<br/>
{<br/>
GetJamIsDone = true ;<br/>
Monitor.PulseAll(MakeSandwichLock);<br/>
}<br/>
}<br/>
protected internal void MakeSandwich()<br/>
{<br/>
lock (MakeSandwichLock)<br/>
if (!(ToastBreadIsDone && GetJamIsDone))<br/>
Monitor.Wait(MakeSandwichLock);<br/>
MakeSandwichImpl();<br/>
lock (EatBreakfastLock)<br/>
{<br/>
MakeSandwichIsDone = true ;<br/>
Monitor.PulseAll(EatBreakfastLock);<br/>
}<br/>
}<br/>
protected internal void EatBreakfast()<br/>
{<br/>
lock (EatBreakfastLock)<br/>
if (!(MakeTeaIsDone && MakeSandwichIsDone))<br/>
Monitor.Wait(EatBreakfastLock);<br/>
EatBreakfastImpl();<br/>
}<br/>
}<br/>
}<br/>
たくさんのコード! ただし、このコードは、定義した構造を反映しています。 あとは、生成された構造を使用するだけです。
namespace Debugging<br/>
{<br/>
partial class Breakfast<br/>
{<br/>
AutoResetEvent eatHandle = new AutoResetEvent( false );<br/>
Random rand = new Random();<br/>
public void Prepare()<br/>
{<br/>
ThreadStart[] ops = new ThreadStart[] {<br/>
MakeTea,<br/>
GetJam,<br/>
ToastBread,<br/>
MakeSandwich,<br/>
EatBreakfast };<br/>
foreach (ThreadStart op in ops)<br/>
op.BeginInvoke( null , null );<br/>
eatHandle.WaitOne();<br/>
}<br/>
private int RandomInterval<br/>
{<br/>
get<br/>
{<br/>
return (1 + rand.Next() % 10) * 100;<br/>
}<br/>
}<br/>
public void MakeTeaImpl()<br/>
{<br/>
Thread.Sleep(RandomInterval);<br/>
Console.WriteLine( "Make tea" );<br/>
}<br/>
public void ToastBreadImpl()<br/>
{<br/>
Thread.Sleep(RandomInterval);<br/>
Console.WriteLine( "Toast bread" );<br/>
}<br/>
public void GetJamImpl()<br/>
{<br/>
Thread.Sleep(RandomInterval);<br/>
Console.WriteLine( "Get jam" );<br/>
}<br/>
public void MakeSandwichImpl()<br/>
{<br/>
Thread.Sleep(RandomInterval);<br/>
Console.WriteLine( "Make sandwich" );<br/>
}<br/>
public void EatBreakfastImpl()<br/>
{<br/>
Thread.Sleep(RandomInterval);<br/>
Console.WriteLine( "Eat breakfast" );<br/>
eatHandle.Set();<br/>
}<br/>
}<br/>
}<br/>
このコードを呼び出した結果は次のようになります。
お茶を入れる
トーストパン
ジャムを取得
サンドイッチを作る
朝食を食べる
すべて完了
もちろん、
Make tea
と
Toast bread
は異なる順序で表示される場合があります。
おわりに
DSL Toolsは、洗練された強力なツールキットです。 このパッケージの重要な特徴は、定義された後の言語の扱いやすさです。 ここでは、DSLツールを使用した作業を表面的にしか説明できませんでした。 機会とニュアンスはたくさんあります。 この投稿が、誰かが自分の研究を行うよう動機付けられることを願っています。 ■