BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル Javaにおける言語内ドメイン特化言語へのアプローチ

Javaにおける言語内ドメイン特化言語へのアプローチ

ブックマーク

序説

ドメイン特化言語(DSL)は一般に、特定の種類の問題を対象としたコンピュータ言語といわれ、ドメイン外での問題解決は意図していません。DSLは何年もかけて正式に研究されてきました。しかし最近まで、最も読みやすく、簡潔な方法で単に問題解決を図ろうとしていたプログラマーが、幸運にも偶然プログラムに書き込んでいたのが言語内DSLです。Rubyや他の動的言語の到来により、最近ではプログラマーの間でDSLに対する興味が増大しています。こうした緩い構造の言語は、最小限の文法が許容されるDSLへのアプローチを提供し、そのため、特定の言語の最も直接的な表現として現れます。しかしながら、コンパイラと、Eclipseなどの最強かつ近代的なIDEの利用能力を放棄してしまうことは、このアプローチの明らかな欠点であります。著者はこの2つのアプローチの妥協策に成功し、Javaのような構造化言語で、DSLの方向からAPI設計へアプローチする方法が極めて可能かつ有益であると主張します。この記事では、Java言語を使ってドメイン特化言語を書くことがどうして可能かを説明し、構成方法のパターンも提案します。

Javaは言語内ドメイン特化言語の作成に適しているか?

Java言語をDSL作成のツールとして検討する前に、「言語内DSL」の概念を手ほどきする必要があります。言語内DSLはアプリケーションの主要言語で作成され、カスタムのコンパイラやインタプリタの作成(および維持管理)を必要としません。Martin Fowler氏が様々なタイプの言語内DSLと言語外DSL、加えてそれぞれの優れた例について頻繁に書いています。しかしながら、Javaのような言語でDSLを作成することに関しては、ついでとして扱っているだけなのです。

DSLとAPIの区別が難しいことに留意することも重要です。言語内DSLの場合は、DSLとAPIは本質的に同じです。DSLの観点で考えると、ホスト言語を利用して限定範囲内で読みやすいAPIを作成します。可読性の点を考慮に入れ、特定ドメインの特別な問題に焦点を合わせて作成されたAPIにしては、「言語内DSL」はいくぶんしゃれた名称です。

どんな言語内DSLも、そのベース言語の構文と構造に制限されます。Javaの場合、中括弧、括弧とセミコロンの強制使用と、クロージャーとメタプログラミングの欠如により、動的言語で作成したDSLより冗長なDSLという結果になるかもしれません。

明るい面を挙げれば、Java言語を使うことにより、EclipseやIntelliJ IDEAのような強力かつ成熟したIDEを活用することができ、「オートコンプリート」や自動リファクタリング、デバッギングなどの機能のおかげで、DSLの作成、使用、維持管理が容易になります。加えて、Java 5における新しい言語機能(ジェネリクス、vararg、静的インポート)は、Javaのこれまでのバージョンに比べて、一層簡潔なAPI作成に役立ちます。

一般に、Javaで書かれたDSLは、ビジネスユーザがゼロから作成できる言語をもたらすことはありません。それはプログラマーから見て、読み書きが非常に直観的であることに加え、ビジネスユーザにとってもかなり読みやすい言語となるでしょう。言語外DSLや動的言語で書かれたDSLと比べて利点を持っていますが、その利点とは、コンパイラが途中で正当性を実施して、RubyやPearlでは無意味な入力を簡単に受け入れて、ランタイムで失敗するような不適当な使用法にフラグを付けることができることです。これにより、冗長なテストが相当量減少し、アプリケーション品質が劇的に向上します。しかしながら、コンパイラをこのように使って品質を向上させるのは芸術であり、現在、多数のプログラマーが、構文を使ってセマンティクス強化を図る言語作成のためにコンパイラを使うよりも、コンパイラ要件を満たすための「重労働」を嘆いています。

DSL作成にJavaを使うことには、利点と欠点があります。結局、Javaの使用があなたにとって正しい選択であるかどうかは、あなたのビジネスニーズと、あなたが働く環境によって決まるでしょう。 

言語内DSL向けのプラットフォームとしてのJava

