表达式构建器

2013年8月8日

使用流畅接口的一个问题是它会导致一些奇怪的方法。考虑以下示例

customer.newOrder()
  .with(6, "TAL")
  .with(5, "HPK").skippable()
  .with(3, "LGV")
  .priorityRush();

withskippablepriorityRush这样的方法在Order类上看起来不太合适。这种命名方式在流畅接口提供的领域特定语言的上下文中效果很好,但我们通常期望API以我称之为命令-查询API的形式出现。在命令-查询API中,每个方法在任何上下文中都具有独立的意义。它还可以遵循诸如命令查询分离之类的原则,在Java中这意味着更改对象可观察状态的方法不应该有返回值。将流畅风格的方法与命令-查询方法混合可能会导致混淆,因为流畅方法可能会违反大多数API应该具有的外观期望。

表达式构建器是解决此问题的一种方法。表达式构建器是一个独立的对象,我们在其上定义流畅接口,然后将流畅调用转换为底层的常规API调用。因此,用于订单情况的表达式构建器将如下所示。

public class OrderBuilder {
  private Order subject = new Order();
  private OrderLine currentLine;

  public OrderBuilder with(int quantity, String productCode) {
    currentLine = new OrderLine(quantity, Product.find(productCode));
    subject.addLine(currentLine);
    return this;
  }

  public OrderBuilder skippable() {
    currentLine.setSkippable(true);
    return this;
  }

  public OrderBuilder priorityRush() {
    subject.setRush(true);
    return this;
  }

  public Order getSubject() {
    return subject;
  }
}

在本例中,我只有一个表达式构建器类,但你也可以有一个小的构建器结构,例如客户构建器、订单构建器和行构建器。使用单个对象意味着你需要一个变量来跟踪你正在为skippable方法处理的哪一行。使用结构可以避免这种情况,但它稍微复杂一些,你需要确保较低级别的构建器可以处理针对较高级别构建器的方法。在本例中,OrderLineBuilder需要具有所有OrderBuilder方法的委托方法。

进一步阅读

我在表达式构建器方面进行了更详细的讨论,内容在我的关于领域特定语言的书中。

一个很好的、开放的表达式构建器使用示例是在JMock库中。我发现描述JMock的DSL处理演变的OOPSLA论文非常有帮助。

修订历史

首次发布日期:2007年1月4日。修订日期:2013年8月8日。