2.1 打造首个Java DSL

一例胜千言。第1章提到过,本书中的例子主要来自金融证券领域,作为对实现背景的交代,讲解中特地对领域概念给出注解。(请务必阅读补充内容中给出的领域知识。)这些概念注解除了帮助你理解特定领域,还是你学习DSL实现示例时的参考资料,方便你翻查例子中涉及的概念。因为同一个领域贯穿了前后的DSL示例,你可以不断完善和丰富它们。

在1.3节,我们看到交易员老鲍摆弄了一个DSL代码片段,对客户交易单做提交到交易所之前的各种处理。为了你即将开发的第一个DSL,我们再来丰富一下这个场景。

假设你负责实现一种用来处理交易单的DSL,其中的领域语汇与老鲍所用的差不多。看过第1章之后我们知道,领域专家是推进DSL开发的一种主要驱动力。如果有一种表现力充分的DSL,领域专家就能够理解开发团队实现的业务规则和逻辑,能够在代码离开开发实验室之前进行验证,甚至可能会以DSL用户的身份参与编写功能测试套件。专家们丰富的领域知识可以保证测试覆盖巨细靡遗,不仅如此,DSL经过领域专家的阅读和使用等同于经受了一次不折不扣的可用性测试。身为项目领导者,你务必要及早安排像老鲍那样的人物参与到开发中来。

2.1 打造首个Java DSL - 图1金融中介系统:处理客户交易单

第1章提过,交易过程关系到在市场上买入和卖出证券,期间要遵守证券交易规则。交易始自投资者通过注册中介发出的交易单指令,这里的中介可以是股票经纪人、清算银行或者财务顾问。客户发出的交易单指令一般会包括若干信息,如打算交易(买入或卖出)哪种证券、多少数量、单位价格等。这些信息反映了交易双方对成交价格所作的限制。从下单到通知成交包括以下步骤:

  1. 投资者向中介下交易单指令;
  2. 中介记录交易单并转发给证交所;
  3. 交易单被执行,中介接到发回的成交通知
  4. 中介记录具体的成交信息,并将通知转给投资者。

假设你的任务是实现一段DSL代码,用来针对具体的客户指令生成新的交易单。毋庸赘言,这种语言必定由领域语汇构成,并且在有效业务规则的语义约束下,允许用户(团队中的老鲍)任意组合交易单处理规则。不必一开始就纠结于最佳的语法设计,因为我们在第1章就说过,DSL必须迭代式演进,从来不可能一蹴而就。接下来你会看到我们的交易单处理DSL在逐步演进、通过选择不同的实现语言可使其获得更强的表现力,而我们将最终选择一种令老鲍满意的语言。虽然说项目的第一步不应该把目标和期望定得太大太高,但我们从第1章了解到,创造一种DSL首先必须在所有项目干系人之间确立共通语汇。

2.1.1 确立共通语汇

老鲍观察了一下问题域,很快就圈定核心需求,随后三两下总结出交易单处理DSL必不可少的语言结构成分,如表2-1所示。

表2-1 初步总结交易单处理DSL的语汇

领域概念明细
(1)新交易单 - 必须说明票据的名称 - 必须说明数量 - 必须说明是买入还是卖出 - 可以指定某交易单为“全部完成或放弃”(all-or-none),即要么接受交易单指定的全部交易,要么不交易。不存在部分成交
(2)交易单报价 - 要求指定单位价格 - 可以用限价(limit-price)、限价收盘价(limit-on-close-price)、限价开盘价(limit-on-open-price)等形式设定单位价格
(3)交易单定价 - 要求根据报价方案给整个交易单定价 - 报价方案可以从预设方案中选择,也可以由用户当场设定一个临时报价方案

