连贯接口(fluent interface)的Java实现及应用。
几年前在单元测试时使用mockito和junit(使用hamcrest提供的比较方法)的时候,就用到过这样类似的语法:
mockito:
when(mock.someMethod("some arg")) .thenThrow(new RuntimeException()) .thenReturn("foo");
junit:
assertThat(responseString, either(containsString("color")).or(containsString("colour")));
如果按照自然语言来理解,非常清晰:如第一个例子,当被mock的类的某个方法被调用时,先会抛出一个Runtime的异常 然后会返回一个“foo”字符串。
但是,如果按照以前我们学习的java的用法来思维则会很困惑:when().thenThrow().thenReturn() 这是神马意思?想半天你才会猜测着理解这是when()方法返回了某对象有thenThrow()方法,该方法被调用以后又返回了某对象,某对象有thenReturn()方法可以被调用。我一直把它作为一种让编程变得简单的语法糖,没有深究。
在前一段学着写写安卓,第三次发现这种用法,而且被大范围使用了:例如一个Dialog,你会看到这样的用法:
new AlertDialog.Builder(this) .setIcon() .setTitle() .setPositiveButton()
这又跟我们平时的用法不大一样。以前都是new出一个实例来以后,在它的下方整齐的写n个set来设置它的属性。这又没有前面2个例子里的前后处理关系。所以又习惯性的把它当做了语法糖。
直到前一段开始看老马的《特定领域语言》这本书才知道这种用法是一种DSL,老马还给他起了一个名字 连贯接口(fluent interface)。
关于DSL,它最大的用处就是:将常见模式抽取出来,使之变成更加有可读性的描述方式。是的!有好的可读性:你之所以觉得正则表达式(另一种DSL)比一堆String解析代码难读,是你没有付出学习正则表达式的时间。当你掌握了一种DSL的规则后,你会觉得它更简便,更具有可读性,你要付出的是学习成本,而带来的则是效率的提高,与眼前的清爽。
上面的3个例子中,我们发现阅读更加简便,代码量也减少了很多。那么它是如何实现的呢?其实很简单,你可以把它看做是一种责任链模式的简写/变种。我们看一段示例代码,这段代码中,创建了一个AppoinetmentCalendarChained 的实例calendar。对calendar进行了链式操作:add().from().to().at()
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()); } }
实现链式操作的类
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) : ""); } }
我们来看add from to 这些方法。每个方法调用后都把实例本身返回了进去,java支持匿名调用,这样就可以直接调用自身方法了,其实挺简单的。
当链式操作有顺序的时候(如某些操作必须有一些前置操作),就要多封装一些复杂逻辑了,老马的书里有很多丰富的思路介绍(第10,11,33,38,50章),但并不是非常详细,需要自己再拓展阅读。
btw,要多说2句的是,连贯接口只是DSL的一种,2个常见的测试相关框架已经在大量使用了。去年用的较多的Robotframework 使用的关键字驱动也是一种典型的DSL。多学习抽象的方法对自动化框架的设计应该说有很大好处。
参考书籍:《特定领域语言》
参考链接:
1.http://martinfowler.com/bliki/FluentInterface.html
2.http://www.ibm.com/developerworks/cn/java/j-eaed13/
3.http://jmock.org/oopsla2006.pdf