SQLの領域に適した「DSL」構築が抗いがたい強みとなるような箇所で、動的にSQLを組み立てることは、この上ない好例です。

SQLを使った従来のJavaコードは次のようになるでしょう。

String sql = "select id, name " +
"from customers c, order o " +
"where " +
"c.since >= sysdate - 30 and " +
"sum(o.total) > " + significantTotal + " and " +
"c.id = o.customer_id and " +
"nvl(c.status, 'DROPPED') != 'DROPPED'";

著者が最近取り組んだシステムから引用した代替表現は以下のとおりです。

Table c = CUSTOMER.alias();
Table o = ORDER.alias();
Clause recent = c.SINCE.laterThan(daysEarlier(30));
Clause hasSignificantOrders = o.TOTAT.sum().isAbove(significantTotal);
Clause ordersMatch = c.ID.matches(o.CUSTOMER_ID);
Clause activeCustomer = c.STATUS.isNotNullOr("DROPPED");
String sql = CUSTOMERS.where(recent.and(hasSignificantOrders)
.and(ordersMatch)
.and(activeCustomer)
.select(c.ID, c.NAME)
.sql();

DSLバージョンには利点がいくつかあります。あとのバージョンには PreparedStatements を透過的に使うスイッチを入れることができました。String 文字列バージョンでは、バインド変数使用に切り替えるために広範囲にわたる修正が必要です。引用が正しくなかったり、比較のために日付欄に整数パラメータが渡されたりすると、後者はコンパイルしないでしょう。「nvl(foo, 'X') != 'X'」というフレーズは、Oracle SQL特定の形式です。Oracle SQL以外のプログラマーやSQLに精通していない者には、事実上読めないでしょう。例えば、SQLサーバーになるとこのイディオムは、「(foo is null or foo != 'X')」になるでしょう。 このフレーズをより容易に理解できる、言葉に似た「isNotNullOr(rejectedValue),」で置き換えれば、可読性が向上しますし、別のデータベースベンダーが提供する機能を活用するための実装変更という、後々の必要性からシステムを保護することにもなります。

Javaで言語内DSLを作成

DSLを作る最良の方法は、まず希望のAPIをプロトタイピングし、次にベース言語の制約という条件のもとで、その実装に取り掛かることです。DSLの実装には、正しい方向に進んでいることを裏付けるための継続的なテストが伴います。この「プロトタイプとテスト」のアプローチは、テスト駆動開発(TDD)が提唱するところです。

Javaを使ってDSLを作成する際、流れるようなインタフェースを介してDSLを作ったらどうでしょう。流れるようなインタフェースにより、モデル化したいドメイン問題が簡潔かつ読みやすい表現になります。流れるようなインタフェースは、メソッドチェーニングを使って実装します。メソッドチェーニング単体ではDSL作成に不十分という点に留意することが重要です。その良い例が、Javaの StringBuilder で、その「append」メソッドは常に同じ StringBuilder のインスタンスを返します。以下に例を挙げます。

StringBuilder b = new StringBuilder();
b.append("Hello. My name is ")
.append(name)
.append(" and my age is ")
.append(age);

この例では、ドメイン固有の問題は解決しません。

メソッドチェーニングに加えて、静的ファクトリメソッドとインポートは、簡潔でありながら読みやすいDSL作成において、非常に役立ちます。次節では、こうしたテクニックをより詳細に説明します。

1. メソッドチェーニング

メソッドチェーニングを使ってDSLを作成するアプローチは2つあり、両アプローチとも、チェーン内のメソッド戻り値に関係があります。何をやろうとしているのかによって、thisを返すか、中間オブジェクトを返すかの選択肢があります。

1.1 thisを返す

チェーンにおけるメソッド呼び出しが以下の場合、通常thisを返します。

  • 任意
  • 任意の順番による呼び出し
  • 任意の回数の呼び出し

このアプローチについて2つの使用ケースを発見しました。

  1. 関連したオブジェクトの振る舞いをチェーニング
  2. オブジェクトの単純な構造/コンフィギュレーション

1.1.1 関連したオブジェクトの振る舞いをチェーニング

「複数メッセージ」(あるいは複数のメソッド呼び出し)の同一オブジェクトへのディスパッチをシミュレートすることによって、コード内の不要テキストを削減するために1オブジェクトのメソッドをチェーニングしたいだけ、ということが何度もあります。次のコードリストはSwing GUIのテストに使われるAPIを表しています。このテストでは、ユーザがパスワードを入力せずにシステムへのログインを試みた場合に、エラーメッセージが表示されることを検証します。

DialogFixture dialog = new DialogFixture(new LoginDialog());
dialog.show();
dialog.maximize();
TextComponentFixture usernameTextBox = dialog.textBox("username");
usernameTextBox.clear();
usernameTextBox.enter("leia.organa");
dialog.comboBox("role").select("REBEL");
OptionPaneFixture errorDialog = dialog.optionPane();
errorDialog.requireError();
errorDialog.requireMessage("Enter your password");

このコードは容易に読めますが、冗長であり、あまりに多くのタイピングを要します。

以下は例で用いた TextComponentFixture のメソッド2つです。

public void clear() {
target.setText("");
}

public void enterText(String text) {
robot.enterText(target, text);
}

単にthisを返すことにより、テスト用APIを単純化し、その結果、メソッドチェーニングを可能にできます。

public TextComponentFixture clear() {
target.setText("");
return this;
}

public TextComponentFixture enterText(String text) {
robot.enterText(target, text);
return this;
}

全テストフィクスチャ内でメソッドチェーニングを可能にしたら、テストコードが減って以下のようになりました。

DialogFixture dialog = new DialogFixture(new LoginDialog());
dialog.show().maximize();
dialog.textBox("username").clear().enter("leia.organa");
dialog.comboBox("role").select("REBEL");
dialog.optionPane().requireError().requireMessage("Enter your password");

結果として、より簡潔かつ読みやすいコードが出来上がりました。前述したとおり、メソッドチェーニング単独ではDSLがあることを意味しません。オブジェクトの関連した振る舞いに呼応するメソッドをチェーニングする必要があり、それが連携してドメイン固有の問題を解決するのです。この例では、ドメイン固有の問題はSwing GUIのテストでした。

1.1.2 オブジェクトの単純な構造/コンフィギュレーション

今度のケースは前回のケースと類似していますが、相違点は、オブジェクトの関連メソッドを単にチェーニングする代わりに、流れるようなインタフェースを使って、オブジェクトの作成と設定の両方もしくはどちらかを行う、「ビルダー」を作成することです。

次の例では、セッタを使って作成した「dream car(夢の自動車)」を説明します。

DreamCar car = new DreamCar();
car.setColor(RED);
car.setFuelEfficient(true);
car.setBrand("Tesla");

DreamCar クラスのコードはかなり単純です。

// package declaration and imports

public class DreamCar {

private Color color;
private String brand;
private boolean leatherSeats;
private boolean fuelEfficient;
private int passengerCount = 2;

// getters and setters for each field
}

DreamCar 作成は簡単で、コードもかなり読みやすいですが、car builderを使えばいっそう簡潔なコードを作成できます。

// package declaration and imports

public class DreamCarBuilder {

public static DreamCarBuilder car() {
return new DreamCarBuilder();
}

private final DreamCar car;

private DreamCarBuilder() {
car = new DreamCar();
}

public DreamCar build() { return car; }

public DreamCarBuilder brand(String brand) {
car.setBrand(brand);
return this;
}

public DreamCarBuilder fuelEfficient() {
car.setFuelEfficient(true);
return this;
}

// similar methods to set field values
}

このビルダーを使って、DreamCar 作成を以下のように書き直せます。

DreamCar car = car().brand("Tesla")
.color(RED)
.fuelEfficient()
.build();

ここでも、流れるようなインタフェースの使用により、コード内のノイズを減らし、結果としてさらに読みやすいコードが出来上がりました。this を返すとき、チェーン内のいかなるメソッドも、いつでも、何度でも呼び出せるので、注意することが肝要です。この例では、color メソッドを希望するだけ何度でも呼びだすことができ、各呼び出しが前回の呼び出しによって設定された値をオーバーライドすることになりますが、それはアプリケーションというコンテキスト内では有効でしょう。

もう1つの重要注目点は、必要とされるフィールド値を強制するためのコンパイラチェックがないことです。オブジェクト作成とコンフィギュレーションの規則の両方、もしくは一方に違反がある(例えば必要フィールドの欠如)場合、考えられる解決策はランタイムに例外を投入することです。チェーン内のメソッドから中間オブジェクトを返すことによって、ルールの検証を果たすことは可能です。

1.2 中間オブジェクトを返す

流れるようなインタフェースでメソッドから中間オブジェクトを返すことは、this を返すことと比較して利点があります。

  • コンパイラを使ってビジネスルール(例えば必要フィールド)を強制できる
  • チェーン内の次の要素で利用可能なオプションを制限することにより、流れるようなインタフェースのユーザを特定のパス経由で導くことができる
  • ユーザがどのメソッドを呼び出せるか(あるいは呼び出さなければならないか)、APIユーザがどの順番で何回メソッドを呼び出せるかについて、API作成者にさらに大きな制御権を与える

次の例ではコンストラクタ引数を使ったvacation(休暇)作成を例証します。

Vacation vacation = new Vacation("10/09/2007", "10/17/2007",
"Paris", "Hilton",
"United", "UA-6886");

このアプローチの利点は、必要パラメータ全ての指定をユーザに強制することです。残念ながら、パラメータが多すぎますし、パラメータの目的が伝えられていません。「Paris」と「Hilton」は目的地の都市とホテルを意味するのでしょうか。それとも、連れの名前を意味するのでしょうか。 :)

