使用Java实现内部领域特定语言
简介 {4W:/0 h-
4U/pU1
领域特定语言(DSL)通常被定义为一种特别针对某类特殊问题的计算机语言,它不打算解决其领域外的问题。对于DSL的正式研究已经持续很多年,直到最近,在程序员试图采用最易读并且简炼的方法来解决他们的问题的时候,内部DSL意外地被写入程序中。近来,随着关于Ruby和其他一些动态语言的出现,程序员对DSL的兴趣越来越浓。这些结构松散的语言给DSL提供某种方法,使得DSL允许最少的语法以及对某种特殊语言最直接的表现。但是,放弃编译器和使用类似Eclipse这样最强大的现代集成开发环境无疑是该方式的一大缺点。然而,作者终于成功地找到了这两个方法的折衷解决方式,并且,他们将证明该折衷方法不但可能,而且对于使用Java这样的结构性语言从面向DSL的方式来设计API很有帮助。本文将描述怎样使用Java语言来编写领域特定语言,并将建议一些组建DSL语言时可采用的模式。 SYx~H>ph
Xo'|Y%
Java适合用来创建内部领域特定语言吗? '~{#ELc
a<[ gD~%
在我们审视Java语言是否可以作为创建DSL的工具之前,我们首先需要引进“内部DSL”这个概念。一个内部DSL在由应用软件的主编程语言创建,对定制编译器和解析器的创建(和维护)都没有任何要求。Martin Fowler曾编写过大量各种类型的DSL,无论是内部的还是外部的,每种类型他都编写过一些不错的例子。但使用像Java这样的语言来创建DSL,他却仅仅一笔带过。 j[H~}]m E
s @2\Oh$
另外还要着重提出的很重要的一点是,在DSL和API两者间其实很难区分。在内部DSL的例子中,他们本质上几乎是一样的。在联想到DSL这个词汇的时候,我们其实是在利用主编程语言在有限的范围内创建易读的API。“内部DSL”几乎是一个特定领域内针对特定问题而创建的极具可读性的API的代名词。 K"`ZQ}+-w
J$N " z?l
任何内部DSL都受它基础语言的文法结构的限制。比如在使用Java的情况下,大括弧,小括弧和分号的使用是必须的,并且缺少闭包和元编程有可能会导致DSL比使用动态语言创建来的更冗长。 Q E:]'-
rO eSR D
但从光明的一面来看,通过使用Java,我们同时能利用强大且成熟的类似于Eclipse和IntelliJ IDEA的集成开发环境,由于这些集成开发环境“自动完成(auto-complete)”、自动重构和debug等特性,使得DSL的创建、使用和维护来的更加简单。另外,Java5中的一些新特性(比如generic、varargs 和static imports)可以帮助我们创建比以往任何版本任何语言都简洁的API。 :#!C'}l+
o U, nZX
一般来说,使用Java编写的DSL不会造就一门业务用户可以上手的语言,而会是一种业务用户也会觉得易读的语言,同时,从程序员的角度,它也会是一种阅读和编写都很直接的语言。和外部DSL或由动态语言编写的DSL相比有优势,那就是编译器可以增强纠错能力并标识不合适的使用,而Ruby或Pearl会“愉快接受”荒谬的input并在运行时失败。这可以大大减少冗长的测试,并极大地提高应用程序的质量。然而,以这样的方式利用编译器来提高质量是一门艺术,目前,很多程序员都在为尽力满足编译器而非利用它来创建一种使用语法来增强语义的语言。 g& XcPg
利用Java来创建DSL有利有弊。最终,你的业务需求和你所工作的环境将决定这个选择正确与否。 ukL,Ve
Q_Rj};2|.
将Java作为内部DSL的平台 zuRI1s+E:
nC(5Ny"s %
动态构建SQL是一个很好的例子,其建造了一个DSL以适合SQL领域,获得了引人注意的优势。 C 7YALM5m
传统的使用SQL的Java代码一般类似于: W_5)l
Mz k'8Ojk
String sql = "select id, name " + 2TkF[}zka
"from customers c, order o " + !V}$zJi
"where " + .vei}Ps
"c.since >= sysdate - 30 and " + #zra1c;h
"sum(o.total) > " + significantTotal + " and " + wvdC4L0*>
"c.id = o.customer_id and " + 7hj &!Y3
"nvl(c.status, 'DROPPED') != 'DROPPED'"; !"
GE4R_|[-6
vmI> ;V?-
从作者最近工作的系统中摘录的另一个表达方式是: 09+};vp9u
BSX,M)b
Table c = CUSTOMER.alias(); !7EZqI 3H
Table o = ORDER.alias(); <u%N(=9
Clause recent = c.SINCE.laterThan(daysEarlier(30));#p#分页标题#e# 3g4A v-;
Clause hasSignificantOrders = o.TOTAT.sum().isAbove(significantTotal); aIFfY&e\u
Clause ordersMatch = c.ID.matches(o.CUSTOMER_ID); Y^R u)
Clause activeCustomer = c.STATUS.isNotNullOr("DROPPED"); ^@<(PK!%)
String sql = CUSTOMERS.where(recent.and(hasSignificantOrders) kk|.is
.and(ordersMatch) jg)0L
.and(activeCustomer) 0I uVzZ
.select(c.ID, c.NAME) Tq pu=\N+
.sql(); F F3X] O
{#xoq K
-sqJ\W>@3
这个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)”来替代这段代码的话,显然会更具阅读性,并且系统也能够受到保护,从而避免将来为了利用另一个数据库供应商的设施而不得不修改最初的代码实现。 $du0IH4[$}
>0$r> 6
使用Java创建内部DSL <= RaWQ
}_TGDZ~
创建DSL最好的方法是,首先将所需的API原型化,然后在基础语言的约束下将它实现。DSL的实现将会牵涉到连续不断的测试来肯定我们的开发确实瞄准了正确的方向。该“原型-测试”方法正是测试驱动开发模式(TDD-Test-Driven Development)所提倡的。 ~lNR7ph`l
&5AmBvRBr
在使用Java来创建DSL的时候,我们可能想通过一个连贯接口(fluent interface)来创建DSL。连贯接口可以对我们所想要建模的领域问题提供一个简介但易读的表示。连贯接口的实现采用方法链接(method chaining)。但有一点很重要,方法链接本身不足以创建DSL。一个很好的例子是Java的StringBuilder,它的方法“append”总是返回一个同样的StringBuilder的实例。这里有一个例子: P ;j^I9
=:X3 Et$
StringBuilder b = new StringBuilder(); VXc^(
b.append("Hello. My name is ") hY$c0y-
.append(name) p>0sD+tL
.append(" and my age is ") uy 'GVU
.append(age); q|P[I?)
m@4'rJp
$YM.yx`j
该范例并不解决任何领域特定问题。 &4d~F #E
BAzZ #i
除了方法链接外,静态工厂方法(static factory method)和import对于创建简洁易读的DSL来说是不错的助手。在下面的章节中,我们将更详细地讲到这些技术。 'J5HgLF
=b6R$_
1、方法链接(Method Chaining) A7"33I>\.%
3 T{`6(C-
使用方法链接来创建DSL有两种方式,这两种方式都涉及到链接中方法的返回值。我们的选择是返回this或者返回一个中间对象,这决定于我们试图要所达到的目的。 Fj}6 #Ia
X{ z?ytD}
1.1、返回this I2h":6Al8
8@UQbu#4
在可以以下列方式来调用链接中方法的时候,我们通常返回this: n56I6j G
aQYrNk2
◆可选择的。 8\4f ?);]`
◆以任何次序调用。 Ue4r_ _
◆可以调用任何次数。 *e]G('Q ]
bZjOtiM
我们发现运用这个方法的两个用例: yDMQqeD
T%Y2y6q
1、相关对象行为链接。 pkQdogM~
2、一个对象的简单构造/配置。 w>upSWG
Mo+&
1.1.1、相关对象行为链接#p#分页标题#e# sz 23_{c?
db @v6Z\
很多次,我们只在企图减少代码中不必要的文本时,才通过模拟分派“多信息”(或多方法调用)给同一个对象而将对象的方法进行链接。下面的代码段显示的是一个用来测试Swing GUI的API。测试所证实的是,如果一个用户试图不输入她的密码而登录到系统中的话,系统将显示一条错误提示信息。 k`#GL#g26
##-H<
DialogFixture dialog = new DialogFixture(new LoginDialog()); 5l,vVOHi
dialog.show(); 1\.L !OD
dialog.maximize(); '%ET|1#SO
TextComponentFixture usernameTextBox = dialog.textBox("username"); t0!CCY
usernameTextBox.clear(); z\j fXiF
usernameTextBox.enter("leia.organa"); e1jK%``Lj
dialog.comboBox("role").select("REBEL"); {a4 $
OptionPaneFixture errorDialog = dialog.optionPane(); :@/PV1=Q,<
errorDialog.requireError(); jdnX (8
errorDialog.requireMessage("Enter your password"); *fLj>n1
Kh4w Q
jIA"Q+$&%
尽管代码很容易读懂,但却很冗长,需要很多键入。 pumfVcG
:o`|cU^
下面列出的是在我们范例中所使用的TextComponentFixture的两个方法: `)Fa; Cm
le0Bu9Zb
public void clear() { p!5h@SVA!2
target.setText(""); a%S@B%P
} /uwciVi/8
public void enterText(String text) { :##FWg{y
robot.enterText(target, text); VVuA8m|
} Fv,@*z@ke
s\(L;nnnu
2w_tZ#,Yw
我们可以仅仅通过返回this来简化我们的测试API,从而激活方法链接: ZVB~Q $Q
b7K1so$C
public TextComponentFixture clear() { k0x~wO"
target.setText(""); :P/HSh
return this; sxbK29! A
} {5rx
public TextComponentFixture enterText(String text) { %D3x!tH4zs
robot.enterText(target, text); A-Fof~F
return this; 1<lNIW$
} Jf/Eu@sV-
& 6N0h7G
L3GBp Nh
在激活所有测试设施中的方法链接之后,我们的测试代码现在缩减到: \ ;,#by|
E|B@i|_+iv
DialogFixture dialog = new DialogFixture(new LoginDialog()); tkh dXO
dialog.show().maximize(); ^0i9F
dialog.textBox("username").clear().enter("leia.organa"); }O_8BI[>
dialog.comboBox("role").select("REBEL"); 8J_ZdL-
dialog.optionPane().requireError().requireMessage("Enter your password"); ,-npIr0G
s$3R,Qj
DKj%)/toB
这个结果代码显然更加简洁易读。正如先前所提到的,方法链接本身并不意味着有了DSL。我们需要将解决领域特定问题的对象的所有相关行为相对应的方法链接起来。在我们的范例中,这个领域特定问题就是Swing GUI测试。 WPk;M~cDtF
fL9iDVIp
1.1.2、对象的简单构造/配置 A= V)5v8U
w)Wby~1 =Y
这个案例和上文的很相似,不同是,我们不再只将一个对象的相关方法链接起来,取而代之的是,我们会通过连贯接口创建一个“builder”来构建和/或配置对象。 CZ' Bh}1P #p#分页标题#e#
ng{kfc6bT6
下面这个例子采用了setter来创建“dream car”: &Y3 2sU
#sE'+3$=<\
DreamCar car = new DreamCar(); Q{U\0OV
car.setColor(RED); Q"]F3?. <
car.setFuelEfficient(true); ".^9fl
car.setBrand("Tesla"); H.Rc!!St}
k^"J- S_`
(i7T`< |
DreamCar类的代码相当简单: &/V_`^
%)S"v `
// package declaration and imports 1#h1BK
public class DreamCar { =+Cnv/f
private Color color; >3L6{~QAbA
private String brand; EdzdX>p
private boolean leatherSeats; 4Xu@rL
private boolean fuelEfficient; {\xR~|d,0
private int passengerCount = 2; P?Vgh.nu
// getters and setters for each field :zs="F
} h}$0k~4'+G
tQ.3Y$X3U)