在软件设计中,代码的可读性常常与功能性同等重要。当开发者面对一段复杂的业务逻辑时,如果代码读起来像自然语言一样流畅,理解和维护成本将大幅降低。流畅接口(Fluent Interface)正是这样一种设计模式,它将代码转化为“可阅读的语句”,具有可读性的同时具有流式编程的特点。流畅接口由 Martin Fowler 和 Eric Evans在2005年提出,其核心思想是通过链式方法调用(Method Chaining)和领域特定语言(DSL)的设计,让代码的连续操作形成一个连贯的“语言流”。
流畅接口的设计原则与特点
方法链(Method Chaining)
每个方法返回对象自身或下一个阶段性状态,支持连续调用,通常存在最终方法获得结果。执行流程的拆分。
可读性优先:代码即文档
无需注释,代码本身即表达业务逻辑。方法的命名和顺序需符合业务场景的逻辑,反之,如果可读性一般或者命名有歧义的话,流畅接口反而体现不出优势,终将被淘汰。
减少冗余
消除临时变量和重复的对象调用。比如JavaBean的反复setter,相比之下,builder模式的实现相当优雅。
开发友好
IDE的自动补全功能可引导开发者写出正确代码。通过.操作可以极大降低开发者的心智负担。
“异常”处理
快速失败(Fail-Fast)与错误收集。很多流畅接口自身是 monad,天然具有快速失败的特点。
性能不差
Stream的性能不一定比命令式差。Flogger实现性能比普通Logger要好。
领域特定语言(DSL)导向
针对特定业务领域,将API设计为接近自然语言的表达,通常将API动词映射到业务概念需要多维度设计。可以参考 SpringSecurity 的配置实现。这种实现需要开发者付出相当大的学习成本。
偏函数
Scala中偏函数指的是xxx,链式调用中常常使用这种技巧,对于部分数据进行处理。
val negativeOrZeroToPositive: PartialFunction[Int, Int] = { case x if x <= 0 => Math.abs(x) }
val positiveToNegative: PartialFunction[Int, Int] = { case x if x > 0 => -1 * x }
val swapSign: PartialFunction[Int, Int] = { positiveToNegative orElse negativeOrZeroToPositive }
类似的,Java11对Optional的增强就设计了一个方法处理存在值和不存在值两种场景:
Optional.of("Hello, World!").ifPresentOrElse(value -> System.out.println("Value is present: " + value), () -> System.out.println("Value is not present"));
Guava Futures 支持 catching 部分处理异步异常:
Downloader.download(url)
.toFile(path) // 返回FileConfigurator
.onSuccess(() -> log("完成")) // 返回EventConfigurator
.onFailure(e -> alert("失败")); // 不可再调用onSuccess
缺点
- 设计复杂度高:需精心规划方法链的顺序和上下文,涉及的编程技巧和难题如:
- 自限定模式,设计方便的拓展接口,对于大任务的拆分,给出更好的异常信息。
- 调试困难:长链式调用可能让异常栈信息难以追踪。一个很有效的技巧是每一个操作都写在新行中,这样方便大断点;链式调用应该组成一个完整的逻辑实体。
- 代码量大。技巧是通过使用一些工具降低工作量,比如:Lombok, AutoValue, Immutables。
测试断言
例子1:AssertJ vs JUnit Assertion
传统断言:机械式调用
assertEquals(expected, actual);
assertTrue(list.contains("a"));
assertNotNull(user.getProfile());
流畅断言:链式语义化验证
assertThat(actual)
.isEqualTo(expected)
.hasSize(3)
.contains("a", "b");
assertThat(user)
.isNotNull()
.hasFieldOrProperty("profile")
.matches(u -> u.isActive());
使用流畅断言可以对于单个对象进行多次验证,轻松实现更复杂的功能。
举例2:字符串处理
String#split vs Splitter
String[] parts = "a,,b, ".split(","); // 结果: ["a", "", "b", " "],包含空值和空格
基础API表达力有限,一些corner case 需要开发者额外注意。
流畅配置:显式控制分割逻辑
List<String> parts = Splitter.on(',')
.trimResults() // 去除空格
.omitEmptyStrings() // 跳过空值
.splitToList("a,,b, "); // 结果: ["a", "b"]
举例3:Guava 异步任务功能
即 Futures.addCallback vs FluentFuture
传统异步任务组合
// 官方示例
ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
ListenableFuture<Explosion> explosion = service.submit(
new Callable<Explosion>() {
public Explosion call() {
return pushBigRedButton();
}
});
// 使用工具类添加回调
Futures.addCallback(
explosion,
new FutureCallback<Explosion>() {
// we want this handler to run immediately after we push the big red button!
public void onSuccess(Explosion explosion) {
walkAwayFrom(explosion);
}
public void onFailure(Throwable thrown) {
battleArchNemesis(); // escaped the explosion!
}
},
service);
流畅接口:链式组合异步任务
相比于CompletableFuture,FluentFuture只有6个示例方法(不考虑Object定义的方法),相当简单易上手。
FluentFuture.from(dataFuture)
.transformAsync(this::processAsync, executor)
.transform(this::formatResult, executor)
.addListener(() -> log("Done"), executor);
举例4:流式编程(面向管道编程)
// 静态方法调用,代码碎片化
List<String> filtered = Lists.newArrayList(
Iterables.filter(
Iterables.transform(users, User::getName),
name -> name.startsWith("A")
)
);
// Guava FluentIterable,最大的特点是可以重复计算,可以自定义一些视图
FluentIterable.from(users)
.transform(User::getName)
.filter(name -> name.startsWith("A"))
.toList();
// 优先使用 Stream
List<String> names = users.stream()
.filter(u -> u.getAge() > 18)
.map(User::getName)
.map(String::toUpperCase)
.collect(Collectors.toList());
举例5: SpringSecurity 配置
以下是官方最新的配置示例,典型的DSL,和就有实现相比,虽然做了一些简化,但是学习成长依然很高。从最简单的角度来说,开发者无法通过. + IDE提示完成代码编写。一个简单的想法:Customizer 似乎可以使用withDefaultXxx实现。
@Bean
public SecurityFilterChain securedFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/secured/**")
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/secured/user").hasRole("USER")
.requestMatchers("/secured/admin").hasRole("ADMIN")
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults());
return http.build();
}
举例6: CFFU vs CompletableFuture
cffu是一个小小的CompletableFuture(CF)辅助增强库,其提供了工具类实现和流程接口实现两种模式。对于不同的开发者可以灵活选择自己喜欢的模式。
比如我们需要实现并发 map,这个功能在 Stream、CompletableFuture 中并不存在原生支持,在cffu类库中可以轻松实现:
// just run multiple actions, fresh and cool 😋
CompletableFutureUtils.thenMApplyFailFastAsync(
completedFuture(42),
v -> v + 1,
v -> v + 2,
v -> v + 3
).thenAccept(System.out::println);
// output: [43, 44, 45]
cffuFactory.completedFuture(42).thenMApplyAllSuccessAsync(
-1,
v -> v + 1,
v -> v + 2,
v -> v + 3
).thenAccept(System.out::println);
// output: [43, 44, 45]
这个例子中,使用CFFU时,用户只需要在下一步操作时使用. + IDE提示即可。 第一段代码实际上有点容易忽略的问题,笔者不止一次见到生产代码中*Async方法没有显示传递执行器,甚至很多人以为只需要传递一次执行器即可。正确的实践是每次都需要传递执行器/线程池,使用cffu可以避免和简化这种问题。这里体现了流畅接口的优点,可以辅助开发者避免编写错误代码。