有了这张语汇表,我们就可以着手实现了——选择Java这种占据统治地位的语言来完成第一次尝试。Java语言的开发者数量无出其右,不管用Java开发什么东西,这个庞大群体都是潜在的支持力量。除了动手实现,我们还要探讨Java作为实现语言在表现力方面的局限。老鲍自告奋勇帮我们编写功能测试和验证业务规则,我们的实现一定要让他觉得舒服才行。

2.1.2 用Java完成的首个实现

Java是一种面向对象(OO)语言,那么设计DSL的第一步自然就是把封装了客户交易单各方面属性的Order(交易单)抽象表达为一个对象。

1. 建立交易单抽象Order

代码清单2-1中就是老鲍即将用来处理新交易单的(用Java实现)Order类。

代码清单2-1 为Java DSL设计得的交易单抽象

  1. public class Order {
  2. static class Builder { Builder设计模式
  3. private String security;
  4. private int quantity;
  5. private int limitPrice;
  6. private boolean allOrNone;
  7. private int value;
  8. private String boughtOrSold;
  9. public Builder() {}
  10. public Builder buy(int quantity, String security) {
  11. this.boughtOrSold = "Bought";
  12. this.quantity = quantity;
  13. this.security = security;
  14. return this; 用方法链接手法实现的连贯接口
  15. }
  16. public Builder sell(int quantity, String security) {
  17. this.boughtOrSold = "Sold";
  18. this.quantity = quantity;
  19. this.security = security;
  20. return this;
  21. }
  22. public Builder atLimitPrice(int p) {
  23. this.limitPrice = p;
  24. return this;
  25. }
  26. public Builder allOrNone() {
  27. this.allOrNone = true;
  28. return this;
  29. }
  30. public Builder valueAs(OrderValuer ov) {
  31. this.value = ov.valueAs(quantity, limitPrice);
  32. return this;
  33. }
  34. public Order build() {
  35. return new Order(this);
  36. }
  37. }
  38. private final String security; ➌不可变属性
  39. private final int quantity;
  40. private final int limitPrice;
  41. private final boolean allOrNone;
  42. private int value;
  43. private final String boughtOrSold;
  44. private Order(Builder b) {
  45. security = b.security;
  46. quantity = b.quantity;
  47. limitPrice = b.limitPrice;
  48. allOrNone = b.allOrNone;
  49. value = b.value;
  50. boughtOrSold = b. boughtOrSold;
  51. }
  52. //获取方法
  53. }

这个类的实现代码用了一些常见的Java惯用法和设计模式来增强其API的表现力。Builder模式❶方便API的使用者分步完成交易单对象的构造。该模式还结合了连贯接口❷的设计手法,把领域问题用更易读的方式呈现出来。(连贯接口将在第4章详细讨论。)借助一个可变的builder对象,Order抽象的数据成员获得了不可变性❸,有利于实现并发。使核心抽象获得不可变性,正是通过builder来构造对象的一个正面作用。

定义 Builder设计模式常用于分步构造对象。它分离了对象的构造过程与对象的表示,所以不同的对象表示可以共用同样的构造过程。详情参见2.6节中的参考文献[5]。

Builder模式的实现部分就说到这里,以后我们再回头讨论代码中存在的一些问题。现在,我们先看看这个实现会给老鲍带来什么样的DSL。

2. 构造交易单

下面是一段交易单buider的应用实例,其中领域语汇的密度非常高;示例中由API形成的语言里,几乎可以找到表2-1列出的全部关键字:

  1. Order o =
  2. new Order.Builder()
  3. .buy(100, "IBM")
  4. .atLimitPrice(300)
  5. .allOrNone()
  6. .valueAs(new StandardOrderValuer()) ➊交易单定价算法
  7. .build();

虽然我们的DSL已经用了正确的语汇,但本质上还是一段Java程序,仍然脱不开Java编程语言语法上的限制和琐碎的缺点。调用valueAs的时候➊,你需要指定一个定价算法作为它的输入,但算法只能在当前上下文以外实现。这是因为Java不直接支持高阶函数,所以我们没办法优雅地就地定义定价策略。这个Java实现的用户只能预先定义每一种交易单定价策略的具体实现。我们把“交易单定价”的契约实现为一个接口:

  1. public interface OrderValuer {
  2. int valueAs(int qty, int unitPrice);
  3. }

