本系列的前一部分介绍了使用领域特定语言(DSL)捕获领域惯用模式的主题。 本期文章继续该主题,展示了各种DSL构建技术。
在他即将出版的新书领域专用语言 ,两种类型的DSL之间Martin Fowler的分化带来(参见相关主题 )。 外部 DSL建立了新的语言语法,需要诸如lexx和yacc或Antlr之类的工具。 内部 DSL在基本语言之上构建新语言,并借用其语法。 本期中的示例使用Java™作为基本语言来构建内部DSL,并在其语法之上构建新的迷你语言。
以下所有用于构建DSL的技术的基础都是隐式上下文的概念。 DSL(尤其是内部DSL)试图通过围绕相关元素创建上下文包装来消除嘈杂的语法。 这个概念的一个很好的例子以父元素和子元素的形式出现在XML中,它们提供了相关项目的包装。 您会注意到,许多DSL技术都使用语言语法技巧达到了相同的效果。
可读性是使用DSL的好处之一。 如果您编写非开发人员可以阅读的代码,则会缩短团队与请求功能的人员之间的反馈循环。 Fowler的书中确定的一种常见DSL模式称为fluent接口 ,他将其定义为能够中继或维护一系列方法调用的指令上下文的行为 。 我将从方法链接开始向您展示几种类型的流利接口。
方法链
方法链接使用方法的返回值来中继指令上下文,在这种情况下,指令上下文是进行第一次方法调用的对象实例。 这听起来比实际要复杂得多,所以我将举一个例子来阐明这个概念。
使用DSL时,通常先从目标语法开始,然后反向进行反向工程以弄清楚如何实现它。 从头开始是有意义的,因为在DSL中可读性受到高度重视。 我将使用的示例是一个跟踪日历条目的小型应用程序。 该应用程序说明了DSL的语法,如清单1所示:
清单1.日历DSL的目标语法
public class CalendarDemoChained {
public static void main(String[] args) {
new CalendarDemoChained();
}
public CalendarDemoChained() {
Calendar fourPM = Calendar.getInstance();
fourPM.set(Calendar.HOUR_OF_DAY, 16);
Calendar fivePM = Calendar.getInstance();
fivePM.set(Calendar.HOUR_OF_DAY, 17);
AppointmentCalendarChained calendar =
new AppointmentCalendarChained();
calendar.add("dentist").
from(fourPM).
to(fivePM).
at("123 main street");
calendar.add("birthday party").at(fourPM);
displayAppointments(calendar);
}
private void displayAppointments(AppointmentCalendarChained calendar) {
for (Appointment a : calendar.getAppointments())
System.out.println(a.toString());
}
}
在顶部处理完Java日历之后,您可以看到在将值添加到两个日历项时正在使用的方法链接流利接口。 请注意,我正在使用空格来分隔一行代码(从Java语法的角度来看)。 在内部DSL中,常见的是对基本语言的使用进行样式化以使DSL更具可读性。
清单2中显示了包含大多数fluent-interface方法的Appointment
类:
清单2. Appointment
类
public class Appointment {
private String _name;
private String _location;
private Calendar _startTime;
private Calendar _endTime;
public Appointment(String name) {
this._name = name;
}
public Appointment() {
}
public Appointment name(String name) {
_name = name;
return this;
}
public Appointment at(String location) {
_location = location;
return this;
}
public Appointment at(Calendar startTime) {
_startTime = startTime;
return this;
}
public Appointment from(Calendar startTime) {
_startTime = startTime;
return this;
}
public Appointment to(Calendar endTime) {
_endTime = endTime;
return this;
}
public String toString() {
return "Appointment:"+ _name +
((_location != null && _location.length() > 0) ?
", location:" + _location : "") +
", Start time:" + _startTime.get(Calendar.HOUR_OF_DAY) +
(_endTime != null? ", End time: " +
_endTime.get(Calendar.HOUR_OF_DAY) : "");
}
}
如您所见,建立流畅的界面非常简单。 对于每个mutator方法,您都可以通过编写setter方法以返回宿主对象( this
),并通过以更具可读性的方式替换set
命名约定,从而不同于标准JavaBean语法。 本节开头的一般定义现在应该很清楚。 通过方法链中继的上下文是this
,这意味着您可以简洁地进行一系列方法调用。
在文章“ 利用可重用代码,第2部分 ”中,我展示了火车车厢的API定义,如清单3所示:
清单3.火车车厢的API
Car2 car = new CarImpl();
MarketingDescription desc = new MarketingDescriptionImpl();
desc.setType("Box");
desc.setSubType("Insulated");
desc.setAttribute("length", "50.5");
desc.setAttribute("ladder", "yes");
desc.setAttribute("lining type", "cork");
car.setDescription(desc);
由于有关内容和历史的法规规定,火车车厢的问题域很复杂。 在产生该示例的项目中,我们有很多复杂的测试场景,这些场景需要数十行set
调用,如清单3所示 。 我们试图让业务分析人员验证我们是否具有正确的神奇组合属性,但由于他们将其视为Java代码,因此对它们没有兴趣,因此他们拒绝了。 这个问题的最终影响是要求开发人员口头翻译细节,这当然容易出错且耗时。
为了解决这个问题,我们将Car
类转换为一个流畅的接口,以便清单3中的代码成为清单4所示的流畅接口:
清单4.火车车厢的流畅界面
Car car = Car.describedAs()
.box()
.length(50.5)
.type(Type.INSULATED)
.includes(Equipment.LADDER)
.lining(Lining.CORK);
该代码具有足够的说明性,并从Java API版本中消除了足够的噪音,我们的业务分析师很乐意为我们进行验证。
回到日历示例,实现的最后一部分是AppointmentCalendar
类,它出现在清单5中:
清单5. AppointmentCalendar
public class AppointmentCalendarChained {
private List<Appointment> appointments;
public AppointmentCalendarChained() {
appointments = new ArrayList<Appointment>();
}
public List<Appointment> getAppointments() {
return appointments;
}
public Appointment add(String name) {
Appointment appt = new Appointment(name);
appointments.add(appt);
return appt;
}
}
add()
方法:
- 通过创建一个新的
Appointment
实例来启动方法链 - 将新实例添加到约会列表中
- 最终返回新的约会实例,这意味着对新约会调用后续方法调用
运行应用程序时,您会看到已配置约会的详细信息,如图1所示:
图1.运行日历应用程序的结果
到目前为止,方法链接似乎是清除过于冗长的语法的一种简单方法,尤其是大多数声明性的方法调用。 这对于紧急设计中的惯用模式非常有效,因为领域模式经常是声明性的。
请注意,使用方法链接必须违反JavaBeans的语法规则,该规则坚持认为mutator方法必须以set
开头并返回void
。 建立流畅的界面是了解何时打破某些规则的一个示例。 如果JavaBeans规范强迫您编写混淆的代码,那么它对您没有任何帮助! 但是,在创建或使用流利的接口方面,没有任何东西不能同时支持流利的接口和JavaBeans接口。 流利的接口方法可以转换并调用标准set
方法,即使框架坚持认为它与JavaBeans类交互,也可以使用流利的接口。
解决整理问题
在某些情况下,流畅的界面固有的一个陷阱被称为完成问题 。 我将通过清单5中的AppointmentCalendar
类的更改来说明这个问题。 大概,您不仅要显示约会,还要做更多工作,例如将约会放入数据库或其他持久性机制中。 您在哪里添加代码以将完成的约会保存到存储中? 您可以在返回约会之前尝试在AppointmentCalendar
的add()
方法中执行此操作。 清单6显示了尝试在此处访问约会的尝试,就像打印它一样简单:
清单6.添加打印
public Appointment add(String name) {
Appointment appt = new Appointment(name);
appointments.add(appt);
System.out.println(appt.toString());
return appt;
}
图2.添加到AppointmentCalendar
后的错误输出
显示的错误是在Appointment
类的toString()
方法中发生的NullPointerException
。 即使该方法正确运行,在这里为什么抱怨也是解决问题的本质。
发生错误是因为我试图在调用其余的fluent-interface setter方法之前在约会实例上调用toString()
方法。 尝试打印约会的代码出现在创建约会实例并启动链的方法中。 我可以创建必须作为链中的最后一个方法调用的save()
或finished()
方法,但我不想对DSL用户强加一个容易忘记的规则。 实际上,我宁愿不要在我流畅的界面中对方法施加任何顺序语义。
真正的问题是我对方法链接技术太过激进了。 方法链最适合用于创建简单数据对象,但是在这里,我同时将其用于Appointment
和AppointmentCalendar
的setter方法来启动方法链。
我可以通过用约会日历的add()
方法的括号完全包裹约会的创建来解决整理问题,如清单7所示:
清单7.通过参数包装
AppointmentCalendar calendar = new AppointmentCalendar();
calendar.add(
new Appointment("Dentist").
at(fourPM));
calendar.add(
new Appointment("Conference Call").
from(fourPM).
to(fivePM).
at("555-123-4321"));
calendar.add(
new Appointment("birthday party").
from(fourPM).
to(fivePM)).
add(
new Appointment("Doctor").
at("123 Main St"));
calendar.add(
new Appointment("No Fluff, Just Stuff").
at(fourPM));
displayAppointments(calendar);
在清单7中 , add()
方法的括号封装了Appointment
流畅接口的全部用法,允许add()
方法处理其想要的任何其他行为(打印,持久性等)。 实际上,我无法抗拒向AppointmentCalendar
本身添加一些流利的接口:现在可以将add()
方法链接在一起,如清单7所示,并在清单8中实现:
清单8.参数包装的AppointmentCalendar
public class AppointmentCalendar {
private List<Appointment> appointments;
public AppointmentCalendar() {
appointments = new ArrayList<Appointment>();
}
public AppointmentCalendar add(Appointment appt) {
appointments.add(appt);
return this;
}
public List<Appointment> getAppointments() {
return appointments;
}
}
当您混合流利接口类时,可能会出现整理问题。 在此示例中弹出它是因为我使用约会日历来启动方法链,混合了构造和包装行为。 通过将构造和初始化推迟到Appointment
类,我可以更轻松地分离其他包装行为(例如持久性)。
通过功能顺序包装
到目前为止,我已经展示了流利接口DSL的三种上下文传递技术中的两种。 第三个功能序列使用继承和匿名内部类创建上下文包装器。 清单9中显示了使用功能序列重写的日历应用程序:
清单9.通过功能序列包装
calendar.add(new Appointment() {{
name("dentist");
from(fourPM);
to(fivePM);
at("123 main street");
}});
calendar.add(new Appointment() {{
name("birthday party");
at(fourPM);
}});
清单9显示了我在“ 利用可重用代码,第2部分 ”中以删除结构重复为幌子引入的模式。 由于双{{
大括号,因此语法看起来很奇怪。 第一组括起来的花括号描绘了匿名内部类的构造,第二组描绘了匿名内部类的实例初始化器。 (如果这听起来有些令人困惑,则可以参考“ 利用可重用代码,第2部分 ”以获取对此Java惯用语的冗长解释。)
这种流利的界面风格的主要优点在于它的适应性。 类需要使用的唯一方法是默认构造函数(允许您创建从您的类继承的匿名内部类实例)。 这意味着您可以轻松地将流利接口方法添加到现有Java API中,而无需更改任何当前调用语义。 这使您可以逐渐“简化”现有的API。
结论
DSL简洁高效地捕获惯用域模式。 流利的接口提供了一种简单的方法来更改您编写代码的方式,以便您可以更轻松地查看要确定的惯用模式。 它们也迫使您稍微改变对代码的看法:它不仅应具有功能性而且应具有可读性,尤其是在非开发人员需要使用它的任何方面的情况下。 流利的接口可消除语法中不必要的干扰,从而使代码更具可读性。 对于声明性结构,您可以用更少的精力更清晰地表达想法。
在下一部分中,我将继续讨论DSL技术,作为一种在紧急设计中收集惯用模式的机制。
翻译自: https://www.ibm.com/developerworks/java/library/j-eaed14/index.html