关于接口设计,再有Fluent Interface,这种有趣的接口设计风格

关于接口设计,还有Fluent Interface,这种有趣的接口设计风格

这个故事我早就想说了,可能是在好多个月前,只是一直不知道怎么说才能说合适,现在我重新整理了一下,讲述给大家。

 

这个故事是从下面这样一个对外暴露接口的调用开始的。

 

QueryUserEvent event = new QueryUserEvent();
event.setName(name);
event.setAge(18);
event.setType(QueryUserEvent.TYPE_NORMAL);
event.setSex(QueryUserEvent.SEX_MALE);
……
List<User> userList = userService.query(event);

 

我想做的事情其实很简单,我想查询一个用户列表,可是接口参数的拼装让我感到头疼,这样的代码太过啰嗦,我希望有可读性更好的解决办法。

 

P兄台说,如果我直接传入一个user对象,是不是可以避开了这个未必带来多少好处的event?

 

 

User user = new User();
user.setName(name);
user.setAge(18);
user.setSex(QueryUserEvent.SEX_MALE);
……
List<User> userList = userService.query(user, UserService.QUERY_TYPE_NORMAL);
 

我有时候会考虑你说的办法的,可是,你没有解决实际的问题,我现在的最大问题在于,这一堆的setXXX方法,它破坏了我构造这个查询条件对象的流畅性。

 

他紧接着说,那要不然,我们把setXXX方法的劳动省下来,让构造器来替我们完成这个任务吧:

 

User user = new User(name, 18, QueryUserEvent.SEX_MALE, ……);
List<User> userList = userService.query(user, UserService.QUERY_TYPE_NORMAL);

 

我说,你的办法看起来不错,不过有时候按你的办法做,我的构造方法会变得臃肿无比,比如出现十多个参数;

另外还有一个问题,假如说,我的查询条件是简单的(我只需要根据年龄查询),那么其它的参数都要写成null,类似这样子:

 

 

User user = new User(null, 18, null, null, null, null, ……);
List<User> userList = userService.query(user, UserService.QUERY_TYPE_NORMAL);

 

 

天,让谁去阅读这样的代码,他都不会喜欢的。

而且,有时候情形变得复杂,比如,我不是要查询18岁的所有user,而是要查询大于18岁的所有user,你的办法似乎行不通了呢……

 

你真是一个麻烦的人,他说,这样吧,我定义一个Condition,他给查询条件带来了灵活的组装方式:

 

 

UserQueryCondition condition = new UserQueryCondition();
condition.setAge(Condition.GREATER_THAN, 18);
List<User> userList = userService.query(condition, UserService.QUERY_TYPE_NORMAL);

 

 

不过,他补充道,如果在JavaScript中,我可以采取的办法要好得多。如果要查询18岁的和符合其他条件的用户,可以这样写:

 

 

userService.query({
    name : name,
    age : 18,
    sex : User.SEX_MALE
}, UserService.QUERY_TYPE_NORMAL);
 

如果要大于18岁呢,可以这样写:

 

 

userService.query({
    name : name,
    greaterThan : {
        age : 18
    },
    sex : User.SEX_MALE
}, UserService.QUERY_TYPE_NORMAL);

 

不过,他接着说,在Java里面好像还没有类似简洁的表示方法……

 

万幸的是,有一种接口连续调用的风格,叫做“Fluent Interface”,可以让这个问题写成这样一种有趣的实现:

 

 

List<User> userList = new UserService().setName(name).setAge(18).setSex(User.SEX_MALE).query(UserService.QUERY_TYPE_NORMAL);

 

大于18岁的话,这样写:

 

 

List<User> userList = new UserService().setName(name).greaterThan(new User().setAge(18)).setSex(User.SEX_MALE).query(UserService.QUERY_TYPE_NORMAL);

 

我想,这样的设计如果在数学计算的时候,会显得有用得多,看这样一个算式:

ln(sin((x+y)的平方))

如果用传统的方式来实现的话,应该类似这样子:

 

 

Math.log(Math.sin(Math.sqrt(x + y)))

 

显然,它的可读性不如Fluent Interface来得好:

 

 

new MyNumber(x+y).sqrt().sin().log()

 

这样的例子还有很多,比如在JQuery中的使用,在EasyMock中的使用等等。看下面的例子,这样构建一个DOM树,是不是比单纯的字符串拼接,要好理解一些呢?

 

 

$("#div1")
    .div({id:"subDIV"})
        .h1("A Title")
            .a({href:"xxx"})
            .a()
        .h1()
    .div();
 

《CommandQuerySeperation》这篇文章把一个对象的方法大致分成下面两种:

 

Queries: Return a result and do not change the observable state of the system (are free of side effects).

Commands: Change the state of a system but do not return a value.

 

对于Fluent Interface而言,它的接口调用既改变了对象的状态,又返回了对象(this或其他),并不属于上面的两种类型。

 

文章系本人原创,转载请注明出处和作者

 

1 楼 mfkvfn 2012-02-07  
连缀式语法,不是早就有了么?
Hibernate中的Query就是这种。
jQuery将连缀式语法发扬光大了。
2 楼 该用户名已经存在 2012-02-07  
传说中的链式编程么?以前写Filter的时候用到过,我一直认为JDK中Map的put()方法和List的add()方法等应该使用这样的链式编程。
3 楼 该用户名已经存在 2012-02-07  
JDK中Map的put()方法和List的add()方法等返回的是被放进去的那个Object,一直不知道返回这个东西有什么好处?倒不如直接返回this?火哥指教。
4 楼 wait10000y 2012-02-07  
又要自定义规则了,去推广吧,等你还没有被认同,又被代替了!关于接口设计,再有Fluent Interface,这种有趣的接口设计风格
5 楼 RayChase 2012-02-07  
mfkvfn 写道
连缀式语法,不是早就有了么?
Hibernate中的Query就是这种。
jQuery将连缀式语法发扬光大了。

大家似乎只注意到了最后提到的Fluent Interface。
是的,其实很早就有了,最早还是Martin Fowler提出来的概念,有兴趣的话可以去找一下他的blog,我记得里面有。
6 楼 RayChase 2012-02-07  
该用户名已经存在 写道
JDK中Map的put()方法和List的add()方法等返回的是被放进去的那个Object,一直不知道返回这个东西有什么好处?倒不如直接返回this?火哥指教。

对于List来说,你是不是记错了,add返回的是布尔型。
对于Map来说,你可能也有一个误解,返回Value对象可不是做一件无所谓的事情,这个Value可不是你放入的这个Value啊:
See the Documentation of Class Map:
return the previous value associated with key, or null if there was no mapping for key.(A null return can also indicate that the map previously associated null with key, if the implementation supports null values.)
7 楼 everyday1985 2012-02-07  
貌似这种方式早就有了吧,builder经常这么用,Effective Java里就有些,还有jdom和dom4j里面的format
8 楼 NicholasBugs 2012-02-14  
把setName, setAge 重命名为 name, age这样代码就变成了 user.name("Tom").age(18).query(); 更接近自然语言。
9 楼 RayChase 2012-02-15  
NicholasBugs 写道
把setName, setAge 重命名为 name, age这样代码就变成了 user.name("Tom").age(18).query(); 更接近自然语言。

关于接口设计,再有Fluent Interface,这种有趣的接口设计风格