在Java中模拟高阶函数

虽然Java不直接支持高阶函数,但是有一些库用对象模拟出这种特性,如lambdaJ(http://code.google.com/p/lambdaj)、Google Collections(http://code.google.com/p/guava-libraries)和Functional Java(http://functionaljava.org)。如果Java是你的唯一选项,但又希望对高阶函数建模,这几个库不失为可行的途径。但这些库提供的选项存在缺点,就是较为烦琐,而且优雅程度肯定不如Groovy、Ruby、Scala等语言直接提供的语言特性。

DSL的用户针对特定定价策略分别定义该接口的具体实现:

  1. public class StandardOrderValuer implements OrderValuer {
  2. public int valueAs(int qty, int unitPrice) {
  3. return unitPrice * qty;
  4. }
  5. }

这时候老鲍发现自己没办法在现场随时定义定价策略,我们的DSL满足不了他一开始提出的需求。这可是个大挫折,毕竟我们自诩DSL能让不懂编程的领域专家写出有意义的功能测试。另外,老鲍还对交易单处理DSL提出了几点意见,如下。

  • 语法烦琐 语言中含有太多括号等令人眼花缭乱的东西,容易干扰不懂编程的领域专家。

  • 语法中与领域无关的复杂性 老鲍指的是用户必须显式使用Builder类。其实,去掉Builder类这重复杂性也能实现DSL,只要把Order类的获取方法都改成链式方法,同样能构造出连贯接口。只不过,Builder类对抽象设计还有正面的影响,它促使我们设计出一种不含可变属性的不可变抽象。那么,能不能从语言中去除这些不必要的语法成分?我们可以再次运用抽象手段把builder隐藏起来,让表面上的语法看起来更直观:

  1. new Order.toBuy(100, "IBM")
  2. .atLimitPrice(300)
  3. .allOrNone()
  4. .valueAs(new StandardOrderValuer())
  5. .build();

这个取巧方案仅仅将复杂性从语法推到实现中,但毕竟使烦琐的部分留在DSL的实现层面,使用起来简洁多了。

3. 分析Java DSL

对于代码中显式出现的Builder模式,Java程序员百分百认可它的作用,也赞赏它使API连贯流畅这一点。如果DSL的用户都是Java程序员,那么我们设计的这种基于Java的DSL还算令人满意。但不可否认,Java的语法烦琐是一个缺点,我们可以选择一种表现力更强,而代码更简洁的实现语言来克服这个缺点。下面我们就来详细分析Java代码,看看是Java语言的哪些特性导致了老鲍不愿看到的那些语法复杂性。表2-2罗列了老鲍报告的各种不足应该归咎的Java语言特性。

表2-2 Java DSL的不足和应该归咎的Java语言局限

DSL的不足应该归咎的Java语言特性
烦琐(不必要的括号和语法成分) - 属于基本的Java语法 - 函数必须带括号。在对象和类上调用方法必须用点号
与领域无关的复杂性 - Java不是一种可以自我扩展的语言。很多常见的惯用法必须通过额外的间接层(设计模式)来表达 - 抽象设计中需要构造一些额外的类结构,其中一些会在对外公开的API里冒出头来,成为表面语法的一部分。前面示例中的Builder类就属于这种不必要的语法障碍 - Java不是一种解释语言。执行任何Java代码片段都必须先定义一个带public static voidmain方法的类。不管怎么说,在DSL用户的眼中,这就是掺杂进来的语法噪音
不能当场定义定价策略函数 - Java不直接提供高阶函数这种语言特性

接下来,我们将逐一探索能满足老鲍愿望的各种改进手段,使DSL对领域专家更加友好。