> 作者:[Cay Horstmann](http://www.aosabook.org/en/intro1.html#horstmann-cay)
> 譯者:[Xiao Jia](http://xiao-jia.com/)(賈梟)
2002 年,我寫了一本關于面向對象設計與模式的本科教材 [[Hor05](http://www.aosabook.org/en/bib1.html#bib%3ahorstmann%3aoodp)]。和很多書一樣,這本書也源于我對經典課程的沮喪。一般來說,計算機科學專業的學生,會在他們的第一門編程課上,學習如何設計一個獨立的類。而此后,直到在高年級的軟件工程課中,他們才在面向對象設計方面接受更多的訓練。在這門課程中,學生在幾個星期內匆忙地學習 UML 和設計模式,最終也只是走馬觀花。我寫的這本書是為一個學期的課程準備的,學生需要具備一些 Java 編程和基本數據結構的知識(通常這些知識來自基于 Java 的 CS1 或 CS2 課程安排)。這本書在學生所熟悉的上下文中涵蓋了面向對象設計原則和設計模式的內容。比如用 Swing 里面的?`JScrollPane`?類來介紹修飾模式(Decorator Pattern),目的是希望這個例子比經典的 Java 流的例子①更容易讓人記住。
> ① 譯者注:如?`FileInputStream`?和?`BufferedInputStream`?等。

圖 22.1:Violet 里的對象圖(Object Diagram)
在這本書里,我需要一種輕量級的 UML,包括類圖、順序圖,以及能夠顯示出 Java 對象引用的一種對象圖(參見圖 22.1)。我還想讓學生能夠繪制他們自己的UML圖。然而,像 Rational Rose 這樣的商業軟件,不僅價格昂貴,還難以學習 [[Shu05](http://www.aosabook.org/en/bib1.html#bib%3ashumba%3aratrose)]。而在當時可用的開源替代品,則功能有限并且缺陷很多?。比如需要使用文字來描述 UML 圖,而不是使用更常見的點擊式的界面。特別是 ArgoUML 軟件中的順序圖,根本無法使用。
> ? 當時我還不知道 Diomidis Spinellis 的令人欽佩的 UMLGraph 程序 [[Spi03](http://www.aosabook.org/en/bib1.html#bib%3aspinellis%3aumlgraph)]
于是我決定自己嘗試去實現一個最簡單的 UML 編輯器,它一要對學生有用,二要是一個可擴展的框架,便于學生理解和修改。就這樣,Violet 誕生了。
## 22.1\. 初識 Violet
Violet 是一個輕量級的 UML 編輯器,適用于學生、教師,以及需要快速創建簡單 UML 圖的作者。它非常易于學習和使用。你可以用它繪制類圖、順序圖、狀態圖、對象圖和用例圖。(至今,其他類型的 UML 圖也已經有人貢獻代碼,實現了它們。)它是一個開放源代碼并且跨平臺的軟件。Violet 的核心使用了一種簡單而靈活的圖形框架,該框架充分利用了 Java 2D 圖形接口的優勢。
Violet 的用戶界面被故意設計得很簡單。你不需要通過一系列單調乏味的對話框來輸入屬性和方法。相反,你只需要把它們輸入到一個文本框中。只要點幾下鼠標,你就能快速地創建出既吸引人又實用的 UML 圖。
Violet 并不嘗試去成為一個工業級的 UML 程序。下面是一些 Violet 不具備的特性:
* Violet 不能從 UML 圖生成代碼,也不能從代碼生成 UML 圖;
* Violet 不會對模型進行語義檢查,所以你可以繪制出自相矛盾的 UML 圖;
* Violet 生成的文件不能在其他UML工具中導入,它也不能讀取其他工具產生的模型文件;
* 除了一些簡單的功能如“自動對齊到網格”,Violet 不提供 UML 圖的自動布局功能。
(嘗試列出一些這樣的局限性,對學生項目會很有幫助。)
Violet 后來發展出一個由設計者構成的用戶群體,他們想要一個比較正規的工具,但又不要像工業級的 UML 工具那么重量級。這時,我在 SourceForge 上以 GNU GPL 協議發布了代碼。從 2005 年開始,Alexandre de Pellegrin 加入了這個項目,并提供了一個 Eclipse 插件和一個更加好看的用戶界面。從那時起,他就開始進行很多架構上的改動。現在,他是這個項目的主要維護者。
在這篇文章里,我會討論在 Violet 原本架構中的一些選擇,以及它的演變。這篇文章會有一部分的重點是在圖形編輯上,但其他幾部分,比如 JavaBeans 屬性的使用、持久化、Java WebStart、插件架構,這些應該都是大家所普遍感興趣的。
## 22.2\. 圖形框架
Violet 基于一個通用的圖形編輯框架,這個框架能夠渲染和編輯任意形狀的節點和邊。Violet UML 編輯器把類、對象、(順序圖中的)方法調用框(activation bar)等對應為節點,而把 UML 圖中的各種線條形狀對應為邊。這個圖形框架的另一實例則可以顯示實體關系圖(ER 圖)或鐵路圖②。
> ② 譯者注:railroad diagram,又稱語法圖(syntax diagram),是形式文法的一種圖形化表示方式。

圖 22.2:該圖形編輯框架的一個簡單實例
為了更好地解釋這個框架,我們來考慮一個非常簡單的圖形編輯器,它包括黑色和白色的圓圈節點,以及直線邊(參見圖 22.2)。下面的?`SimpleGraph`?類定義了節點和邊的原型對象(prototype objects),解釋了什么是原型模式:
~~~
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`?方法,用來返回一個 Java 2D 的?`Shape`?對象(分別是節點和邊的形狀)。
* `Edge`?接口具有用來在邊的兩端產生節點的方法。
* `Node`?接口中的?`getConnectionPoint`?方法負責計算出在節點邊界上的一個最優的附著點(參見圖 22.3)。
* `Edge`?接口中的?`getConnectionPoints`?方法負責產生這條邊的兩個端點。繪制當前被選中的邊的兩個可以拖動的端點的時候需要這個方法。
* 一個節點可以有一些跟隨其自身移動的子節點。有很多方法就是用來枚舉和管理這些子節點的。

圖 22.3:在節點的邊界上找一個連接點
輔助類?`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;
}
}
~~~

圖 22.4:簡單圖形編輯器的類圖
總的來說,Violet 為編寫圖形編輯器提供了一個簡單的框架。通過定義節點和邊所對應的類,并在圖形類中提供產生節點和邊的原型對象的方法,就可以得到一個編輯器的實例。
當然,還有其他圖形框架可以使用,比如 JGraph [[Ald02](http://www.aosabook.org/en/bib1.html#bib%3aalder%3ajgraph)] 和 JUNG ?。然而,這些框架都相當復雜,提供的也只是“用來繪制圖形”的框架,而不是“用來繪制圖形的應用程序”的框架。
> ??[http://jung.sourceforge.net](http://jung.sourceforge.net/)
## 22.3\. JavaBeans 屬性的使用
在客戶端 Java 的鼎盛時期,人們制定了 JavaBeans 規范,用來給在可視化 GUI 設計環境里編輯 GUI 組件提供可移植的機制。其目的是為了讓一個第三方的 GUI 組件可以放在任意的 GUI 設計器中,并且它的屬性可以像按鈕、文本等標準組件一樣進行設置。
Java 語言本身沒有對屬性的原生支持。JavaBeans 屬性可以從成對的 getter 和 setter 方法中發現出來,或者通過相應的?`BeanInfo`?類指定。進一步地,可以指定?*屬性編輯器*?來可視化地編輯屬性的值。JDK 甚至包含了一些基本的屬性編輯器,比如用來編輯?`java.awt.Color`?類型的編輯器。
Violet 框架充分利用了 JavaBeans 規范。比如,`CircleNode`?類可以通過提供如下兩個方法,來暴露出顏色這一屬性:
~~~
public void setColor(Color newValue)
public Color getColor()
~~~
現在,不需要任何額外的工作,這個圖形編輯器就能編輯圓圈節點的顏色了(參見圖 22.5)。

圖 22.5:使用默認的 JavaBeans 顏色編輯器來編輯圓圈節點的顏色
## 22.4\. 長期的持久化
和任何編輯器一樣,Violet 需要將用戶創建的 UML 圖保存到文件中,并在之后重新載入進來。人們設計了 XMI 標準?,作為 UML 模型的一種公共交換格式。我在看過 XMI 標準后,覺得它非常蹩腳,難以理解和使用。我想我并非唯一有這種感受的人——XMI 以極差的互操作性出名,即使對于最簡單的模型也是如此 [[PGL+05](http://www.aosabook.org/en/bib1.html#bib%3apersson%3aosstools)]。
> ??[http://www.omg.org/technology/documents/formal/xmi.htm](http://www.omg.org/technology/documents/formal/xmi.htm)
我曾考慮過直接使用 Java 的序列化(serialization)功能,但在實現經常有改動的情況下,讀取舊版本的序列化后的對象非常困難。JavaBeans 的架構師們同樣預見到了這一問題,于是他們為長期的持久化開發了一套標準的 XML 格式?。一個 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>
~~~
> ??[http://jcp.org/en/jsr/detail?id=57](http://jcp.org/en/jsr/detail?id=57)
當?`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);
~~~
只要這些構造函數、屬性和方法的語義沒有改變,新版本的程序就能夠讀取舊版本程序生成的文件。
生成這樣的文件非常直觀。編碼器(encoder)自動地枚舉每個對象的屬性,對于那些不同于默認值的屬性,編碼器會為這些屬性輸出設置屬性值的語句。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);
~~~
因為解碼器(decoder)只是簡單地執行語句,所以不需要額外的配置。可以像下面這樣讀取一個圖形:
~~~
Graph graph = (Graph) decoder.readObject();
~~~
在 Violet 經過了許許多多版本的過程中,這個方法一直工作得相當好。但是有一次例外:最近的一次重構改變了一些包的名字,因此破壞了向后兼容性。一種選擇是,仍然保留舊包中的那些類,即使它們和新的包結構已經不再匹配。然而,維護者沒有這樣做,而是提供了一個 XML 轉換器,用來在讀取舊版本文件的時候改寫包名。
## 22.5\. Java WebStart
Java WebStart 是用來在網頁瀏覽器中啟動應用程序的一門技術。部署者發布一個 JNLP 文件③,該文件在瀏覽器中會觸發一個輔助程序,該程序會下載并運行相應的 Java 應用程序。應用程序可以是數字簽名過的,此時用戶必須同意接受其證書;或者程序是未簽名的,此時它只能運行在一個比 applet 沙盒權限稍高的沙盒環境中。
> ③ 譯者注:JNLP 即 Java Network Launching Protocol。
我不認為終端用戶有能力判斷數字證書的有效性及其暗含的安全性。Java 平臺長處之一就是它的安全性,而我覺得發揮這一長處是很重要的。
Java WebStart 沙盒已經足夠強大了,它可以支持用戶進行很多有用的工作,包括讀取和保存文件,以及打印。這些操作都從用戶的角度保證了安全性和易用性。當應用程序想要訪問本地文件系統時,會彈出對話框提示用戶,并由用戶來選擇可以被讀寫的文件。應用程序僅能得到一個用于讀寫文件的流對象,而在文件選擇的過程中,沒有任何機會窺探到文件系統的情況。
讓人討厭的是,在 WebStart 下運行時,開發者必須自己編寫代碼來和?`FileOpenService`?以及`FileSaveService`?進行交互。更討厭的是,沒有一個 WebStart 接口調用,可以知道應用程序是否是由 WebStart 啟動的。
類似地,保存用戶的使用偏好也必須以兩種方式來實現:當應用程序正常運行時,使用 Java 偏好接口(Java Preferences API);當應用程序在 WebStart 下運行時,使用 WebStart 偏好服務(WebStart Preferences Service)。然而,打印功能對于應用開發者來說則是完全透明的④。
> ④ 譯者注:即正常運行和在 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 接口本身并非如此方便;在其生命周期里,很少有人喜歡它,以至于它基本上已經被大家忽略了。大多數的項目直接為其 WebStart 應用程序使用可一個自己簽名的證書,而對于用戶來說,這毫無安全性可言。這是一種恥辱——開源開發者應該擁護 JNLP 沙盒,把它作為嘗試新項目的一種零風險的方式。
## 22.6\. Java 2D
Violet 大量使用了 Java 2D;它是 Java API(應用程序接口)中鮮為人知的珍寶。每個節點和邊都有一個?`getShape`?方法,返回一個?`java.awt.Shape`?接口的對象。這個接口是 Java 2D 中所有形狀的公共接口,矩形、圓形、路徑,以及它們的并、交和差,都實現了這一接口。如果要創建由任意線段和二次/三次曲線段構成的圖形,比如直箭頭和彎箭頭,`GeneralPath`?類就很有用。
考慮下面這段繪制陰影的代碼(摘自?`AbstractNode.draw`?方法),從中我們可以欣賞到 Java 2D 接口的靈活之美:
~~~
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 打印機的時候,Java 2D 操作會被翻譯成 PostScript 矢量繪圖操作。如果你打印到一個文件,輸出的文件可以使用?`ps2eps`?這樣的程序處理,進而導入到 Adobe Illustrator 或 Inkscape 中。下面是相關的代碼(這里?`comp`?是一個 Swing 組件,它的?`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);
~~~
開始的時候,我還在擔心使用通用的形狀(general shapes)會影響性能,但事實證明并非如此。裁剪功能(clipping)工作得很好,只有那些更新當前視圖(viewport)的形狀操作才會被執行。
## 22.7\. 不使用 Swing 應用程序框架
大多數 GUI 框架都有一個類似“應用程序”的概念,它負責管理一組文檔,一個文檔則負責處理菜單、工具欄、狀態欄,等等。然而,Java API 中從來沒有這個概念。JSR 296 ?的提出,目的是為 Swing 應用程序提供一個基本的框架,但目前它已經處于閑置狀態了。因此,Swing 應用程序的作者有兩種選擇,要么“重新發明輪子”⑤,要么依靠一個第三方的框架。在編寫 Violet 的時候,應用程序框架的主流選擇是 Eclipse 和 NetBeans 平臺,但當時它們看上去都太重量級了。(現如今我們有更多的選擇了,比如 GUTS ?,它是 JSR 296 的一個分支。)因此,Violet 被迫重新發明了處理菜單和內部框架(internal frames)的機制。
> ??[http://jcp.org/en/jsr/detail?id=296](http://jcp.org/en/jsr/detail?id=296)
> ??[http://kenai.com/projects/guts](http://kenai.com/projects/guts)
>
> ⑤ 譯者注:reinvent the wheel,指對一些基本或常見的問題,不采納現有的成熟的方案。
在 Violet 中,你可以像下面這樣,在屬性文件(property files)中指定菜單項:
~~~
file.save.text=Save
file.save.mnemonic=S
file.save.accelerator=ctrl S
file.save.icon=/icons/16x16/save.png
~~~
有一個工具方法從前綴(比如這里的?`file.save`)創建菜單項。`.text`、`.mnemonic`這樣的后綴,在今天一般被稱作“約定優于配置”。使用資源文件來描述這些設置,顯然要比調用 API 來建立菜單高級得多,因為這讓本地化(localization)變得十分簡單。我在另一個開源項目 GridWorld ?中重用了這一機制,它是一個高中計算機科學教學的環境。
> ??[http://horstmann.com/gridworld](http://horstmann.com/gridworld)
Violet 這樣的應用程序,都允許用戶打開多個文檔,每個文檔包含一個圖形。最初編寫 Violet 的時候,多文檔接口(MDI,multiple document interface)還很流行。使用 MDI 時,主框架(main frame)有一個菜單欄,而每個文檔的視圖顯示在一個內部框架中,這個內部框架有標題欄但是沒有菜單欄。每個內部框架都被包含在主框架中,用戶可以修改它的大小,或者最小化它。此外,還有操作用于層疊或平鋪窗口。
很多開發者不喜歡 MDI,因此這種風格的用戶界面已經過時了。單文檔接口(SDI,single document interface)的應用程序則顯示很多頂層的框架。一段時間里,SDI 被認為更加高級,大概是因為可以使用宿主操作系統的標準窗口管理工具來操作這些頂層框架。當人們最終意識到太多的頂層窗口也不是很方便的時候,標簽頁界面(tabbed interfaces)出現了。標簽頁界面中,多個文檔再次被放到一個單個的框架中,但每個都是以完整大小顯示的,并且可以通過標簽(tabs)來選擇。這種界面不允許用戶并排比較兩個文檔,但看起來還是勝出了。
Violet 最初使用的是 MDI。Java API 包含了內部框架這一特性,但我需要增加對平鋪和層疊窗口的支持。Alexandre 切換到了標簽頁界面;從某種程度上來說,這種界面在 Java API 中得到了更好的支持。在應用程序框架中,如果文檔的顯示策略對于開發者是透明的,或許是可以讓用戶來選擇的,那將是非常可取的。
Alexandre 還增加了對側邊欄、狀態欄、歡迎面板、啟動閃屏的支持。理想情況下,所有這些都應該是 Swing 應用程序框架所支持的。
## 22.8\. 撤消(undo)和重做(redo)
實現多次撤消和重做,看起來是一件令人怯步的任務,但 Swing 的撤銷功能包(undo package,[[Top00](http://www.aosabook.org/en/bib1.html#bib%3atopley%3acoreswing)],第九章)給出了一個很好的架構上的指南。一個?`UndoManager`(撤消管理器)管理一個`UndoableEdit`(可撤消的編輯)對象的棧(stack)。這里面的每一個對象,都有一個?`undo`?方法負責撤消該編輯操作的作用,還有一個?`redo`?方法負責重做該編輯操作(恢復效果)。一個`CompoundEdit`(復合編輯)是一串?`UndoableEdit`?操作,它們可以作為一個整體被撤消或重做。我們鼓勵你定義小的、原子的(atomic)編輯操作(比如在一個圖里增加或刪除一條邊或一個節點),而這些操作可以按需被組織成一個復合的編輯操作。
隨之而來的一個挑戰就是如何定義一個小的原子操作集合,使得其中每個操作都容易撤消。Violet 中有如下幾種原子操作:
* 增加或刪除一個節點或一條邊
* 附著(attach)或拆開(detach)一個節點的子節點
* 移動一個節點
* 改變節點或邊的屬性
上述每種操作都有一個很顯然的撤消方法。比如,“增加一個節點”的撤消方法就是刪掉這個節點,“移動一個節點”的撤消方法就是按相反的向量移動這個節點。

圖 22.6:撤消操作必須撤消模型中的結構化的更改
注意,這些原子操作**不同于**用戶界面中的那些動作,或者是這些動作所調用的?`Graph`?接口的方法。比如,考慮圖 22.6 中的順序圖,假設用戶從左側的方法調用框拖動鼠標到右側的對象生命線,當鼠標被松開時,下面的方法會被調用:
~~~
public boolean addEdgeAtPoints(Edge e, Point2D p1, Point2D p2)
~~~
這個方法會增加一條邊,但它也可能根據具體參與調用的?`Edge`?和?`Node`?子類,進行其他操作。在這個例子中,右側的對象生命線會增加一個新的方法調用框,那么撤消這一操作的時候,這個新的方法調用框也要被刪掉。因此,模型(此例中的圖形)還需要記錄所需撤消的結構化的更改,而僅僅記錄控制器(controller)中的操作是不夠的。
正如 Swing 的撤銷功能包所預想的,在發生一個結構化的更改時,圖形、節點、邊的這些類需要向`UndoManager`?發送?`UndoableEditEvent`(可撤消的編輯事件)通知。Violet 具有一個更加通用的設計——圖形自身為如下接口管理監聽器(listeners):
~~~
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);
}
~~~
Violet 框架在每個圖形中安裝一個監聽器,作為與撤消管理器交互的橋梁。對于支持撤消功能來說,為模型增加通用的監聽器支持是有點過度設計了(overdesigned)——圖形操作可以直接與撤銷管理器交互。然而,我還想支持一種實驗性質的協作式編輯特性(collaborative editing feature)。
如果你想在你的應用程序中支持撤消和重做,仔細想想你的模型(而非你的界面)中的原子操作。在模型中,當發生結構化的改變時,觸發相應的事件,并允許 Swing 的撤銷管理器來收集、組合這些事件。
## 22.9\. 插件架構
對于熟悉 2D 圖形編程的程序員來說,為 Violet 增加新類型的圖并不困難。比如,活動圖就是由第三方貢獻開發的。當我需要創建鐵路圖和 ER 圖的時候,我發現給 Violet 寫一個擴展,要比胡亂使用 Visio 或 Dia 來得更快。(每種類型的圖需要花一天的時間來實現。)
這些實現并不要求你理解整個 Violet 框架。只需要實現圖形、節點和邊的接口即可。為了讓貢獻開發者更容易地脫離 Violet 框架的演變過程,我設計了一個簡單的插件架構。
當然,很多程序都有一個插件架構,其中的很多都說明詳盡、實現精巧。當有人建議說 Violet 應該支持 OSGi 的時候,我打了個哆嗦,然后實現了能夠讓插件機制工作的最簡單的事情。
貢獻開發者只需要生成一個包含了圖形、節點和邊的實現的 JAR 文件,并把它放到?`plugins`?目錄中。當 Violet 啟動時,它會用 Java 的?`ServiceLoader`(服務加載器)類加載這些插件。`ServiceLoader`?類是用來加載像 JDBC 驅動這樣的服務的。`ServiceLoader`?會加載那些能夠對指定接口提供實現的 JAR 文件(比如這里的?`Graph`?接口)。
每個 JAR 文件必須包含一個名為?`META-INF/services`?的子目錄,該目錄中需要包含一個以相應接口的全稱命名的文件(比如?`com.horstmann.violet.Graph`),文件內容是實現了這一接口的所有類的名字,每行一個。`ServiceLoader`?會為插件目錄構造一個類加載器(class loader),并加載所有的插件:
~~~
ServiceLoader<Graph> graphLoader = ServiceLoader.load(Graph.class, classLoader);
for (Graph g : graphLoader) // ServiceLoader<Graph>實現了Iterable<Graph>這個接口
registerGraph(g);
~~~
這是標準 Java 中的一個簡單而實用的設施;你或許會在你自己的項目中發現它的價值。
## 22.10\. 總結
和很多開源項目一樣,Violet 誕生于未被滿足的需求,即以最小的混亂代價,來繪制簡單的 UML 圖。Java SE 平臺令人驚奇的廣泛應用成就了 Violet。同時,Violet 也利用了該平臺中的很多技術。在這篇文章中,我描述了 Violet 如何利用 JavaBeans、長期的持久化、Java WebStart、Java 2D、Swing 撤消和重做,以及服務加載器設施。這些技術并不總是和基礎的 Java 及 Swing 一樣容易被人理解,但它們可以極大地簡化桌面應用程序的架構。它們讓我能夠在最開始作為一個獨立的開發者,在幾個月的業余時間里創建出一個成功的應用程序。依賴這些標準的機制,也讓其他人改進 Violet,或是從中提取一些片段利用在他們自己的項目中,變得更加容易。
- 前言(卷一)
- 卷1:第1章 Asterisk
- 卷1:第3章 The Bourne-Again Shell
- 卷1:第5章 CMake
- 卷1:第6章 Eclipse之一
- 卷1:第6章 Eclipse之二
- 卷1:第6章 Eclipse之三
- 卷1:第8章 HDFS——Hadoop分布式文件系統之一
- 卷1:第8章 HDFS——Hadoop分布式文件系統之二
- 卷1:第8章 HDFS——Hadoop分布式文件系統
- 卷1:第12章 Mercurial
- 卷1:第13章 NoSQL生態系統
- 卷1:第14章 Python打包工具
- 卷1:第15章 Riak與Erlang/OTP
- 卷1:第16章 Selenium WebDriver
- 卷1:第18章 SnowFlock
- 卷1:第22章 Violet
- 卷1:第24章 VTK
- 卷1:第25章 韋諾之戰
- 卷2:第1章 可擴展Web架構與分布式系統之一
- 卷2:第1章 可擴展Web架構與分布式系統之二
- 卷2:第2章 Firefox發布工程
- 卷2:第3章 FreeRTOS
- 卷2:第4章 GDB
- 卷2:第5章 Glasgow Haskell編譯器
- 卷2:第6章 Git
- 卷2:第7章 GPSD
- 卷2:第9章 ITK
- 卷2:第11章 matplotlib
- 卷2:第12章 MediaWiki之一
- 卷2:第12章 MediaWiki之二
- 卷2:第13章 Moodle
- 卷2:第14章 NginX
- 卷2:第15章 Open MPI
- 卷2:第18章 Puppet part 1
- 卷2:第18章 Puppet part 2
- 卷2:第19章 PyPy
- 卷2:第20章 SQLAlchemy
- 卷2:第21章 Twisted
- 卷2:第22章 Yesod
- 卷2:第24章 ZeroMQ