间章:聊聊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,分别是:setUsernamegetRolesetName,从形式上看来,有点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风格的流畅代码是怎么回事,因为后续很多代码您都会遇到这样风格的源码,就当做是对读者知识的补充和解读,方便大家阅读代码。

results matching ""

    No results matching ""