最近のほとんどの Web アプリケーションでは、リクエスト変数とレスポンス変数をHTML の入力タグと同期できるほどインテリジェントなビュー・レイヤーを持っています。ユーザーからの入力は、Webアプリケーションと HTTP の構造化されたレイヤーを経由するため、この同期プロセスは簡単に実現することができます。一方、JavaGUI アプリケーションには、必ずしもこうした秩序がありません。Java GUI アプリケーションは、SWT(Standard Widget Toolkit) で書かれている場合であっても Swing で書かれている場合であっても、通常はアプリケーションのドメイン・オブジェクトとGUI コントロール (一般的にはコンポーネントとも呼ばれます) との間に、定義されたパスを持っていません。
秩序がないおかげで、最悪の場合は大混乱が起き、それほどひどくない場合でも、決まり切った大きな同期コード・ブロックからバグが生じやすくなります。リスト1 のコードを考えてみてください。FormBean という、単純なドメイン・オブジェクトが定義されていることがわかります。あるダイアログがこのデータを利用しようとすると、コンストラクターの最後でのメソッド・コールを見るとわかるように、そのダイアログはドメイン・オブジェクトからデータを抽出し、表示用のコンポーネントに挿入する必要があります。また逆にユーザーが情報を変更した後には、そのデータをGUI コンポーネントから抽出し、ドメイン・モデルの中に設定し直す必要があります。この、行ったり来たりのプロセスは、syncBeanToComponents()メソッドと syncComponentsToBean() メソッドによって行われます。そして最後に、GUIコンポーネントへの参照は同期メソッドの中でアクセスできるように、オブジェクト・スコープの中で使える必要があります。
リスト 1. データ・バインディングを持たない Swing ダイアログ
package com.nfjs.examples;
import com.jgoodies.forms.layout.FormLayout;
import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.forms.builder.DefaultFormBuilder;
import javax.swing.*;
import java.awt.event.ActionEvent;
public class NoBindingExample {
private JFrame frame;
private JTextField firstField;
private JTextField lastField;
private JTextArea descriptionArea;
private FormBean bean;
public NoBindingExample() {
frame = new JFrame();
firstField = new JTextField();
lastField = new JTextField();
descriptionArea = new JTextArea(6, 6);
DefaultFormBuilder builder =
new DefaultFormBuilder(new FormLayout("r:p, 2dlu, f:p:g"));
builder.setDefaultDialogBorder();
builder.append("First:", firstField);
builder.append("Last:", lastField);
builder.appendRelatedComponentsGapRow();
builder.appendRow("p");
builder.add(new JLabel("Description:"),
new CellConstraints(1,
5, CellConstraints.RIGHT,
CellConstraints.TOP),
new JScrollPane(descriptionArea),
new CellConstraints(3,
5, CellConstraints.FILL,
CellConstraints.FILL));
builder.nextRow(2);
builder.append(new JButton(new MessageAction()));
frame.add(builder.getPanel());
frame.setSize(300, 300);
bean = new FormBean();
syncBeanToComponents();
}
private void syncBeanToComponents() {
firstField.setText(bean.getFirst());
lastField.setText(bean.getLast());
descriptionArea.setText(bean.getDescription());
}
private void syncComponentsToBean() {
bean.setFirst(firstField.getText());
bean.setLast(lastField.getText());
bean.setDescription(descriptionArea.getText());
}
public JFrame getFrame() {
return frame;
}
private class FormBean {
private String first;
private String last;
private String description;
public FormBean() {
this.first = "Scott";
this.last = "Delap";
this.description = "Description";
}
public String getFirst() {
return first;
}
public void setFirst(String first) {
this.first = first;
}
public String getLast() {
return last;
}
public void setLast(String last) {
this.last = last;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
private class MessageAction extends AbstractAction {
public MessageAction() {
super("Message");
}
public void actionPerformed(ActionEvent e) {
syncComponentsToBean();
JOptionPane.showMessageDialog(null, "First name is " + bean.getFirst());
}
}
public static void main(String[] args) {
NoBindingExample example = new NoBindingExample();
example.getFrame().show();
}
}
|
この例は単純なものですが、コンストラクターの中でのコンポーネント参照割り当てまで数えると、余分なコードが10 行もあります。もし新しいフィールドが bean に追加されると、GUI コンポーネントとドメイン・モデル間の両方向に対して、初期化と同期化のために3 行をさらに追加する必要があります。このコードを何度も何度も書くのは退屈であり、アプリケーションにバグが発生する元になりがちです。しかし幸いなことに、もっと良いソリューションがあるのです。
データ・バインディング・フレームワークを利用することによって、JavaBeanプロパティーと GUI コンポーネントとを、容易に「結合」することができます。通常、JavaBeanプロパティーはストリングによって参照され、このストリングが、JavaBean の対応するゲッターとセッターを探すようにデータ・バインディング・フレームワークに伝えます。例えば「first」は、対象のJavaBeanに getFirst() メソッドと setFirst() メソッドがあることを意味します。コンポーネントは、データによって自動的に初期化されます。コンポーネントの中の値が変化した場合には、そのコンポーネントに関連付けられたJavaBean プロパティーが変更されます。同様に、JavaBean はプロパティー変更リスナーをサポートしているため、対応するJavaBean プロパティーが変更されると、GUI コンポーネントは更新されます。
また最近の Java データ・バインディング・フレームワークでは、いつ変更の同期を行うかを設定することもできます(通常は、キーが押された時、マウスがクリックされた時、あるいはフォーカスが外れた時などです)。さらに、非常に多様なGUI コンポーネント (例えばテキスト・フィールド、チェック・ボックス、リスト、表など)がサポートされています。
リスト 2 は、リスト 1 のコードを、JGoodies データ・バインディング・フレームワークを使うように書き直したものです。
リスト 2. 同じ Swing ダイアログに JGoodies データ・バインディングを使う
package com.nfjs.examples;
import com.jgoodies.forms.layout.FormLayout;
import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.forms.builder.DefaultFormBuilder;
import com.jgoodies.binding.beans.BeanAdapter;
import com.jgoodies.binding.adapter.BasicComponentFactory;
import javax.swing.*;
import java.awt.event.ActionEvent;
public class BindingExample {
private JFrame frame;
private FormBean bean;
public BindingExample() {
frame = new JFrame();
bean = new FormBean();
BeanAdapter adapter = new BeanAdapter(bean);
JTextField firstField = BasicComponentFactory.createTextField(
adapter.getValueModel("first"));
JTextField lastField = BasicComponentFactory.createTextField(
adapter.getValueModel("last"));
JTextArea descriptionArea = BasicComponentFactory.createTextArea(
adapter.getValueModel("description"));
DefaultFormBuilder builder =
new DefaultFormBuilder(new FormLayout("r:p, 2dlu, f:p:g"));
builder.append("First:", firstField);
builder.append("Last:", lastField);
builder.appendRelatedComponentsGapRow();
builder.appendRow("p");
builder.add(new JLabel("Description:"),
new CellConstraints(1, 5,
CellConstraints.RIGHT,
CellConstraints.TOP),
new JScrollPane(descriptionArea),
new CellConstraints(3, 5,
CellConstraints.FILL,
CellConstraints.FILL));
builder.nextRow(2);
builder.append(new JButton(new MessageAction()));
frame.add(builder.getPanel());
frame.setSize(300, 300);
}
public JFrame getFrame() {
return frame;
}
public class FormBean {
//Same as above
}
private class MessageAction extends AbstractAction {
public MessageAction() {
super("Message");
}
public void actionPerformed(ActionEvent e) {
JOptionPane.showMessageDialog(null, "First name is " + bean.getFirst());
}
}
public static void main(String[] args) {
BindingExample example = new BindingExample();
example.getFrame().show();
}
}
|
実装の詳細については、すぐ後で説明します。とりあえず、最初の例と比べて何がなくなったかに注意して見ると、以下のことがわかります。
- コンポーネントへの参照を、単に同期用に公開する必要はありません。コンポーネントへの参照は、コンストラクターのスコープ外には伝播されません。
- どちらの同期メソッドもなくなっています。
- コンストラクターの中に、コンポーネントにデータを追加するための、最初の同期がありません。
- ダイアログを表示する前のアクションの中に同期がありません。
こうした項目がなくなっているにもかかわらず、この例は最初の例とまったく同じように動作するのです。
JGoodies データ・バインディング・フレームワークの全体を説明することは、この記事の範囲外です。しかし、その実装の詳細を見ることは無駄ではありません。リスト2 の例を見てください。以下の 2 行が、魔法のすべてを説明してくれます。
BeanAdapter adapter = new BeanAdapter(bean);
JTextField firstField = BasicComponentFactory.createTextField(adapter.getValueModel("first"));
最初の行は、BeanAdapter という JGoodies オブジェクトを作成します。このオブジェクトは、値モデル・オブジェクトを作成するために使われます。値モデルは、名前の詳細がわからなくてもJavaBean プロパティーにアクセスできるようにするための、一般的な方法を定義します。リスト3 は、ValueModel インターフェースの定義を示しています。
リスト 3. ValueModel インターフェース
public interface ValueModel {
java.lang.Object getValue();
void setValue(java.lang.Object object);
void addValueChangeListener(PropertyChangeListener propertyChangeListener);
void removeValueChangeListener(PropertyChangeListener propertyChangeListener);
}
|
BasicComponentFactory クラスは、提供される ValueModel に結合される Swingコンポーネントを作成するメソッドを含んでいます。2 行目は、BasicComponentFactoryを使って JTextField を作ります。この場合は、JTextField は FormBean の「first」プロパティーに結びつけられます。JGoodiesのデータ・バインディング API は、(FormBean のデータでテキスト・フィールドを初期化する動作のうちの)それ以外の初期化動作を行います。また、テキスト・フィールドに加えられたすべての変更と同期をとって、FormBeanに戻します。
こうした同期動作は、まだ煙と鏡を使った魔術で行われているように思えるかもしれません。しかし、そんなことはありません。最近のGUI コンポーネントは、その後ろにモデルを持っているものがほとんどです。データ・バインディング・フレームワークの仕事は、ドメイン・オブジェクトの中に保存された値を、そのモデルの中に入れることです。フレームワークは、次の2 つの方法を使ってこれを行います。
1 つの方法は、バインドされている bean フィールドを、コンポーネント自体のモデルにしてしまう方法です。この方法の場合、コンポーネントのビュー部分が値を取得、あるいは修正しようとするときには、直接bean の中の値にアクセスします。この方法は、JGoodies データ・バインディング・フレームワークでよく使われます。
図 1 は、bean が JTextComponent のモデルとなるように、JGoodies が DocumentAdapterクラスと PropertyAdapter クラスを使って bean を装飾している様子を示しています。
図 1. JGoodies がフィールドを JTextComponent Model に変える
モデルと GUI 値を同期化させるための、もう 1 つの方法は、相手側で値が変化した場合に、お互いの間でのゲッターとセッターの呼び出し動作を自動化することです。この方法は、SWTで JFace データ・バインディング・フレームワークを使う際に利用されています。
リスト 4 は、前と同じ例を、SWT と JFace データ・バインディングを使って書き直したものです。このフレームワークは、フィールド同士をつなぎ合わせるためにコンテキスト・オブジェクトを使うことが特徴です。テキスト・コントロールをFormBean フィールドに関連付けるために、3 つの context.bind() メソッド・コールが使われていることに注意してください。
リスト 4. 同じ Swing ダイアログに JFace データ・バインディングを使う
import org.eclipse.jface.examples.databinding.nestedselection.BindingFactory;
import org.eclipse.jface.internal.databinding.provisional.DataBindingContext;
import org.eclipse.jface.internal.databinding.provisional.description.Property;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.MessageBox;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
public class JFaceBindingExample {
private Shell shell;
private FormBean bean;
public void run() {
Display display = new Display();
shell = new Shell(display);
GridLayout gridLayout = new GridLayout();
gridLayout.numColumns = 2;
shell.setLayout(gridLayout);
bean = new FormBean();
DataBindingContext context = BindingFactory.createContext(shell);
Label label = new Label(shell, SWT.SHELL_TRIM);
label.setText("First:");
GridData gridData = new GridData(GridData.FILL_HORIZONTAL);
Text text = new Text(shell, SWT.BORDER);
text.setLayoutData(gridData);
context.bind(text, new Property(bean, "first"), null);
label = new Label(shell, SWT.NONE);
label.setText("Last:");
text = new Text(shell, SWT.BORDER);
context.bind(text, new Property(bean, "last"), null);
gridData = new GridData();
gridData.horizontalAlignment = SWT.CENTER;
gridData.horizontalSpan = 2;
Button button = new Button(shell, SWT.PUSH);
button.setLayoutData(gridData);
button.setText("Message");
button.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent e) {
MessageBox messageBox = new MessageBox(shell);
messageBox.setMessage("First name is " + bean.getFirst());
messageBox.open();
}
});
shell.pack();
shell.open();
while (!shell.isDisposed()) {
if (!display.readAndDispatch())
display.sleep();
}
display.dispose();
}
public static void main(String[] args) {
JFaceBindingExample example = new JFaceBindingExample();
example.run();
}
}
|
Java GUI API に使うための、有望なデータ・バインディング・フレームワークがいくつか登場しています。Swingアプリケーションで使うための主なオープンソース API の主導的プロジェクトとして、次のようなものがあります。
この一般的な JGoodies データ・バインディング API は、オープンソース・プロジェクトとして、数年前からJava.net で入手できるようになっています。これは Karsten Lentzsch によって書かれたものですが、KarstenLentzsch はまた、やはり一般的な JGoodies FormLayout も書いています。このフレームワークは最も歴史が長く、実稼働で使えるだけの安定性を持たせるために、多くの改版やバグ修正が行われてきています。
Spring の RCP (Rich Client Platform) プロジェクトは、よく知られた SpringApplication Framework のサブプロジェクトですが、Swing データ・バインディング・フレームワークも含んでいます。Spring RCP も JGoodies も、VisualWorks Smalltalk のデータ・バインディングの設計に影響を受けています。SpringRCP は、まだ V1.0 もリリースされていませんが、コード・ベースのデータ・バインディング部分は安定しており、多くの開発者に使われています。
Java.net の SwingLabs プロジェクトには、数年前から Swing データ・バインディング・フレームワークのための開発が含まれています。しかし、このプロジェクトでの作業は最近、SunMicrosystems をスポンサーとする、新たなデータ・バインディング JSR (JavaSpecification Request) に移管されています。
この JSR は、Sun の Scott Violet と、Ben Galbraith や Karsten Lentzschを含むエキスパート・グループ・メンバーを中心に最近作成され、デスクトップやサーバー環境で使うためのデータ・バインディング用の標準API を提供しようとしています。JSR 295 は、まだ当分は利用できそうにないため、今すぐにソリューションを必要とする人の選択肢にはなり得ません。
SWT で使えるオープンソースのデータ・バインディング API には、主に次の 2つがあります。
Jaysoft は、よく知られた JGoodies データ・バインディング API を SWT で使えるように移植しました。このコア・クラスは、実質的にJGoodies と同じです。Swing 専用のモデルは、SWT コントロールに適したものに置き換えられています。
Java データ・バインディングに最近登場した、もう 1 つが、JFace データ・バインディング・フレームワークです。EclipseV3.2 リリースには、この API の暫定版が含まれています。JFace データ・バインディングはSWTBinding や JGoodies フレームワークとは異なり、SWT と JFace 専用に、白紙の状態から新たに作られました。
アプリケーションでデータ・バインディング・フレームワークを使うと、同期の問題に対応できる以外にも有利な点がいくつかあります。自分で独自の同期コードを作る代わりに同じ同期コードを繰り返し使えるため、バグが少なくなります。もう1 つ、大きな利点は、アプリケーションをテストしやすくなることです。
よく知られたプレゼンテーション・モデル (「参考文献」を参照) では、アプリケーションの状態とビジネス・ロジックを、ビューの GUIコントロールとは別の、モデル・レイヤーに分離することを推奨しています。図2 に示すように、モデルの状態は頻繁にビューと同期されます。
図 2. プレゼンテーション・モデルを使った場合の関係
こうした設計を行うと、アプリケーションのすべてのビジネス・ロジックを、ビューをインスタンス化せずにテストすることができます。例えば、合計が100 を越えた場合にフォームで何らかのコントロールを行う、などがあります。「iftotal > 100」という実行可能条件があるのです。また、この条件の評価に基づいて状態を関連付けることもできます。
プレゼンテーション・モデル・パターンでは、この状態はプレゼンテーション・モデルの変数の中に設定され、ビューと同期してコントロール実行可能条件を変更します。その結果、ビューの中のGUI コンポーネントにアクセスせずに、ロジックをテストできるのです。
SWT やSwing では、GUI コンポーネントにアクセスし、いじることは困難なことが多いものです。プレゼンテーション・モデルは条件ロジックを含み、その条件を実行した結果起こる状態変化を保持する場所を持っているため、すべてのテストをプレゼンテーション・モデルに対して行うことができます。しかしこのパターンでは、プレゼンテーション・モデルとビューとの間で、データの変化をいつ、どのように同期させるのか、という問題があります。データ・バインディングが登場するまでは、その解決は困難でした。しかし今や、コントロールをプレゼンテーション・モデルのフィールドや関連のドメイン・オブジェクトに単純にバインドするだけですむのです。
アプリケーションの中でデータ・バインディング・フレームワークを使うことには、いくつかの欠点もあります。まず、バインディングという余分なレイヤーが追加されているため、コントロールとドメイン・オブジェクトとの間でのデータの流れを追いにくく、アプリケーションをデバッグしにくくなります。しかし自分が使うフレームワークの実装の詳細に慣れてくれば、それほど困難ではなくなるものです。
また、ストリングを使ってプロパティーが表現されているため、リファクタリングを行うとアプリケーションが壊れやすくなります。例えば、リスト5 に示すコード抜粋を考えてみてください。JFace データ・バインディング・フレームワークをgetFirst() / setFirst() プロパティーにバインドするために、「first」が使われています。getFirst() と setFirst() を getFirstName() と setFirstName() にリファクタリングするためには、このストリングを「firstName」に変更する必要があります。しかし現在のIDE のリファクタリング・ツールは、この変更には対応していません。
リスト 5. リファクタリングで対応できない領域
context.bind(text, new Property(bean, "first"), null);
. . .
private class FormBean {
private String first;
...
public FormBean() {
this.first = "Scott";
this.last = "Delap";
this.description = "Description";
}
public String getFirst() {
return first;
}
public void setFirst(String first) {
this.first = first;
}
. . .
} |
SWT の開発であれ Swing の開発であれ、プロジェクトの中にデータ・バインディング・フレームワークを取り入れることによって、様々な点で有利になります。ドメイン・モデル同期化用の決まりきったGUI コードを書いたり維持したりすることは、誰にとっても面倒なものです。この記事では、Javaデータ・バインディング・フレームワークを利用することによって、そうした作業から解放されることを紹介しました。しかも、適切なGUI 設計パターンと組み合わせれば、テストもしやすくなるという利点もあることを説明しました。
学ぶために
- ClientJava.com の記事、「How Many Data Binding Frameworks = A Bad Thing」を読んでください。
- Eclipse Foundation の wiki で JFace データ・バインディングについて学んでください。
- 新しい、JSR 295: Beans Binding を読んでみてください。
- Martin Fowler による、Presentation Model の解説を見てください。
- IBM developerWorks の Eclipse project resources を利用して、皆さんの Eclipse スキルを向上させてください。
- developerWorks に用意された、Eclipse に関する他の記事も読んでください。
-
developerWorks technical events and webcasts で最新情報を入手してください。
製品や技術を入手するために
-
JGoodies Binding プロジェクトについて調べてみてください。
- また、Spring Rich Client Project (RCP) についても調べてみてください。
-
SWTBinding をダウンロードしてください。
-
IBM alphaWorks に用意された、最新の Eclipse technology downloads を調べてみてください。
- 皆さんの次期オープンソース開発プロジェクトを IBM trial software を使って革新してください。ダウンロード、あるいは DVD で入手することができます。
議論するために
-
developerWorks blogs から developerWorks のコミュニティーに加わってください。
