间章:聊聊Fluent
很多初学者也许是在Vert.x中才开始接触到Fluent
的概念,而官方文章中也只是只言片语,简单到“哭”,以至于很多开发人员不知道这种设计的意义以及好处,所以很多时候就只是一个思维划过天空:好吧,用了就用了。最初这个章节的名字叫做“Fluent哲学”,后来觉得我还没有办法将Fluent的知识整理到那么高的高度,所以最终大家一起聊聊吧,用谦卑的姿态对待软件技术您将得到谦卑的结果。下边这段代码取自于官方:
request.response().putHeader("Content-Type", "text/plain").write("some text").end();
然后文档就说,这种方法就是Fluent的,然后就木有然后了,您也可以拆开来写:
HttpServerResponse response = request.response();
response.putHeader("Content-Type", "text/plain");
response.write("some text");
response.end();
然后它告诉你,我们不强制您使用Fluent的方式来写,就没有了下文……(这也是很多软件官方文档不照顾初学者的原因,如果我是一个初学者,我就只记住了:好吧这就是Fluent,如何自己来写或者自己实现,得了吧,我只是软件小白,我只会用。)
1.再谈Fluent
那么什么样的代码称为Fluent呢,先看看Fluent的翻译:流利的、流畅的,实际上我更多时候将Fluent的代码称为“流”,而每次调用方法时都具有绝对的“安全性”,先总结一下Fluent的特征:
- 每次调用了这种方法后返回值为原始对象引用;
- 调用这种方法会产生一定的效果(可能由原始对象做一些行为、或者改变原始对象的状态);
- 它可以反复重复流畅地多次调用对应的API;
好吧,看了上边的总结,您是不是对Fluent的API有了一定的认识了呢?那就让我们通过两个例子来对比一下。
1.1.伪装的Fluent
最容易混淆的一种为“引用替换”,这种方式在处理XML Schema(xjc)的代码时会被系统自动生成,然后也可以在主代码中形成“流”,但是这种方式不是Fluent的,为什么要在本书中将这种模式的代码抽取出来单独讲解,主要是帮助大家理解Fluent。
Role
package io.vertx.up._02.fluent;
public class Role {
private String name;
public String getName() {
return this.name;
}
public void setName(final String name) {
this.name = name;
}
}
User
package io.vertx.up._02.fluent;
public class User {
private String username;
private Role role;
public String getUsername() {
return this.username;
}
public User setUsername(final String username) {
this.username = username;
return this;
}
public Role getRole() {
return this.role;
}
public Role setRole(final Role role) {
this.role = role;
return this.role;
}
}
NoFluent
package io.vertx.up._02.fluent;
public class NoFluent {
private static void main(final String[] args) {
final User user = new User();
// 潜在的Bug
user.setUsername("Lang").getRole().setName("XXXXX");
}
}
细心的读者会发现,上边的代码,对于原始引用user
而言,一行代码调用了三个API,分别是:setUsername
、getRole
、setName
,从形式上看来,有点Fluent的味道,但是它不是Fluent的,反而会引起NullPointerException
的异常信息,因为在getRole()
这个API调用过后,返回的对象引用类型已经是Role,那么这样的方式会让一些读者觉得,好吧,只要我链式代码形成了就Fluent了,这就错了。
1.2. Fluent的书写
看了上边的反例,那么如何书写正常的Fluent呢?实际上上边例子中的setUsername
可以称为Fluent的API,由于它本身具备了Fluent上边总结的特性,那么对于书写Fluent就没有想象中那么复杂了。
User
package io.vertx.up._02.fluent;
public class User1 {
private String username;
private String email;
private Role role;
public User1 setRole(final Role role) {
this.role = role;
return this;
}
public User1 setEmail(final String email) {
this.email = email;
return this;
}
public User1 setUsername(final String username) {
this.username = username;
return this;
}
}
Fluent
package io.vertx.up._02.fluent;
public class Fluent {
public static void main(final String[] args) {
final User1 user = new User1()
.setEmail("[email protected]")
.setRole(new Role())
.setUsername("Lang Yu");
}
}
仔细看一下调用的三个方法,setEmail, setRole, setUsername
都是Fluent的,有了上边的代码对比,那么读者了解Fluent了么?实际上对于Fluent的代码也可以使用Vert.x官方提到的第二种写法如:
final User1 user1 = new User1();
user1.setEmail("[email protected]");
user1.setRole(new Role());
user1.setUsername("Lang Yu");
这些API虽然没有形成链式调用,但最终改变了对象user1
的内部状态。
1.3.Fluent的好处
最后总结一下Fluent的API的好处是什么?根据上述的代码,我把Fluent的API的好处归纳成以下几点:
- 更符合响应式/流编程的风格:这一点怎么讲,实际上从用过Rxjava的读者而言这点一点都不陌生,每一次方法调用可以当做将当前对象的状态执行了一次转换,不同的是Rxjava中传入的是函数,而我们的代码例子中传入的是数据;
- 更流畅的代码风格:从这点意义上讲仁者见仁,智者见智,实际上使用了Fluent的API过后,您的代码将会变成很流畅的代码,因为它可以一直延续不断地往下调用,代码整体结构上更显得间接一点——这种说法类似于A让B做事:“Hi B,帮我拿一下剪刀、白纸和画笔。”,而非Fluent的就类似于:“Hi B,帮我拿一下剪刀;Hi B,帮我拿一下白纸;Hi B,帮我拿一下画笔。”,实际上我个人觉得这种写法更符合“文学化编程”的风格,使得代码更简洁。
- Bug区域的偏向性:这点很容易理解了,如果出现了这样一行代码:
user.setUsername(role.getUsername())
,当这一行的代码抛出NullPointerException
时,有可能为空的是user,也有可能是role。如果setUsername
只是位于Fluent API调用链的节点上,那么就可以将异常信息直接定位到role一边,由于调用全部返回了“自引用”,一般都是输入异常,而不是调用异常(开脑洞觉得NullPointerException
来自于setUsername内部的逻辑除外),所以对这一点我的理解是:缩小思考区域吧,虽然有点牵强,但确实是Fluent帮助我们带来的一种考虑。
2.Vert.x中的Fluent
实际上在Vert.x中,随处可见的都是Fluent的代码,不仅仅如此,当您写了一个接口方法时,您甚至可以将这种代码直接使用@Fluent
来进行限定,Vert.x中有工具箱针对这样的代码进行检查,代码形如:
import io.vertx.codegen.annotations.Fluent;
......
@Fluent
@Override
QiyClient init(final JsonObject params);
......
而高频使用的Fluent的代码就是JsonObject和JsonArray
两个数据结构类,看看下边的代码:
@Address(Addr.TOPICS_SUBSCRIBE)
public Future<JsonArray> subscribe(final Envelop envelop) {
final JsonObject pager = Ux.getBody(envelop);
// Fluent调用
final JsonObject filter = new JsonObject().put("userId", Extractor.getUserId(envelop));
LOGGER.info("[ Query ] Filters : {0}", filter);
return this.subscribe.query(TargetType.TOPIC, filter,
MongoReadOpts.toFull(Ux.toPager(pager), Ux.toSorter("subscribeTime", false)));
}
上述代码中的JsonObject对象filter
在初始化时候就被Fluent了一下,而Vert.x中的其他很多类也是Fluent的,比如:
// Web中的Router
Route route = router.route().path("/some/path/");
// 上述代码可以改写成
Route route = router.route();
route.path("/some/path/");
// Socket
BridgeOptions options = new BridgeOptions().addInboundPermitted(inboundPermitted);
// 上述代码可以改写成
BridgeOptions options = new BridgeOptions();
options.addInboundPermitted(inboundPermitted);
// OAuth中的Provider
OAuth2Auth authProvider = OAuth2Auth.create(vertx, OAuth2FlowType.AUTH_CODE, new OAuth2ClientOptions()
.setClientID("CLIENT_ID")
.setClientSecret("CLIENT_SECRET")
.setSite("https://accounts.google.com")
.setTokenPath("https://www.googleapis.com/oauth2/v3/token")
.setAuthorizationPath("/o/oauth2/auth"));
// 上述代码可以改写成:
OAuth2ClientOptions options = new OAuth2ClientOptions();
options.setClientID("CLIENT_ID");
options.setClientSecret("CLIENT_SECRET");
options.setSite("https://accounts.google.com");
options.setTokenPath("https://www.googleapis.com/oauth2/v3/token");
options.setAuthorizationPath("/o/oauth2/auth");
OAuth2Auth authProvider = OAuth2Auth.create(vertx, OAuth2FlowType.AUTH_CODE, options);
3.总结
扯了这么多和Fluent相关的代码,那么最终想要传达给读者的是什么呢?实际上这个章节充其量只能是一个间章,为了告诉读者在Vert.x中有一种新的(其实不算新)编程风格,就是提倡尽可能尝试用Fluent的方式去编写程序,不论您是自己实现这种Fluent还是直接使用Vert.x本身拥有的Fluent,那么您都需要理解Vert.x官方提到的Fluent风格的流畅代码是怎么回事,因为后续很多代码您都会遇到这样风格的源码,就当做是对读者知识的补充和解读,方便大家阅读代码。