以下是第四个版本,连贯接口更完善的版本。这次,方法返回的是中间对象,而不是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; } } |

