连贯接口(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

posted @ 2014-01-11 23:37  skytraveler  阅读(2272)  评论(0编辑  收藏  举报