2番目のアプローチでは、各パラメータを記録する方法としてセッタを使います。

Vacation vacation = new Vacation();
vacation.setStart("10/09/2007");
vacation.setEnd("10/17/2007");
vacation.setCity("Paris");
vacation.setHotel("Hilton");
vacation.setAirline("United");
vacation.setFlight("UA-6886");

コードは読みやすくなりましたが、同時に冗長になってしまいました。3番目のアプローチでは、前節の例のように、流れるようなインタフェースを作ってvacation(休暇)を構築できるでしょう。

Vacation vacation = vacation().starting("10/09/2007")
.ending("10/17/2007")
.city("Paris")
.hotel("Hilton")
.airline("United")
.flight("UA-6886");

このバージョンはいっそう簡潔かつ読みやすいのですが、最初のバージョンにあった欠落フィールドのコンパイラチェック(コンストラクタを使用したもの)がなくなってしまいました。換言すると、ミスの可能性をチェックするためにコンパイラを利用していないのです。この時点で、このアプローチに対する最善策は、必要フィールドが未設定の場合、ランタイムに例外を投入することです。

次は、流れるようなインタフェースの4番目で、より高性能なバージョンです。今回メソッドが返すのは、thisではなく中間オブジェクトです。

Period vacation = from("10/09/2007").to("10/17/2007");
Booking booking = vacation.book(city("Paris").hotel("Hilton"));
booking.add(airline("united").flight("UA-6886");

ここでは、Location (場所)と BookableItem (Hotel〔ホテル〕と Flight〔便名〕)、Airline〔航空会社〕に加えて、Period〔期間〕Booking〔予約〕のコンセプトを導入しました。このコンテキストで航空会社は、Flight オブジェクトのファクトリ役を果たしており、LocationHotel 項目のファクトリ役、等々となっています。こうした各オブジェクトには、望ましいbooking構文が伴っていますが、システム内にもこの他の重要動作が多数発生していくことはほぼ確実です。中間オブジェクトの使用により、ユーザができることとできないことを、コンパイラによってチェックする制約を導入できるようになります。例えば、このAPIのユーザが、開始日はあっても終了日のない休暇を予約しようとすると、コードは単にコンパイルしません。前述したように、セマンティクスを強制する構文を使用するような言語を作成できるのです。

前の例では、静的ファクトリメソッドの使用法も紹介しました。静的ファクトリメソッドを静的インポートと一緒に使うと、簡潔度を増した流れるようなインタフェースを作成できます。例えば、静的インポートを使わないのであれば、前の例は次のようにコードする必要があります。

Period vacation = Period.from("10/09/2007").to("10/17/2007");
Booking booking = vacation.book(Location.city("Paris").hotel("Hilton"));
booking.add(Flight.airline("united").flight("UA-6886");

上の例は、静的インポートを使った例と比較すると、読みにくくなっています。静的ファクトリメソッドとインポートについては、次節でさらに詳しく説明します。

Javaを使ったDSLの2例目は、次のとおりです。今回はJavaリフレクションの使用を単純化しています。

Person person = constructor().withParameterTypes(String.class)
.in(Person.class)
.newInstance("Yoda");

method("setName").withParameterTypes(String.class)
.in(person)
.invoke("Luke");

field("name").ofType(String.class)
.in(person)
.set("Anakin");

メソッドチェーニングの使用には注意が必要です。使い過ぎを起こしやすいので、単一行に多数の呼び出しがチェーニングされた結果、「悲惨な状況」に陥ります。可読性の著しい低下や、例外発生時のスタックトレース内の曖昧さなど、多数の問題をもたらす可能性があります。

2. 静的ファクトリメソッドとインポート

静的ファクトリメソッドとインポートにより、APIをいっそう簡潔かつ読みやすくできます。静的ファクトリメソッドは、Javaで名前付きパラメータをシミュレートする都合の良い方法であることが分かり、これは多くの開発者がJavaにあればいいなと思っている機能です。例えば、次のコードを考えてみましょう。コードの目的は、ユーザによる JTableの横列選択をシミュレートすることにより、GUIをテストすることです。

dialog.table("results").selectCell(6, 8); // row 6, column 8 

コメント「// row 6, column 8(横列6、縦列8)」がなければ、このコードの目的を簡単に誤解してしまう(あるいは、まったく理解しない)でしょう。ドキュメンテーションをチェックしたり、さらにコードを読み進めたりして、余分な時間を費やして「6」と「8」が何を表しているのかを理解する必要があるでしょう。横列と縦列のインデックスを変数として、いっそのこと定数として宣言することもできます。

int row = 6;
int column = 8;
dialog.table("results").selectCell(row, column);

コードの可読性は改善されましたが、それには維持すべきコードをさらに追加するという代償を払いました。コードを可能な限り簡潔に保つには、理想的な解決策は以下のように書くことです。

dialog.table("results").selectCell(row: 6, column: 8); 

残念ながらJavaは名前付きパラメータをサポートしていないため、これは不可能です。救いは、静的ファクトリメソッドと静的インポートを使うことにより、これをシミュレートして、以下のように書けることです。

dialog.table("results").selectCell(row(6).column(8)); 

メソッドのシグネチャを変えることから始めるが、それには、すべてのパラメータを、パラメータを含有する1オブジェクトで置き換えます。今回の例では、selectCell(int, int) のシグネチャを以下のように変えることができます。

selectCell(TableCell); 

TableCell には横列と縦列インデックスの値が含まれています。

public final class TableCell {

public final int row;
public final int column;

public TableCell(int row, int column) {
this.row = row;
this.column = column;
}
}

この時点では、単に問題を移動しただけです。 TableCell のコンストラクタは依然2つの int(整数)値をとっています。次のステップは TableCell のファクトリ導入で、オリジナル版の selectCellではパラメータ毎に1つのメソッドがあります。さらに、ユーザにファクトリの使用を強制するために、TableCell のコンストラクタを private(プライベート)に変える必要があります。

public final class TableCell {

public static class TableCellBuilder {
private final int row;

public TableCellBuilder(int row) {
this.row = row;
}

public TableCell column(int column) {
return new TableCell(row, column);
}
}

public final int row;
public final int column;

private TableCell(int row, int column) {
this.row = row;
this.column = column;
}
}

ファクトリ TableCellBuilder を備えることにより、メソッド呼び出し毎に1つのパラメータを有する TableCell を作成できます。ファクトリ内の各メソッドがそのパラメータの目的を伝達します。

selectCell(new TableCellBuilder(6).column(8)); 

最後のステップは静的ファクトリメソッドを導入して、6が何を表すかを伝達していない TableCellBuilder コンストラクタの使用に置き換えることです。以前もしたように、ユーザにファクトリメソッドの使用を強制するために、コンストラクタを private(プライベート)にする必要があります。

public final class TableCell {

public static class TableCellBuilder {
public static TableCellBuilder row(int row) {
return new TableCellBuilder(row);
}

private final int row;

private TableCellBuilder(int row) {
this.row = row;
}

private TableCell column(int column) {
return new TableCell(row, column);
}
}

public final int row;
public final int column;

private TableCell(int row, int column) {
this.row = row;
this.column = column;
}
}

こうなると、TableCellBuilder のメソッド row (横列)向けに静的にインポートを行い、selectCell を呼び出すコードに追加すればよいだけです。記憶を呼び覚ますために、selectCell の呼び出しがどんなものかを以下に記しました。

dialog.table("results").selectCell(row(6).column(8)); 

余分な作業を多少行えば、ホスト言語の限界の中には克服できるものもあることを例が示しています。前述したように、静的ファクトリメソッドとインポートを使ってコードの可読性を改善する方法は複数あり、ここでお見せしたのはそのうちの一例にすぎません。次のコードリストは、静的ファクトリメソッドとインポートを異なる方法で使い、同一のテーブルインデックス問題に対する代替解決法を示しています。

/**
* @author Mark Alexandre
*/
public final class TableCellIndex {

public static final class RowIndex {
final int row;
RowIndex(int row) {
this.row = row;
}
}

public static final class ColumnIndex {
final int column;
ColumnIndex(int column) {
this.column = column;
}
}

public final int row;
public final int column;
private TableCellIndex(RowIndex rowIndex, ColumnIndex columnIndex) {
this.row = rowIndex.row;
this.column = columnIndex.column;
}

public static TableCellIndex cellAt(RowIndex row, ColumnIndex column) {
return new TableCellIndex(row, column);
}

public static TableCellIndex cellAt(ColumnIndex column, RowIndex row) {
return new TableCellIndex(row, column);
}

public static RowIndex row(int index) {
return new RowIndex(index);
}

public static ColumnIndex column(int index) {
return new ColumnIndex(index);
}
}

2バージョン目の解決策は、最初のバージョンよりいっそう柔軟性があります。なぜなら、横列と縦列インデックスが2つの方法で指定可能だからです。

dialog.table("results").select(cellAt(row(6), column(8));
dialog.table("results").select(cellAt(column(3), row(5));

コードの組織化

メソッドが中間オブジェクトを返すコードよりも、メソッドが this を返す流れるようなインタフェースのコードの方がずっと簡単に組織化できます。流れるようなインタフェースの場合は、インタフェースのロジックをカプセル化するクラス数がより少ないため、非DSLコードの組織化に際しても、同じルールや約束を使うことができます。

流れるようなインタフェースのロジックが小クラス数個にまたがって散在しているため、中間オブジェクトを戻り型として用いる流れるようなインタフェースのコード組織化には、用心が必要です。こうしたクラスが連携し、全体として流れるようなインタフェースを形成しているので、一緒にしておくことは道理にかなっており、DSL以外のクラスとの混合は避けた方がよいでしょう。2つの選択肢が見つかりました。

  • 中間オブジェクトを内部クラスとして作成する
  • 中間オブジェクトをそれ自身の最上位クラスに置き、すべてを同じパッケージに入れる

システムを分解するために使うアプローチの決定は、達成したい構文、DSLの目的、中間オブジェクトの(コード行の)数と大きさ(中間オブジェクトが存在する場合)、DSLが他のあらゆるDSLや残りのコードベースといかにして適合するかといった、いくつかの要因に左右されます。

コードのドキュメンテーション

コードの組織化同様に、中間オブジェクトを返す流れるようなインタフェースのドキュメンテーションよりも、メソッドがthisを返す流れるようなインタフェースのドキュメンテーションの方がずっと簡単であり、特にJavadocを使ってドキュメンテーションしている場合にその傾向が強いのです。

Javadocは1度に1クラスのドキュメンテーションを表示しますが、中間オブジェクトを使ったDSLでは最良とは言えないかもしれません。DSLはクラスのグループで構成されており、個々のクラスではないからです。JavadocがAPIのドキュメンテーションを表示する方法を変えることはできないので、流れるようなインタフェースの使用例(すべての参加クラスを含む)とチェーン内の各メソッドへのリンクを、package.htmlファイルに入れれば、Javadocの限界を最小限にできることが分かりました。

ドキュメンテーションが重複しないよう、注意を払わねばなりません。重複はAPIクリエータの維持費増加を招くからです。最良のアプローチは、可能な限り、実行可能なドキュメンテーションとしてテストに依存することです。

結論

開発者が非常に直観的に読み書きでき、ビジネスユーザから見ても、かなり読みやすい言語内ドメイン特化言語の作成には、Javaが適していると言えます。Javaで作成したDSLは動的言語で作ったDSLより冗長かもしれません。明るい方を見れば、Javaを使うことによってDSLのセマンティクスを強制するコンパイラを利用できるのです。さらに、完成度が高くて強力なJava IDEを利用して、DSLの作成、使用、維持管理が非常に簡単に行えます。

JavaでDSLを作成すると、APIデザイナーにもさらにたくさんの仕事をしてもらう必要があります。作成と維持管理の必要なコードとドキュメンテーションが増加します。しかし、そうするだけの価値ある成果が待っているかもしれません。我々のAPIユーザは、コードベースの改善に気付くでしょう。コードがいっそう簡潔で、維持管理も容易になり、これによって日々の仕事も簡単にできます。

何を達成しようとしているかによって、Javaを利用したDSL作成にも異なる方法が多数あります。「1つで皆に適合する」アプローチは存在しませんが、メソッドチェーニングと静的ファクトリメソッドおよびインポートの組合せにより、読み書きの両方が容易にでき、クリーンで簡潔なAPIがもたらされることが分かりました。

まとめれば、DSL作成にJavaを使うと、利点も欠点もあります。決定を下すのは我々開発者であり、プロジェクトのニーズに基づいて、Javaが正しい選択かどうかを決定することになります。

余談になりますが、Java 7(サイト・英語)は新しい言語機能(クロージャなど)を備える可能性があり、冗長性を低減したDSL作成に役立つかもしれません。提案された機能に関する総合リストは、Alex Millerのブログ(source)でご覧ください。

著者について

Alex RuizはOracleの開発ツール組織でソフトウェア・エンジニアとして働いています。AlexはJavaやテスト、OOP、AOP関連であれば、何でも喜んで読み、プログラミングが大好きです。Oracle入社前は、ThoughtWorksのコンサルタントをしていました。Alexのブログはhttp://www.jroller.com/page/alexRuizです。

Jeff Bayはニューヨークのヘッジファンドで上級ソフトウェア・エンジニアをしています。Onstar向けのプログラム登録やソフトウェアリース、Webサーバー、構築プロジェクト管理など、多様なシステムに取り組む質も速度も高いXPチームを何度も作り上げてきました。開発者の効率と仕事に割り当てられた時間の改善を図るため、重複削除とバグ防止に情熱を注ぎ、ソフトウェアに取り組みます。

参考資料

  • Domain Specific Language(ドメイン特化言語)(source)(Martin Fowler)
  • DSL Boundary(DSL境界)(source)(Martin Fowler)
  • Language Workbenches: The Killer-App for Domain Specific Languages?(言語のワークベンチ:ドメイン特化言語のキラーアプリケーション)(source)(Martin Fowler)
  • Fluent Interface(流れるようなインタフェース)(source)(Martin Fowler)
  • Design Patterns: Elements of Reusable Object-Oriented Software (設計パターン:再利用可能なオブジェクト指向ソフトウェアの構成要素)(source)(Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides)
  • Effective Java (効果的なJava)(source)(Joshua Bloch)
  • Java.sun.com(サイト・英語)のStatic Import(source)
  • Generics(ジェネリクス)(source)(Gilad Bracha)
  • Proposed features in Java 7(Java 7で提案された機能)(source)(Alex Miller)
  • WikipediaのTest-Driven Development(テスト駆動環境)(source)の定義
  • Simulating Named Parameters in Java(Javaで名前付きパラメータをシミュレート)(source)(Alex Ruiz)
  • Mark Alexandreによる TableCellIndex (Simulating Named Parameters in Javaの最初のコメント)(source)
  • FEST(Fixtures for Easy Software Testing)(source)のSwing module(source)から選んだSwing GUIのテスト例
  • これもFEST(source)のReflection module(Swing moduleに含まれる)(source)から選んだリフレクション例
  • 読者に身近なオープンソースプロジェクトで見られるSQL例:)
原文はこちらです:http://www.infoq.com/articles/internal-dsls-java

この記事に星をつける

おすすめ度
スタイル

BT