拥抱流畅接口模式,6个例子提升你的代码能力

457 阅读5分钟

在软件设计中,代码的可读性常常与功能性同等重要。当开发者面对一段复杂的业务逻辑时,如果代码读起来像自然语言一样流畅,理解和维护成本将大幅降低。流畅接口(Fluent Interface)正是这样一种设计模式,它将代码转化为“可阅读的语句”,具有可读性的同时具有流式编程的特点。流畅接口由 Martin FowlerEric 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);

流畅接口:链式组合异步任务

image.png 相比于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可以避免和简化这种问题。这里体现了流畅接口的优点,可以辅助开发者避免编写错误代码。