Violet
2002 年、私はオブジェクト指向を使ったデザインとパターンに関する学部生向けの教科書 [Hor05] を執筆しました。多くの本と同様、この本を書いた理由は現在支配的なカリキュラムに不満があったからです。コンピューターサイエンスを学ぶ学生によくあるのは、最初のプログラミングの講義で単一クラスの設計方法を学んだら、その後は上級ソフトウェアエンジニアリングの講義までオブジェクト指向デザインの訓練を受けないという状況です。最初の講義では UML やデザインパターンを数週間でさっと触れるだけであり、学生には何となくの理解しか残りません。私が書いた本は Java プログラミングと基本的なデータ構造 (Java を使った CS1/CS2 講義に相当)の知識を持つ学生に向けた一セメスター分の講義で使えるようになっていて、馴染みある例を使ってオブジェクト指向のデザイン原則やデザインパターンを取り上げます。例えばデザインパターンの Decorator の紹介には Swing の JScrollPane
が使われ、よくある Java ストリームを使った例よりも分かりやすくなるよう工夫がしてあります。
この本には軽量な UML のサブセットが必要でした。クラス図、シーケンス図、そして Java オブジェクトの参照関係を示すオブジェクト図の一種 (図 22.1) が書ける必要があり、さらに学生が自分で図を書けることも求められました。しかし Rational Rose といった商用製品は高価な上に低速で習得が難しく [Shu05]、かといって当時あったオープンソースの代替品は機能が少なくてバグが多く、利用できませんでした1。特に ArgoUML のシーケンス図は完全に壊れていました。
こんなわけで、私は (a) 学生が使いやすくて (b) 拡張可能なフレームワークの例となるような一番単純なエディタであって、学生が理解・拡張しやすいものを作ることになりました。こうして生まれたのが Violet です。
Violet の紹介
Vliolet は軽量な UML エディタであり、対象ユーザーは簡単な UML 図を素早く作成したい学生・教師・本の著者です。習得と利用は非常に簡単で、クラス図、シーケンス図、状態図、オブジェクト図、ユースケース図が描画できます (他の種類の図も後になって追加されました)。Violet はオープンソースかつクロスプラットフォームのソフトウェアであり、Java 2D グラフィックス API を存分に利用した単純でありながらも柔軟性の高いグラフフレームワークがコアで使われています。
Violet のユーザーインターフェースは意図的に単純にしてあります。属性やメソッドを入力するのにダイアログをいくつも開く必要ありません。Violet ではそういった値をテキストフィールドに直接タイプするので、何回かクリックだけで綺麗で分かりやすい図がすぐに手に入ります。
Violet は本格的な UML プログラムになろうとはしていません。Violet が実装しない機能をいくつかあげます:
-
Violet は UML ダイアグラムからソースコードを生成したり、ソースコードから UML ダイアグラムを生成したりしない。
-
Violet は意味論的なモデルのチェックを行わない。つまり、Violet を使って矛盾のある図を作成できる。
-
Violet は他の UML ツールへインポートできるファイルを生成しない。他のツールのファイルを読めもしない。
-
Violet は図を自動的に配置する (「グリッドに合わせる」という簡単な機能ならある)。
(こういった制限を撤廃するのは学生が取り組むプロジェクトとしてちょうどよいでしょう。)
カクテルナプキン以上で本格 UML ツール以下な何かを求めていたデザイナーが何人か Violet に興味を持った時点で、私はコードを GNU General Public License の下で SourceForge に公開しました。2005 年からは Alexandre de Pellegrin もプロジェクトに加わり、Eclipse プラグインや優れた見た目のインターフェースが実装されました。彼はそれから膨大な回数のアーキテクチャの変更を行い、プロジェクトの主要メンテナとなっています。
本章では、Violet のオリジナルのアーキテクチャとその進化について議論します。グラフの編集について解説する部分もありますが、その他の部分で解説される JavaBeans のプロパティや persistence の利用、Java WebStart、プラグインアーキテクチャは多くの人が楽しめる内容であるはずです。
グラフフレームワーク
Violet は頂点と辺を自由な形に描画できるグラフ編集フレームワークをベースとしています。Violet UML エディタの頂点はクラス、オブジェクト、(シーケンス図の) アクティベーションバーを表し、辺は UML 図における様々な形の辺を表します。同じグラフフレームワークを使えば、エンティティ同士の関係図や路線図なども描画できるでしょう。
フレームワークの動作を説明するために、円形の頂点と直線の辺からなる非常に単純な白黒のグラフを考えます (図 22.2)。頂点と辺のプロトタイプオブジェクトを表す次の SimpleGraph
クラスは prototype パターンを使っています:
public class SimpleGraph extends AbstractGraph
{
public Node[] getNodePrototypes()
{
return new Node[]
{
new CircleNode(Color.BLACK),
new CircleNode(Color.WHITE)
};
}
public Edge[] getEdgePrototypes()
{
return new Edge[]
{
new LineEdge()
};
}
}
図 22.2 上部にある頂点ボタンと辺ボタンの作成にはプロトタイプオブジェクトが使われます。ユーザーが頂点または辺のインスタンスをグラフに追加すると、プロトタイプオブジェクトがクローンされます。Node
と Edge
はインターフェースであり、次のメソッドを持ちます:
-
両方のインターフェースは
getShape
メソッドを持つ。このメソッドは頂点あるいは辺の図形に対応する Java2D のShape2D
オブジェクトを返す。 -
Edge
インターフェースは辺の始点と終点の頂点を返すメソッドを持つ。 -
Node
インターフェースのgetConnectionPoint
メソッドは、頂点の境界上にある最良の接触点を計算する (図 22.3 参照)。 -
Edge
インターフェースのgetConnectionPoints
メソッドは、辺の二つの端点を返す。このメソッドは選択された辺を強調する「グラバー」を描画するのに使う。 -
頂点は子を持つことができる。子は親と一緒に移動する。子の列挙・管理のためのメソッドが多数用意されている。
AbstractNode
と AbstractEdge
は様々なメソッドを実装する便利クラスであり、RectangularNode
と SegmentedLineEdge
がそれぞれタイトル文字列の付いた長方形の頂点と線分の集まりの完全な実装を提供します。
今考えている単純なグラフエディタでは CircleNode
と LineEdge
という子クラスを作成し、draw
メソッドと contains
メソッド、そして頂点の境界を表す getConnectionPoint
メソッドを実装することになります。実際のコードを以下に、このプログラムのクラス図を (図 22.4) に示します (描画にはもちろん Violet を使います)。
public class CircleNode extends AbstractNode
{
public CircleNode(Color aColor)
{
size = DEFAULT_SIZE;
x = 0;
y = 0;
color = aColor;
}
public void draw(Graphics2D g2)
{
Ellipse2D circle = new Ellipse2D.Double(x, y, size, size);
Color oldColor = g2.getColor();
g2.setColor(color);
g2.fill(circle);
g2.setColor(oldColor);
g2.draw(circle);
}
public boolean contains(Point2D p)
{
Ellipse2D circle = new Ellipse2D.Double(x, y, size, size);
return circle.contains(p);
}
public Point2D getConnectionPoint(Point2D other)
{
double centerX = x + size / 2;
double centerY = y + size / 2;
double dx = other.getX() - centerX;
double dy = other.getY() - centerY;
double distance = Math.sqrt(dx * dx + dy * dy);
if (distance == 0) return other;
else return new Point2D.Double(
centerX + dx * (size / 2) / distance,
centerY + dy * (size / 2) / distance);
}
private double x, y, size, color;
private static final int DEFAULT_SIZE = 20;
}
public class LineEdge extends AbstractEdge
{
public void draw(Graphics2D g2)
{ g2.draw(getConnectionPoints()); }
public boolean contains(Point2D aPoint)
{
final double MAX_DIST = 2;
return getConnectionPoints().ptSegDist(aPoint) < MAX_DIST;
}
}
まとめると、Violet はグラフエディタ用の単純なフレームワークを提供します。エディタのインスタンスを取得するには、頂点と辺に対するクラスを定義し、頂点と辺オブジェクトのプロトタイプを生成するメソッドをグラフクラスに提供します。
もちろんグラフフレームワークは他にもあります。例えば JGraph[Ald02] や JUNG2 などです。ただこういったフレームワークは非常に複雑であり、さらに提供されるのはグラフ描画のフレームワークであってグラフ描画アプリケーションのフレームワークではありません。
JavaBeans のプロパティ
JavaBeans の仕様はクライアントサイド Java の黄金時代に作成されました。その目的はビジュアル GUI ビルダー環境における GUI コンポーネントの編集に関するポータブルなメカニズムの提供です。サードパーティの GUI コンポーネントを任意の GUI ビルダーにドロップできて、さらに標準のボタンやテキストコンポーネントと同じ方法でプロパティを設定できることが目標とされました。
Java はプロパティという仕組みをネイティブには持ちません。その代わり JavaBeans のプロパティが提供する getter と setter というメソッドの組や BeanInfo クラスを利用します。さらに、プロパティエディタを使ってプロパティの値を視覚的に編集することもできます。JDK ではプロパティエディタさえも提供されます。java.awt.Color
がその例です。
Violet フレームワークは JavaBeans という仕様をフル活用します。例えば CircleNode
クラスは次の二つのメソッドを提供するだけで color
プロパティを公開できます:
public void setColor(Color newValue)
public Color getColor()
これだけで全ての準備が完了し、グラフエディタを使って円頂点の色を編集できるようになります (図 22.5)。
Long-Term Persistence
他のエディタプログラムと同様に、Violet はユーザーが作ったものをファイルに保存し、後で読み込む必要があります。UML モデルのやり取りの共通フォーマットとして設計された XMI という仕様3があります。私も目を通したのですが、巨大で分かりにくく、実装が難しいと感じました。こう感じたのが私だけだとは思いません ――XMI はごく単純なモデルさえ互換性を保てないと言われていました [PGL+05]。
Java のシリアライズを使うという単純な方法も考えましたが、この方法だとどこかのタイミングで実装が変更された場合に古いバージョンのオブジェクトを読み込むのが難しくなってしまいます。JavaBeans の技術者も同じ問題に直面しており、彼らはオブジェクトの永続保存のための標準 XML フォーマット4を策定しています。このフォーマットにおいて Java オブジェクト (Violet では UML 図) はそれを生成および変更する文の列としてシリアライズされます。例を次に示します:
<?xml version="1.0" encoding="UTF-8"?>
<java version="1.0" class="java.beans.XMLDecoder">
<object class="com.horstmann.violet.ClassDiagramGraph">
<void method="addNode">
<object id="ClassNode0" class="com.horstmann.violet.ClassNode">
<void property="name">...</void>
</object>
<object class="java.awt.geom.Point2D$Double">
<double>200.0</double>
<double>60.0</double>
</object>
</void>
<void method="addNode">
<object id="ClassNode1" class="com.horstmann.violet.ClassNode">
<void property="name">...</void>
</object>
<object class="java.awt.geom.Point2D$Double">
<double>200.0</double>
<double>210.0</double>
</object>
</void>
<void method="connect">
<object class="com.horstmann.violet.ClassRelationshipEdge">
<void property="endArrowHead">
<object class="com.horstmann.violet.ArrowHead" field="TRIANGLE"/>
</void>
</object>
<object idref="ClassNode0"/>
<object idref="ClassNode1"/>
</void>
</object>
</java>
XMLDecoder
クラスがこのファイルを読み、指定される文を実行します (パッケージの名前は省略してあります)。
ClassDiagramGraph obj1 = new ClassDiagramGraph();
ClassNode ClassNode0 = new ClassNode();
ClassNode0.setName(...);
obj1.addNode(ClassNode0, new Point2D.Double(200, 60));
ClassNode ClassNode1 = new ClassNode();
ClassNode1.setName(...);
obj1.addNode(ClassNode1, new Point2D.Double(200, 60));
ClassRelationShipEdge obj2 = new ClassRelationShipEdge();
obj2.setEndArrowHead(ArrowHead.TRIANGLE);
obj1.connect(obj2, ClassNode0, ClassNode1);
コンストラクタ、プロパティ、メソッドのセマンティクスが変わらない限り、新しいバージョンのプログラムからも古いバージョンが作ったファイルを読めます。
こういったファイルの生成は簡単に行えます。エンコーダーは各オブジェクトのプロパティを全て調べ、デフォルトと異なる値を持つプロパティに対して値を設定する setter を呼び出す文を書き込みます。基本的なデータ型は Java プラットフォームが処理しますが、Point2D
, Line2D
, Rectangle2D
についてはハンドラを書く必要がありました。最も重要なこととして、エンコーダーはグラフが addNode
と connect
というメソッドの列としてシリアライズできることを知っておく必要があります。
encoder.setPersistenceDelegate(Graph.class, new DefaultPersistenceDelegate()
{
protected void initialize(Class<?> type, Object oldInstance,
Object newInstance, Encoder out)
{
super.initialize(type, oldInstance, newInstance, out);
AbstractGraph g = (AbstractGraph) oldInstance;
for (Node n : g.getNodes())
out.writeStatement(new Statement(oldInstance, "addNode", new Object[]
{
n,
n.getLocation()
}));
for (Edge e : g.getEdges())
out.writeStatement(new Statement(oldInstance, "connect", new Object[]
{
e, e.getStart(), e.getEnd()
}));
}
});
エンコーダーを設定しておけば、グラフの保存はごく単純です:
encoder.writeObject(graph);
デコーダーは文を実行するだけなので、設定は必要ありません。グラフは次のように簡単に読み込めます:
Graph graph = (Graph) decoder.readObject();
このアプローチは Violet が何度もバージョンを変更する中でも非常に上手く働きました。ただ一つ例外として、最近になって行ったリファクタリングで一部のパッケージの名前を変更したので、このときには後方互換性が失われました。このとき古いクラスをオリジナルのパッケージに残しつつパッケージの構造を変更するのも一つの手ですが、ここでは古いファイルを読んだときにパッケージの名前を変更する XML 変換プログラムがメンテナから提供されました。
Java WebStart
Java WebStart はアプリケーションをウェブブラウザから起動する技術です。開発者はウェブブラウザで実行されるヘルパーアプリケーションに JNLP ファイルを post し、Java プログラムをダウンロード・実行させます。アプリケーションに対するデジタル署名が可能であり、その場合にはユーザーが証明書を受理します。そうでなくて署名が無い場合には、アプリケーションの実行はアプレットのサンドボックスよりも制限が多少緩いサンドボックスで行われます。
エンドユーザーがデジタル署名の有効性を判断できると信用してよい、とは私は思いません。Java プラットフォームの強みの一つはセキュリティであり、その強みを生かすのが重要であると考えます。
Java WebStart のサンドボックスは十分にパワフルであり、ユーザーはファイルの入出力や印刷といった便利な処理を実行できます。こういった操作はユーザーから見てセキュアで使い勝手が良いように処理されます。例えば、アプリケーションがローカルのファイルシステムにアクセスしたときにはユーザーに警告が表示され、ファイルの入出力を行うかどうかを選択できます。その後アプリケーションにはファイルがストリームオブジェクトとして渡され、ファイル選択時にファイルシステムを覗き見る隙は与えられません。
WebStart で実行されるときには FileOpenService
や FileSaveService
と対話するのに専用のコードが必要になるというのは開発者にとって不便であり、WebStart にアプリケーションが WebStart から起動されたかどうかを取得する API が存在しないのはさらに不便です。
同様に、ユーザー設定には二つの実装が必要になります: アプリケーションが通常通り起動したときに使う Java のプリファレンス API による実装と、WebStart で実行されたときに使う WebStart のプリファレンスサービスを使った実装です。一方で印刷機能はアプリケーションプログラマーに対して完全に透過的になっています。
Violet はこういったサービスに対する簡単な抽象レイヤーを提供し、アプリケーションプログラマーの仕事を大幅に単純化します。例えば、ファイルを開くときには次のようにします:
FileService service = FileService.getInstance(initialDirectory);
// WebStart で実行されているかどうかを自動的に検出する
FileService.Open open = fileService.open(defaultDirectory, defaultName, extensionFilter);
InputStream in = open.getInputStream();
String title = open.getName();
FileService.Open
インターフェースは JFileChooser
のラッパーと JNLP FileOpenService
という二つのクラスで実装されます。
JNLP API にはこういった便利な機能を持ちません。そのためこの API はほとんど注目されず、ほぼ無視されてきました。大半のプロジェクトは WebStart アプリケーションに自己署名証明書を付けており、ユーザーのセキュリティは存在しないも同然です。これは残念なことです ――オープンソース開発者は JNLP サンドボックスを受け入れて、リスクフリーにプロジェクトを試せるようにするべきです。
Java 2D
Violet は Java2D ライブラリというあまり知られていない優れた Java API の一つを徹底的に利用しています。全ての頂点と辺は getShape
メソッドを持ち、このメソッドが Java2D のシェイプに対する共通インターフェース java.awt.Shape
を返します。このインターフェースは長方形、円、パス、およびこれらの和、交差、差分について実装されます。例えば自由曲線や二次/三字曲線からなるシェイプ (真っ直ぐな矢印や曲がった矢印など) の作成には GeneralPath
クラスが使われます。
Java2D API の柔軟性を見るために、AbstractNode.draw
メソッドを使って影を描画する次のコードを考えます:
Shape shape = getShape();
if (shape == null) return;
g2.translate(SHADOW_GAP, SHADOW_GAP);
g2.setColor(SHADOW_COLOR);
g2.fill(shape);
g2.translate(-SHADOW_GAP, -SHADOW_GAP);
g2.setColor(BACKGROUND_COLOR);
g2.fill(shape);
これだけのコードで全てのシェイプに対して影を落とすことができ、開発者が後から追加したシェイプに対しても影が作られます。
もちろん Violet にはビットマップ画像を保存する機能があり、javax.imageio
パッケージがサポートする GIF, PNG, JPEG といったフォーマットが利用可能です。出版社がベクタ画像を求めたとき、私は Java 2D ライブラリの利点をもう一つ発見しました。PostScript に印刷すると Java2D の操作が PostScript のベクタ描画操作に変換されるのですが、こうして印刷されたファイルを ps2eps
といったプログラムに渡せば、Adobe Illustrator や Inkscape にインポートできるのです。Swing コンポーネント comp
の paintComponent
を使ってグラフを描画するコードを示します:
DocFlavor flavor = DocFlavor.SERVICE_FORMATTED.PRINTABLE;
String mimeType = "application/postscript";
StreamPrintServiceFactory[] factories;
StreamPrintServiceFactory.lookupStreamPrintServiceFactories(flavor, mimeType);
FileOutputStream out = new FileOutputStream(fileName);
PrintService service = factories[0].getPrintService(out);
SimpleDoc doc = new SimpleDoc(new Printable() {
public int print(Graphics g, PageFormat pf, int page) {
if (page >= 1) return Printable.NO_SUCH_PAGE;
else {
double sf1 = pf.getImageableWidth() / (comp.getWidth() + 1);
double sf2 = pf.getImageableHeight() / (comp.getHeight() + 1);
double s = Math.min(sf1, sf2);
Graphics2D g2 = (Graphics2D) g;
g2.translate((pf.getWidth() - pf.getImageableWidth()) / 2,
(pf.getHeight() - pf.getImageableHeight()) / 2);
g2.scale(s, s);
comp.paint(g);
return Printable.PAGE_EXISTS;
}
}
}, flavor, null);
DocPrintJob job = service.createPrintJob();
PrintRequestAttributeSet attributes = new HashPrintRequestAttributeSet();
job.print(doc, attributes);
一般的なシェイプを使ってはパフォーマンスが悪化するのではないかと当初私は思っていましたが、そんなことはありませんでした。クリッピングが上手く働いて、現在のビューポートについて更新が必要なシェイプ操作だけが実行されるためです。
Swing アプリケーションフレームワークを使わない
たいていの GUI フレームワークでは、アプリケーションがメニュー、ツールバー、ステータスバーを管理します。しかし Java API にはこれらがありません。JSR 2965 は Swing アプリケーションの基礎的なフレームワークとなる予定でしたが、現在アクティブではありません。そのため Swing アプリケーションの開発者は選択を迫られます: 結構な数の車輪を自分で再発明するか、サードパーティのライブラリを使うかです。Violet が書かれたころアプリケーションフレームワークの選択肢には Eclipse や NetBeans プラットフォームがありましたが、どちらも大きすぎるように感じました (現在であれば JSR 296 のフォークの GUTS6 などが選択肢に入るでしょう)。そのため Violet ではメニューや内部フレームを処理する仕組みを再発明しています。
Violet ではメニュー項目をプロパティファイルで指定します:
file.save.text=Save
file.save.mnemonic=S
file.save.accelerator=ctrl S
file.save.icon=/icons/16x16/save.png
このプロパティの接頭部 (file.save
) からユーティリティ関数を使ってメニュー項目が作られます。.text
や .mnemonic
といった接尾部は「設定よりも規約を (convention over configuration)」という考え方に沿っています。こういった設定をリソースファイルで記述するとローカライズが簡単になるので、API を呼び出してメニューを作成するよりも優れていると言えます。私はこの仕組みを GridWorld7 と呼ばれるオープンソースな高校生向けコンピューターサイエンス教育環境プロジェクトでも使いました
Violet は他のアプリケーションと同様、グラフを含んだ「ドキュメント」をいくつも開くことができます。Violet が最初に書かれたときには multiple document interface (MDI) がまだ広く使われていました。MDI ではメインフレームがメニューバーを持ち、それぞれのドキュメントのビューは内部フレームに描画されます。このビューにはタイトルはありますがメニューバーはありません。内部フレームはメインフレームに収まり、ユーザーからはリサイズや最小化を行えます。またウィンドウを重ねて表示したり、タイル状に並べることも可能です。
MDI は開発者から嫌われたので、このスタイルのユーザーインターフェースは廃れていきました。それから一時は single document interface (SDI) が優れているとされたこともありました。SDI ではアプリケーションが複数のトップレベルフレームに表示され、優れているとされたのはおそらくホスト OS の標準的なウィンドウ管理ツールを使ってフレームが生成されたからです。しかしトップレベルのウィンドウをいくつも持っても得るものがたいして無いことが明らかになると、タブインターフェースが登場しました。タブインターフェースも複数のウィンドウが単一のフレームに含まれる方式ですが、一つのフレームが全体に表示され、ドキュメントはタブから選べるようになっています。こうするとドキュメントを横に並べて見比べることができませんが、最後に生き残ったのはこのインターフェースであるようです。
Violet の最初のバージョンは MDI インターフェースでした。この Java API は内部フレームの機能を持ちますが、ウィンドウを重ねたり並べたりする機能は自分で追加する必要がありました。その後 Alexandre がタブインターフェースに変更しました。このインターフェースは Java API のサポートが充実しているようです。ドキュメントを表示する際のポリシーが開発者に提示されていて、ユーザーからも選択できるようなアプリケーションフレームワークが望まれます。
Alexandre は他にもサイドバー、ステータスバー、ウェルカムパネル、スプラッシュスクリーンを追加しました。理想的にはこういったものも Swing のアプリケーションフレームワークに含まれているべきでしょう。
Undo/Redo
一度以上の undo/redo の実装はとても難しく思えますが、Swing の undo パッケージ ([Top00], 第九章) がアーキテクチャの指針となります。UndoManager
が UndoableEdit
オブジェクトのスタックを管理し、このオブジェクトの undo
メソッドが対応する編集操作を打ち消し、redo
メソッドが打ち消し操作を打ち消します (つまり元の操作をもう一度行います)。CompoundEdit
は UndoableEdit
の列であり、undo や redo を一度に行う必要がある複数の操作を表します。小さくてアトミックな編集操作 (グラフであれば辺や頂点を一つ追加・削除するなど) を定義し、必要に応じて編集をまとめる (compound する) のが良いとされます。
ここで難しいのが、簡単に undo できるアトミックな操作の小さな集合を定義する部分です。Violet ではこの集合は次の操作からなります:
-
一つの頂点または辺の追加・削除
-
頂点の子の接続/切り離し
-
頂点の移動
-
頂点または辺のプロパティの変更
これらの操作に対する undo は明らかです。例えば頂点の追加の undo はその頂点の削除であり、頂点の移動の undo は逆方向の移動です。
ここで注目してほしいのですが、ユーザーインターフェースで見える処理、あるいはユーザーインターフェースの操作が開始する Graph
インターフェースのメソッドというのは上記のアトミックな操作と同じではありません。例えば 図 22.6 に示すシーケンス図で、ユーザーが右のアクティベーションバーから左のライフラインにマウスをドラッグしたとします。そしてユーザーがマウスボタンを離すと、次のメソッドが呼ばれます:
public boolean addEdgeAtPoints(Edge e, Point2D p1, Point2D p2)
このメソッドによって辺が追加されますが、受け取った Edge
や Code
のサブクラスに応じて他の操作も行れるので、この場合には右のライフラインにアクティベーションバーが追加されます。そのためこの操作を undo するには、このアクティベーションバーも削除する必要があります。そのため モデル (今の場合グラフ) が undo すべき構造的変更を記録しなければなりません。コントローラーの操作を収集するだけでは不十分です。
Swing の undo パッケージから分かるのは、グラフ・頂点・辺のクラスは構造的な編集が起こるたびに UndoableEditEvent
の通知を UndoManager
に送信するべきであるということです。Violet はこれよりも一般的な設計を持っており、グラフ自身が次のインターフェースを持つリスナーを管理します:
public interface GraphModificationListener
{
void nodeAdded(Graph g, Node n);
void nodeRemoved(Graph g, Node n);
void nodeMoved(Graph g, Node n, double dx, double dy);
void childAttached(Graph g, int index, Node p, Node c);
void childDetached(Graph g, int index, Node p, Node c);
void edgeAdded(Graph g, Edge e);
void edgeRemoved(Graph g, Edge e);
void propertyChangedOnNodeOrEdge(Graph g, PropertyChangeEvent event);
}
フレームワークは各グラフへこのリスナーをインストールし、このリスナーが undo マネージャへの橋渡しを行います。このように undo のサポートのために一般的なリスナーを登場させるのはやりすぎかもしれません ――グラフ操作が直接 undo マネージャと対話することもできるからです。しかし、協調編集という実験的な機能をサポートが欲しかったのでこうしました。
undo と redo を自分のアプリケーションでサポートしたいのなら、そのモデルにおけるアトミックな操作が何かをよく考えるようにしましょう (考えるのはユーザーインターフェースの操作ではありません)。モデルで構造的な変更が起きたらイベントを発火し、Swing の undo マネージャにイベントを記録・収集させるのです。
プラグインアーキテクチャ
2D グラフィックスを知っているプログラマーであれば、Violet に新しい種類の図を追加するのは難しくありません。例えばアクティビティ図はサードパーティによって追加されました。路線図や ER 図が必要になったときには、Visio や Dia に手を出すよりも Violet の拡張機能を作成する方が速く済みました (実装にはそれぞれ一日もかかりませんでした)。
こういった拡張機能の実装には Violet フレームワーク全体に関する知識が必要とされず、グラフ、頂点、辺のインターフェースに関する知識と簡単な実装だけが求められます。コントリビューターをフレームワークの進化から切り離すために、単純なプラグインアーキテクチャが設計されました。
もちろん、プラグインアーキテクチャを持つプログラムはたくさんあり、その多くはとても複雑です。Violet も OSGi をサポートしてはどうかと言われたとき私はゾッとして、代わりにもっとも単純な実装を行いました。
コントリビューターはグラフ、頂点、辺の実装を JAR ファイルにして、それを plugins
ディレクトリに配置するだけです。Violet は起動時に Java の ServiceLoader
を使ってプラグインをロードします。ServiceLoader
は JDBC ドライバなどのサービスをロードするように設計されており、事前に設定したインターフェース (つまり Graph
インターフェース) クラスを提供する JAR ファイルを読み込みます。
プラグインの JAR ファイルにはサブディレクトリ META-INF/services
があり、ここにあるファイル (例えば com.horstmann.violet.Graph
) には同じ名前のインターフェースが実装する全クラスの名前が一行ごとに書かれています。ServiceLoader
はプラグインディレクトリからクラスローダーを作成し、全てのプラグインをロードします:
ServiceLoader<Graph> graphLoader = ServiceLoader.load(Graph.class, classLoader);
for (Graph g : graphLoader) // ServiceLoader<Graph> は Iterable<Graph> を実装する
registerGraph(g);
これは単純で便利な Java の標準機能であり、あなたのプロジェクトでもきっと役に立つでしょう。
最後に
多くのオープンソースプロジェクトと同じように、Violet はできないこと ――簡単な UML 図を素早く書くこと―― を行うために生まれました。Violet は Java SE プラットフォームの驚くほど幅広い機能によって可能になっており、このプラットフォームに含まれる多様なテクノロジを利用しています。この記事では Violet が Java Beans, Lont-Term Persistence, Java Web Start, Java 2D, Swing Undo/Redo そしてサービスローダーを使っていることを紹介しました。これらのテクノロジは Java や Swing の基礎ほどには理解されないこともありますが、デスクトップアプリケーションのアーキテクチャを格段に単純化してくれます。最初たった一人の開発者だった私は、空いた時間を数か月費やすだけで完全なアプリケーションを作成できました。さらにこういった標準化された仕組みには、他の人が Violet を改善したり一部分を自身のプロジェクトに利用するのが簡単になるという利点もあります。
-
当時の私は Spinellis による素晴らしい UMLGraph [Spi03] を知りませんでした。このプログラムでは一般的なポイントアンドクリックのインターフェースではなく宣言的なテキストによって図が指定されます。[return]
-
http://www.omg.org/technology/documents/formal/xmi.htm[return]