快捷搜索:

演化架构和紧急设计: 连贯接口

本 系列 的 上一期 先容了若何应用特定领域说话(DSL)来捕获域惯用模式。本期将继承该主题,展示各类 DSL 构建措施。

鄙人一本书 Domain Specific Languages 中,Martin Fowler 将阐述两种 DSLs 之间的差别。外部 DSLs 可构建一个新语法,构建时必要应用 lexx 和 yacc 或 Antlr 等对象。一个内部 DSL 在基础说话根基之上构建新的说话,并借用和样式化基础说话的语法。本期示例将以 Java™ 为基础说话构建内部 DSLs,在其语法之上构建新的小型说话。

强调经由过程以下所有措施构建 DSLs 是隐式高低文 的观点所在。DSLs(分外是内部 DSLs)试图经由过程创建环抱相关元素的高低文包装器来打消复杂的语法。该观点的一个范例示例是 XML 中的父元素和子元素,这些元素供给一个环抱相关项目的包装器。您会留意到,这些 DSL 措施中有许多应用说话语法技术来达到同样的效果。

可读性是应用 DSL 的上风之一。假如您编写非开拓职员可以读懂的代码,那么就缩短了您的团队与哀求相关代码功能的人之间的反馈环路。Fowler 的书中确定的一个公共 DSL 模式叫作连贯接口,他将其定义为能够为一系列措施调用中转或掩护指令高低文的行径。我会向您展示几种连贯接口,首先是措施链接。

措施链接

措施链接应用措施的返回值来中转指令高低文,在这种环境下是进行第一个措施调用的工具实例。这听起来要繁杂多了,是以我要经由过程一个示例来说明该观点。

应用 DSLs 时,经常必要从目标语法开始向后运转,以弄清若何实现它。从终点开始行得通是由于可读性在 DSLs 中异常紧张。我要应用的示例是一个跟踪日历条款的小型利用法度榜样。该利用法度榜样阐释了 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 加倍可读的行径在内部 DSLs 中是很常见的。

Appointment 类包孕清单 2 中呈现的大年夜部分连贯接口措施:

清单 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) : "");

}

}

如您所见,构建连贯接口很简单。对付每个赋值措施,您可以编写 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 appointments;

public AppointmentCalendarChained() {

appointments = new ArrayList();

}

public List getAppointments() {

return appointments;

}

public Appointment add(String name) {

Appointment appt = new Appointment(name);

appointments.add(appt);

return appt;

}

}

add() 措施:

经由过程创建一个新的 Appointment 实例开始措施链接

将新实例添加到 appointments 列表中

着末返回新 appointment 实例,这意味着随后的措施调用是在新的 appointment 长进行的

运行利用法度榜样时,您可以看到您设置设置设备摆设摆设的 appointments 的细节,如图 1 所示:

图 1. 日历利用法度榜样运行结果

到今朝为止,措施链接看起来像一个清理过多繁琐语义的简单措施,分外是最具声明性的那些措施调用。这对付紧急设计中的惯用模式很有效,由于域模式平日都是声明性的。

留意,应用措施链接就一定会违反 JavaBeans 的语律例则,该规则规定赋值措施必须以 set 开始并返回 void。构建连贯接口就可以知道什么时刻违反一些规则是行得通的。假如 JavaBeans 规范强制您编写肴杂代码,那么它不会给您带来任何赞助。然则创建和应用连贯接口也不能扫除对连贯接口和 JavaBeans 接口的同时支持。连贯接口措施可以转而调用标准 set 措施,从而容许您在框架坚持与 JavaBeans 类交互时仍旧应用连贯接口。

办理终了问题

连贯接口固有的一个缺陷在某些环境下也称作终了问题。我会经由过程变动 清单 5 中的 AppointmentCalendar 类来阐明这个问题。假设您盼望做的不光是显示 appointments,比如将其放在一个数据库或其他一些持久性机制中。您要在哪里添加代码来保存已完成的 appointment?您可以考试测验在返回 appointment 之前在 AppointmentCalendar 的 add() 措施中履行它。清单 6 显示了试图造访 appointment 并进行简单的打印操作的代码:

清单 6. 添加打印

public Appointment add(String name) {

Appointment appt = new Appointment(name);

appointments.add(appt);

System.out.println(appt.toString());

return appt;

}

运行 清单 6 中的代码时,会呈现图 2 所示的不满结果:

图 2. 变动 AppointmentCalendar 之后的差错输出

显示的差错是一个 NullPointerException,它发生在 Appointment 类上的 toString() 措施中。只管措施运行精确,它照样会报错,这便是终了问题的实质。

您可能还会对下面的文章感兴趣: