
使用Java實現內部領域特定語言
作者 Alex Ruiz and Jeff Bay 譯者 沙曉蘭 發布于 2008年3月12日 上午1時4分
- Java
- 主題
- 領域特定語言
- 標簽
- 語言特性,
- 模式,
- 語言
簡介
領域特定語言(DSL)通常被定義為一種特別針對某類特殊問題的計算機語言,它不打算解決其領域外的問題。對于DSL的正式研究已經持續很多年,直到最近,在程序員試圖采用最易讀并且簡煉的方法來解決他們的問題的時候,內部DSL意外地被寫入程序中。近來,隨著關于Ruby和其他一些動態語言的出現,程序員對DSL的興趣越來越濃。這些結構松散的語言給DSL提供某種方法,使得DSL允許最少的語法以及對某種特殊語言最直接的表現。但是,放棄編譯器和使用類似Eclipse這樣最強大的現代集成開發環境無疑是該方式的一大缺點。然而,作者終于成功地找到了這兩個方法的折衷解決方式,并且,他們將證明該折衷方法不但可能,而且對于使用Java這樣的結構性語言從面向DSL的方式來設計API很有幫助。本文將描述怎樣使用Java語言來編寫領域特定語言,并將建議一些組建DSL語言時可采用的模式。
Java適合用來創建內部領域特定語言嗎?
在我們審視Java語言是否可以作為創建DSL的工具之前,我們首先需要引進“內部DSL”這個概念。一個內部DSL在由應用軟件的主編程語言創建,對定制編譯器和解析器的創建(和維護)都沒有任何要求。Martin Fowler曾編寫過大量各種類型的DSL,無論是內部的還是外部的,每種類型他都編寫過一些不錯的例子。但使用像Java這樣的語言來創建DSL,他卻僅僅一筆帶過。
另外還要著重提出的很重要的一點是,在DSL和API兩者間其實很難區分。在內部DSL的例子中,他們本質上幾乎是一樣的。在聯想到DSL這個詞匯的時候,我們其實是在利用主編程語言在有限的范圍內創建易讀的API。“內部DSL”幾乎是一個特定領域內針對特定問題而創建的極具可讀性的API的代名詞。
任何內部DSL都受它基礎語言的文法結構的限制。比如在使用Java的情況下,大括弧,小括弧和分號的使用是必須的,并且缺少閉包和元編程有可能會導致DSL比使用動態語言創建來的更冗長。
但從光明的一面來看,通過使用Java,我們同時能利用強大且成熟的類似于Eclipse和IntelliJ IDEA的集成開發環境,由于這些集成開發環境“自動完成(auto-complete)”、自動重構和debug等特性,使得DSL的創建、使用和維護來的更加簡單。另外,Java5中的一些新特性(比如generic、varargs 和static imports)可以幫助我們創建比以往任何版本任何語言都簡潔的API。
一般來說,使用Java編寫的DSL不會造就一門業務用戶可以上手的語言,而會是一種業務用戶也會覺得易讀的語言,同時,從程序員的角度,它也會是一種閱讀和編寫都很直接的語言。和外部DSL或由動態語言編寫的DSL相比有優勢,那就是編譯器可以增強糾錯能力并標識不合適的使用,而Ruby或Pearl會“愉快接受”荒謬的input并在運行時失敗。這可以大大減少冗長的測試,并極大地提高應用程序的質量。然而,以這樣的方式利用編譯器來提高質量是一門藝術,目前,很多程序員都在為盡力滿足編譯器而非利用它來創建一種使用語法來增強語義的語言。
利用Java來創建DSL有利有弊。最終,你的業務需求和你所工作的環境將決定這個選擇正確與否。
將Java作為內部DSL的平臺
動態構建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版本有幾項優點。后者能夠透明地適應轉換到使用PreparedStatement的方法——用
String拼寫SQL的
版本則需要大量的修改才能適應轉換到使用捆綁變量的方法。如果引用不正確或者一個integer變量被傳遞到date column作比較的話,后者
版本根本無法通過編譯。代碼“nvl(foo, 'X') != 'X'
”是Oracle SQL中的一種特殊形式,這個句型對于非Oracle SQL程序員或不熟悉SQL的人來說很難讀懂。例如在SQL Server方言中,該代碼應該這樣表達“(foo is null or foo != 'X')
”。但通過使用更易理解、更像人類語言的“isNotNullOr(rejectedValue)
”來替代這段代碼的話,顯然會更具閱讀性,并且系統也能夠受到保護,從而避免將來為了利用另一個數據庫供應商的設施而不得不修改最初的代碼實現。
使用Java創建內部DSL
創建DSL最好的方法是,首先將所需的API原型化,然后在基礎語言的約束下將它實現。DSL的實現將會牽涉到連續不斷的測試來肯定我們的開發確實瞄準了正確的方向。該“原型-測試”方法正是測試驅動開發模式(TDD-Test-Driven Development)所提倡的。
在使用Java來創建DSL的時候,我們可能想通過一個連貫接口(fluent interface)來創建DSL。連貫接口可以對我們所想要建模的領域問題提供一個簡介但易讀的表示。連貫接口的實現采用方法鏈接(method chaining)。但有一點很重要,方法鏈接本身不足以創建DSL。一個很好的例子是Java的StringBuilder
,它的方法“append”總是返回一個同樣的StringBuilder
的實例。這里有一個例子:
StringBuilder b = new StringBuilder();
b.append("Hello. My name is ")
.append(name)
.append(" and my age is ")
.append(age);
該范例并不解決任何領域特定問題。
除了方法鏈接外,靜態工廠方法(static factory method)和import對于創建簡潔易讀的DSL來說是不錯的助手。在下面的章節中,我們將更詳細地講到這些技術。
1.方法鏈接(Method Chaining)
使用方法鏈接來創建DSL有兩種方式,這兩種方式都涉及到鏈接中方法的返回值。我們的選擇是返回this或者返回一個中間對象,這決定于我們試圖要所達到的目的。
1.1 返回this
在可以以下列方式來調用鏈接中方法的時候,我們通常返回this
:
我們發現運用這個方法的兩個用例:
- 相關對象行為鏈接
- 一個對象的簡單構造/配置
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
的兩個方法:
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 對象的簡單構造/配置
這個案例和上文的很相似,不同是,我們不再只將一個對象的相關方法鏈接起來,取而代之的是,我們會通過連貫接口創建一個“builder”來構建和/或配置對象。
下面這個例子采用了setter來創建“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
}
通過builder,我們還能這樣重新編寫DreamCar
的創建過程:
DreamCar car = car().brand("Tesla")
.color(RED)
.fuelEfficient()
.build();
使用連貫接口,再一次減少了代碼噪音,所帶來的結果是更易讀的代碼。需要指出的很重要的一點是,在返回this
的時候,鏈中任何方法都可以在任何時候被調用,并且可以被調用任何次數。在我們的例子中,color
這個方法我們可想調用多少次就調用多少次,并且每次調用都會覆蓋上一次調用所設置的值,這在應用程序的上下文中可能是合理的。
另一個重要的發現是,沒有編譯器檢查來強制必需的屬性值。一個可能的解決方案是,如果任何對象創建和/或配置規則沒有得到滿足的話(比如,一個必需屬性被遺忘),在運行時拋出異常。通過從鏈中方法返回中間對象有可能達到規則校驗的目的。
1.2 返回中間對象
從連貫接口的方法中返回中間對象和返回this
的方式相比,有這樣一些優點:
- 我們可以使用編譯器來強制業務規則(比如:必需屬性)
- 我們可以通過限制鏈中下一個元素的可用選項,通過一個特殊途徑引導我們的連貫接口用戶
- 在用戶可以(或必須)調用哪些方法、調用順序、用戶可以調用多少次等方面,給了API創建者更大的控制力
下面的例子表示的是通過帶參數的構建函數來創建一個vacation對象的實例:
Vacation vacation = new Vacation("10/09/2007", "10/17/2007",
"Paris", "Hilton",
"United", "UA-6886");
這個方法的好處在于它可以迫使我們的用戶申明所有必需的參數。不幸的是,這兒有太多的參數,而且沒有表達出他們的目的。“Paris”和“Hilton”所指的分別是目的地的城市和酒店?還是我們同事的名字?:)
第二個方法是將setter方法對每個參數進行建檔:
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");
現在我們的代碼更易讀,但仍然很冗長。第三個方案則是創建一個連貫接口來構建vacation對象的實例,如同在前一章節提供的例子一樣:
Vacation vacation = vacation().starting("10/09/2007")
.ending("10/17/2007")
.city("Paris")
.hotel("Hilton")
.airline("United")
.flight("UA-6886");
這個版本的簡明和可讀性又進了一步,但我們丟失了在第一個版本(使用構建函數的那個版本)中所擁有的關于遺忘屬性的校驗。換句話說,我們并沒有使用編譯器來校驗可能存在的錯誤。這時,對這個方法我們所能做的最好的改進是,如果某個必需屬性沒有設置的話,在運行時拋出異常。
以下是第四個版本,連貫接口更完善的版本。這次,方法返回的是中間對象,而不是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");
這里,我們引進了Period
、Booking
、Location
、BookableItem
(Hotel
和Flight)
、以及 Airline
的概念。在這里的上下文中,airline作為Flight
對象的一個工廠;Location
是Hotel
的工廠,等等。我們所想要的booking的文法隱含了所有這些對象,幾乎可以肯定的是,這些對象在系統中會有許多其他重要的行為。采用中間對象,使得我們可以對用戶行為可否的限制進行編譯器校驗。例如,如果一個API的用戶試圖只通過提供一個開始日期而沒有明確結束日期來預定假期的話,代碼則不會被編譯。正如我們之前提到,我們可以創建一種使用文法來增強語義的語言。
我們在上面的例子中還引入了靜態工廠方法的應用。靜態工廠方法在與靜態import同時使用的時候,可以幫助我們創建更簡潔的連貫接口。若沒有靜態import,上面的例子則需要這樣的代碼:
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");
上面的例子不及采用了靜態import的代碼那么易讀。在下面的章節中,我們將對靜態工廠方法和import做更詳細的講解。
這是關于使用Java編寫DSL的第二個例子。這次,我們將Java reflection的使用進行簡化:
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");
在使用方法鏈接的時候,我們必須倍加注意。方法鏈接很容易會被爛用,它會導致許多調用被一起鏈接在單一行中的“火車殘骸”現象。這會引發很多問題,包括可讀性的急劇下滑以及異常發生時棧軌跡(stack trace)的含義模糊。
2. 靜態工廠方法和Imports
靜態工廠方法和imports可以使得API更加簡潔易讀。我們發現,靜態工廠方法是在Java中模擬命名參數的一個非常方便的方法,是許多程序員希望開發語言中所能夠包含的特性。比如,對于這樣一段代碼,它的目的在于通過模擬一個用戶在一個JTable
中選擇一行來測試GUI:
dialog.table("results").selectCell(6, 8); // row 6, column 8
沒有注釋“// row 6, column 8
”,這段代碼想要實現的目的很容易被誤解(或者說根本沒有辦法理解)。我們則需要花一些額外的時間來檢查文檔或者閱讀更多行代碼才能理解“6”和“8”分別代表什么。我們也可以將行和列的下標作為變量來聲明,而非像上面這段代碼那樣使用常量:
int row = 6;
int column = 8;
dialog.table("results").selectCell(row, column);
我們已經改進了這段代碼的可讀性,但卻付出了增加需要維護的代碼的代價。為了將代碼盡量簡化,理想的解決方案是像這樣編寫代碼:
dialog.table("results").selectCell(row: 6, column: 8);
不幸的是,我們不能這樣做,因為Java不支持命名參數。好的一面的是,我們可以通過使用靜態工廠方法和靜態imports來模擬他們,從而可以得到這樣的代碼:
dialog.table("results").selectCell(row(6).column(8));
我們可以從改變方法的簽名(signature)開始,通過包含所有參數的對象來替代所有這些參數。在我們的例子中,我們可以將方法selectCell(int, int)
修改為:
selectCell(TableCell);
TableCell
will contain the values for the row and column indices:
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
的構造函數仍然需要兩個int
值。下一步則是將引入一個TableCell
的工廠,這個工廠將對初始版本中selectCell
的每個參數設置一個對應的方法。另外,為了迫使用戶使用工廠,我們需要將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
工廠,我們可以創建對每個參數都有一個調用方法的TableCell
。工廠中的每個方法都表達了其參數的目的:
selectCell(new TableCellBuilder(6).column(8));
最后一步是引進靜態工廠方法來替代TableCellBuilder
構造函數的使用,該構造函數沒有表達出6代表的是什么。如我們在之前所實現的那樣,我們需要將構造函數設置為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;
}
}
現在我們只需要selectCell
的調用代碼中增加內容,包含對TableCellBuilder
中row
方法的靜態import。為了刷新一下我們的記憶,這是如何實現調用selectCell
的代碼:
dialog.table("results").selectCell(row(6).column(8));
我們的例子說明,一點點額外的工作可以幫助我們克服主機編程語言中的一些限制。正如之前提到的,這只是我們通過使用靜態工廠方法和imports來改善代碼可讀性的很多方法中的一個。下列代碼段是以另一種不同的方法利用靜態工廠方法和imports來解決相同的table坐標問題:
/**
* @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);
}
}
這個方案的第二個版本比第一個版本更具靈活性,因為這個版本允許我們通過兩種途徑來聲明行和列的坐標:
dialog.table("results").select(cellAt(row(6), column(8));
dialog.table("results").select(cellAt(column(3), row(5));
組織代碼
相比返回中間對象的的方式來說,返回this
的方式更加容易組織連貫接口的代碼。前面的案例中,我們的最后結果是使用更少的類來封裝連貫接口的邏輯,并且使得我們可以在組織非DSL代碼的時候使用同樣的規則或約定。
采用中間對象作為返回類型來組織連貫接口的代碼更具技巧性,因為我們將連貫接口的邏輯遍布在一些小的類上。由于這些類結合在一起作為整體而形成我們的連貫接口,這使得將他們作為整體對待更為合理,我們可能不想將他們和DSL外的其他一些類混淆一起,那么我們有兩個選擇:
- 將中間對象作為內嵌類創建
- 將中間對象至于他們自己的頂級類中,將所有這些中間對象類放入同一個包中
分解我們的系統所采用的方式取決于我們想要實現的文法的幾個因素:DSL的目的,中間對象(如果有的話)的數量和大小(以代碼的行數來計),以及DSL如何來與其它的代碼庫及其它的DSL相協調。
對代碼建檔
在組織代碼一章節中提到,對方法返回this
的連貫接口建檔比對返回中間對象的連貫接口建檔來的簡單的多,尤其是在使用Javadoc來建檔的情況下。
Javadoc每次顯示一個類的文檔,這對于使用中間對象的DSL來說可能不是最好的方式:因為這樣的DSL包含一組類,而不是單個的類。由于我們不能改變Javadoc顯示我們的API文檔的方式,我們發現在package.html文件中,加入一個使用連貫接口(包含所有相關類)、且對鏈中每個方法提供鏈接的例子,可以將Javadoc的限制的影響降到最低。
我們需要注意不要創建重復文檔,因為那樣會增加API創建者的維護代價。最好的方法是盡可能依賴于像可執行文檔那樣的測試。
結論
Java適用于創建開發人員易讀易寫的、并且對于商業用戶用樣易讀的內部領域特定語言。用Java創建的DSL可能比那些由動態語言創建的DSL來的冗長。但好的一面是,通過使用Java,我們可以利用編譯器來增強DSL的語義。另外,我們依賴于成熟且強大的Java集成開發環境,從而使DSL的創建、使用和維護更加簡單。
使用Java創建DSL需要API設計者做更多的工作,有更多的代碼和文檔需要創建和維護。但是,付出總有回報。使用我們API的用戶在他們的代碼庫中會看到更多的優化。他們的代碼將會更加簡潔,更易于維護,這些將使得他們的生活更加輕松。
使用Java創建DSL有很多種不同的方式,這取決于我們試圖達到的目的是什么。盡管沒有什么通用的方法,我們還是發現結合方法鏈接和靜態工廠方法與imports的方式可以得到干凈、簡潔、易讀易寫的API。
總而言之,在使用Java來創建DSL的時候有利有弊。這都由我們——開發人員根據項目需求去決定它是否是正確的選擇。
另外一點題外話,Java 7可能會包含幫助我們創建不那么冗長的DSL的新語言特性(比如閉包)。如果想得到更多關于建議中所提特性的全面的列表,請訪問Alex Miller的blog。
關于作者
Alex Ruiz是Oracle開發工具組織中的一名軟件工程師。Alex喜歡閱讀任何關于Java、測試、OOP 和AOP的信息,他最大的愛好就是編程。在加入Oracle之前,Alex曾是ThoughtWorks的咨詢顧問。Alex的blog為 http://www.jroller.com/page/alexRuiz。
Jeff Bay是紐約一家對沖基金的高級軟件工程師。他曾多次建立高質量、迅速的XP團隊工作于例如Onstar的計劃注冊系統、租賃軟件、web服務器、建筑項目管理等各種系統。他對于消除重復和防止bug方面懷有極大的熱情,以提高開發者的工作效率和減少在各種任務上所花費的時間。
相關資料
最近意外發現JunitFactory這個關鍵字,于是便去研究了一下,研究發現后得到更有意義的發現。
首先我們大概講一下什么是JunitFactory. JunitFactory 其實就是Junit's Factory.如果曾經是java的開發人員
應該大家都知道Junit 就是java的單元測試。他的功能是什么呢?其實主要是檢查一個方法輸入相關參數后得到的
結果是否是自己期望的。而且在以前的應用中,往往是開放人員根據參數預先心中算出結果然后手工放入到Junit中,
接著運行這個junit 看看是否成功或失敗。而JunitFactory則能預先輸入相關參數包括邊界參數,然后也能預先得
到與剛才相關參數相關的結果。然后自動生成對應的Junit。這個聽上去好像有點牛了。因為你要知道方法是無法去
完全去分析的。那他是怎么去做的呢?比如說有這么一個方法:
public int plus(int i, int j)
{
return i+j;
}
那么預先得到的junit是
int result = new MathDemo().plus(100, 1000);
assertEquals("result", 1100, result);
和
int result = new MathDemo().plus(0, 0);
assertEquals("result", 0, result);
兩種情況。
如果你把 plus中的 i+j 改為 i+10+j,那么junit就會自動變成
int result = new MathDemo().plus(100, 1000);
assertEquals("result", 1110, result);
和
int result = new MathDemo().plus(0, 0);
assertEquals("result", 10, result);
同樣如果改為string 那么他的junit也會相應的改掉。當然也許你要問如果我的方法很復雜,那么他怎么能自動分析產生
預期的結果?我的答案是肯定不能完全能產出所有結果。為什么?因為如果你的方法不是wellformat 或者說不符合尋常的思
路(我們稱之為低質量代碼,本來想說垃圾代碼,后來想想不太文明)那么還需要自動分析嗎?那就沒這個自動分析的價值。
怎么自動知道這些代碼是wellformat 還是unwellformat 的呢?其實這需要兩種工作的集合,經驗豐富的人工辨別和有規律
的機器辨別。值得注意的是,該JunitFactory的Eclipse pluign 就需要用戶填寫JunitFactory的website,并且保證運行
JunitFactory的時候,網絡是通的,他能連接到她的服務器。他同時upload 當前需要junit的方法,并有相應的反饋。其實
這種兩者合一的方法也解決了審核代碼的問題,所以junitFactory 官方的解釋就是With a full suite of
characterization tests generated by JUnit Factory you can bring your legacy code under control,
就是能合法地控制代碼。
上面是JunitFactory帶給我們具體的東西,我現在想討論的是軟件公司的管理模式,特別是code的管理模式。我沒有進
過500強的軟件公司,所以沒有能有幸接觸他們的管理模式。但我認為如果能把JunitFactory的模式引入軟件公司的話,這是
一件很好的事情。 這種code模式大致是這樣的
流程:coder可以先根據需求去代碼服務器詢問某個通用的方法是否已經在代碼服務器中存在,如果存在并已經被用過,那么
可以自己從代碼服務器中獲取該通用方法,如果沒有那么就需要自己code該方法,coder 通過本地代碼檢查器開發完成一個
方法后可以上傳給代碼服務器,然后由代碼管理員來審核并反饋。 審核通過并測試通過就可以進入代碼服務器,并作相應的
功能描述版本控制什么的。
這個管理的模式的只是code開發管理模式,不包括需求分析模塊,軟件的需求分析等環節同樣需要做。
這個模式的好處是:
1.能在coding的時候就能參與代碼的管理,而不是coded之后再去參與代碼的管理。這樣可以節省很多走流程所造成的時間浪
費,coder可以在這個方法還沒有審核后 可以寫其他的方法。那么有的人就會說 我后面的方法是基于前面的,我豈不是要等
待審核的結果。那我就要問,難道你的這個模塊都和這個方法耦合這么緊,如果真的是這樣 那么設計有問題。
2.能充分實現reused 的軟件思想。雖然reused 對于任何一個公司或開發人員講,他們都會知道,但是很多真正的情況卻不
是很理想,導致不可能充分利用reused的原因有很多,比如員工的溝通不夠,已有的項目積累太多 以及寫的方法是不是能
reused。這應該歸咎于一個制度上的問題,如果用這種模式,coder 的代碼必須經過審核,也就在源頭上解決了這些問題。
3.解放新的職位,很多軟件公司沒有給coder 很好的職業規劃,其實不是很多公司不想,只是沒有合適的職位給他做。那么
新的代審核人員其實是需要開發經驗很豐富的人員來承擔,同時他只要read code 而不需要再去write code。那么這一新的
職位可以部分解決這個問題。
Don't we all remember the days when we programmed C or C++? You had to use new and delete to explicitly create and remove objects. Sometimes you even had to malloc() an amount of memory. With all these constructs you had to take special care that you cleaned up afterwards, else you were leaking memory.
Now however, in the days of Java, most people aren't that concerned with memory leaks anymore. The common line of thought is that the Java Garbage Collector will take care of cleaning up behind you. This is of course totally true in all normal cases. But sometimes, the Garbage Collector can't clean up, because you still have a reference, even though you didn't know that.
I stumbled across this small program while reading JavaPedia, which clearly shows that Java is also capable of inadvertent memory leaks.
public class TestGC
{
private String large =
new String(new char
[100000]);
public String getSubString() {
return this.large.substring(0,2);
}
public static void main(String[] args) {
ArrayList<String> subStrings = new ArrayList<String>();
for (int i = 0; i <1000000; i++) {
TestGC testGC = new TestGC();
subStrings.add(testGC.getSubString());
}
}
}
Now, if you run this, you'll see that it crashes with something like the following stacktrace:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.lang.String.(String.java:174)
at TestGC.(TestGC.java:4)
at TestGC.main(TestGC.java:13)
Why does this happen? We should only be storing 1,000,000 Strings of length 2 right? That would amount to about 40Mb, which should fit in the PermGen space easily. So what happened here? Let's have a look at the substring method in the String class.
public class String {
// Package private constructor which shares value array for speed.
String(int offset,
int count, char value
[]) {
this.
value = value;
this.
offset = offset;
this.
count = count;
}
public String substring(int beginIndex, int endIndex) {
if (beginIndex <0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex> count) {
throw new StringIndexOutOfBoundsException(endIndex);
}
if (beginIndex> endIndex) {
throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
}
return ((beginIndex == 0) && (endIndex == count)) ? this :
new String(offset + beginIndex, endIndex - beginIndex, value);
}
We see that the substring call creates a new String using the given package protected constructor. And the one liner comment immediately shows what the problem is. The character array is shared with the large string. So instead of storing very small substrings, we were storing the large string every time, but with a different offset and length.
This problem extends to other operations, like String.split() and . The problem can be easily avoided by adapting the program as follows:
public class TestGC
{
private String large =
new String(new char
[100000]);
public String getSubString() {
return new String(this.large.substring(0,2)); // <-- fixes leak!
}
public static void main(String[] args) {
ArrayList<String> subStrings = new ArrayList<String>();
for (int i = 0; i <1000000; i++) {
TestGC testGC = new TestGC();
subStrings.add(testGC.getSubString());
}
}
}
I have many times heard, and also shared this opinion that the String copy constructor is useless and causes problems with not interning Strings. But in this case, it seems to have a right of existence, as it effectively trims the character array, and keeps us from keeping a reference to the very large String.
WhirlyCache
借著最近項目用到cache,就順便研究了一下cache 的設計,研究對象WhirlyCache 一個sun公司輕量的cache工具
作為一個cache的整體架構 他分這么幾個模塊
1.配置信息 包括config 文件和resource文件
Config文件 用來記錄cacheManger 中需要的信息以及對每種cache 的屬性記錄,他的結構就是以cache 為基本單位,
<whirlyCache>
<cache>..</cache>
<cache>…</cache>
</whirlyCache>
而對于cache 我認為cache包括可以有一個或多個policy(cache策略)的定義,這些policy現在主要有FIFO,LRU等等,這些多個policys組合成當前該cache的cache策略。除了主要策略 cache 還可以定義其他屬性。
cacheConfig對象與config文件中的cache 聲明可以認為是一一對應的
2.CacheManager cache管理器是cache功能中不可缺少的,和其他管理一樣,通常cache管理器是singleton模式,他的功能如下:
A. load config文件, 將config文件load到內存,將每個cache信息set到對應的cacheconfig中
B. create cache 根據前面load config,根據不同的cacheconfig創建不同的cache,其中包括policy。
C.destroy 可以對manager中某個指定的cache進行destroy或者destroy 所有的cache
D.getCache 通過指定的name來獲取緩存對象
E.shutdown 在Whirlycache中 他實現的其實是 destroy all caches 的功能, 我認為shutdown 應該是destroy all data in cachemanger的功能,包括unload config file
CacheManager的數據結構:
主要有兩個Map 用來存放數據
一個是configMap 在load config之后,存放多個cacheConfig
另一個是caches 用于存放不同的cache。、
3.Cache 接口
Cache 應該具有如下功能
A. store 在cache中存放需要存放的對象
B.retrieve 在cache中獲取需要的對象
C.remove 清除cache中的某個對象
D.size 獲取cache的size
對于數據結構來說, 最終cache應該是map這種數據結構
而WhirlyCache中 cache中的key 有兩種
一種就是map 中的object
另外一種就是他獨有的對象 Cacheable object
4.Cacheable 只是whirlyCache中的一個接口,他下面有三個方法的聲明
OnRemove,onRetreive,onStore。這個接口表示,當你將Cacheable object
作為cache的key的話,那么在執行cache中的remove,retrieve,store 三個操作
的時候,他同時調用cacheable中對于的onXXX方法。我們可以認為這個是Listener,
當做Store的操作時候,如果put 的key是 cacheable 也就對于調用Cacheable 的onStore方法,那么實現onStore 就能在store 觸發器事件。
5.CachePolicy
所謂CachePolicy 就是cache的策略,那么他與ManagerCache 有關,需要為該Policy指定ManagerCache, 所以就有SetManagedCache(), 而某個Policy的屬性是在配置文件中指定的,所以就有了SetConfig(). 那么有混淆的就是Cache 與 ManagedCache, 其實他們的區別在于一個是行為聲明, 一個是具體的數據結構,后面會具體分析,而這里需要說明他們和Policy的關系不同的是,cache 中應該是可以有多個policy,他是面向用戶的,用戶通常說的“我為某個Cache 指定了這么幾個策略組合” 那么這里的cache 就是指的Cache。而對于ManagedCache 他其實就是有個map實體,那么具體policy 的實現就是對該map的操作,比如FIFO 那么就要對這個map實行 FIFO的操作。
下面還有一個方法是Policy interface 必須聲明的,那就是 performMaintenance(), 他就是具體執行該策略。Whirly 提供了下面幾種CachePolicy:
a. FIFO 先進先出策略
b. LFU Least Frequently Used algorithm
c.LRU least recently used algorithm
Policy的機制:
首先需要一個Item對象, 他記錄被引用的object最近被使用,最近用的,以及被使用的記錄數等相關信息,在store的時候New 一個item 對象 一并put到managedCache,在執行cache中remove store retrieve等操作的時候同時需要更新item中的信息。當執行到某個策略的時候,首先需要根據不同的policy 實現不同的Comparator( AddedComparator,CountComparator, UsedComparator) 而這些Comparator的Object比較原則就是將前面說的Item的記錄信息進行比較,然后將Object排序。managedCache 根據相應的Comparator進行Sort。然后將managedCache中大于Cache設置大小的數據進行remove,這樣整個過程就是performMaintenance
6. CacheDecorator
前面的cache只是接口,其實CacheDecrator才是cache的具體實現,前面的cache只是一個interface,他定義了面向用戶接口的相關行為,具體他表示對于cache而言,提供給外部人員使用的行為接口。比如用戶只要關心從cache中remove,retrieve object的這樣的邏輯行為。而對于內部數據結構方面的具體操作,用戶并不關心。為了清晰我們需要另外獨立定義接口。這樣我們定義了ManagedCache接口,他繼承map interface。同時可以在該接口中定義其他數據操作的方法。
AbstractMapBackedCache是實現ManagedCache的抽象類,到這里就需要引入具體的數據結構,所以AbstractMapBackedCache中有個 map的 field。而具體ManagedCache中的方法的實現,也是對該map 變量的操作。
下面我們講講題外話:
對于map type 在java世界中有很多種,有jdk里面的HashMap TreeMap 這些我們都比較熟悉,我們可以認為這些Map 是classic map而今天我們在WhirlyCache中看見了其他幾個map 有的我也看到過 有的也是第一次。
ConcurrentHashMap:
來源:
http://gee.cs.oswego.edu/dl/classes/EDU/oswego/cs/dl/util/concurrent/intro.html
FastHashMapImpl:
來源:
org.apache.commons.collections.FastHashMap
他是apache commons項目中collections的一個Map
SynchronizedHashMapImpl:
Collections.synchronizedMap( new HashMap())
他表示該HashMap 是synchronized,該Map保證了該對象任何時候的一致性
通過繼承AbstractMapBackedCache,為他的map field設置不同的map,就實現不同的cache策略,這個策略有別與前面的policy,前面的是算法的策略,這里應該是數據結構的策略。
7.其他
a.實現對Hibernate Cache的控制 ,實現net.sf.hibernate.cache.Cache接口
b.添加關閉功能,當ServletContext reload的時候具體實現ServletContextListener