---
url: /article/a02yfsh5/index.md
---
## 前言

最近看了【阿里技术】微信公众号的推文[《函数式编程的Java编码实践：利用惰性写出高性能且抽象的代码》](https://mp.weixin.qq.com/s/e-9hrjWK513VJqqyeGLxrQ)，觉得其编码方式很值得学习，故记录在此。本文侧重讲述实践过程，原文中 “函子”、“单子”、“柯里化” 等概念不做细致探究。

## 1. 编程语言的严格（Strict）与惰性（Lazy）

Java 是一门严格的编程语言，我们习惯变量在定义时就完成了初值计算，如：

```java
int a = 10 + 1;
int b = a + 1;
```

这里的变量 a 在定义时就已经完成了初值计算，定义变量 b 时使用的变量 a 的值已经计算好了。而其他编程语言，如 **Haskell** 则是在变量使用时才进行计算，若想在 Java 中实现类似的惰性计算，则可以借助 Java8 以后提供的函数式接口 **Supplier** 来实现，那么上述代码改写如下：

```java
Supplier<Integer> a = () -> 10 + 1;
int b = a.get() + 1;
```

此时，变量 a 在定义的时候不会计算，只有在运行到定义变量 b 的时候才会去计算变量 a 的值，则变量 a 就实现了惰性。但使用 Supplier 存在一个问题，每次调用其 get() 方法时都会重新计算，真正的惰性应该在第一次求值计算后把结果缓存下来，此后再次调用 get() 直接使用缓存，因此对 Supplier 稍作包装：

```java
public class Lazy<T> implements Supplier<T> {
    private final Supplier<? extends T> supplier;

    // 利用 value 属性缓存 supplier 计算后的值
    private T value;

    private Lazy(Supplier<? extends T> supplier) {
        this.supplier = supplier;
    }

    public static <T> Lazy<T> of(Supplier<? extends T> supplier) {
        return new Lazy<>(supplier);
    }

    public T get() {
        if (value == null) {
            T newValue = supplier.get();
            if (newValue == null) {
                throw new IllegalStateException("Lazy value can not be null!");
            }
            value = newValue;
        }
        return value;
    }

    public <S> Lazy<S> map(Function<? super T, ? extends S> function) {
        return Lazy.of(() -> function.apply(get()));
    }

    public <S> Lazy<S> flatMap(Function<? super T, Lazy<? extends S>> function) {
        return Lazy.of(() -> function.apply(get()).get());
    }
}
```

我们定义了一个类 Lazy，并使其实现 Supplier 接口，以便与标准的 Java 函数式接口交互，通过 Lazy 再来改写之前定义变量 a,b 的代码：

```java

Lazy<Integer> a = Lazy.of(() -> 10 + 1);
int b = a.get() + 1;
// get 不会再重新计算, 直接用缓存的值
int c = a.get();
```

在第一次调用 get() 方法时，变量 a 会进行一次计算，第二次调用 get() 方法时，就直接从缓存(value) 中取值。

## 2. 实战练习

```java
public class User {
    // 用户 id
    private Long uid;
    // 用户的部门，为了保持示例简单，这里就用普通的字符串
    // 需要远程调用 通讯录系统 获得
    private String department;
    // 用户的主管，为了保持示例简单，这里就用一个 id 表示
    // 需要远程调用 通讯录系统 获得
    private Long supervisor;
    // 用户所持有的权限
    // 需要远程调用 权限系统 获得
    private Set<String> permission;
}
```

我们来看一个系统抽象中常见的实体 - “用户”，为了在系统中“描述”用户，我们定义一个领域模型 User，除 用户id 以外还包含其他信息，department(部门)、supervisor(主管id)、permission(权限)，它们是通过 Rpc 调用获得的。我们把用户相关的所有信息都放在一个实体里，这样在使用用户的相关信息时就不需要再去从其他地方获得了，这看起来很棒，通常我们也是这样做的，然而每次我们在构造 User 对象时都需要付出三次 Rpc 调用的代价，即使我们没有用到那些信息。而一但其他服务出现宕机，还会影响无关接口的稳定性。或许你可以说，在构造 User 的时候不给 department, supervisor, permission 赋值就好了，在需要使用的时候，再去通过 uid 调用相应的 Rpc 服务获取。那这样就会使裸露的 uid 弥漫在系统中，系统到处散落着用户信息查询的代码，可谓是遍地开花。
因此，我们对模型 User 进行改造，将 department, supervisor, permission 变成懒加载字段，在外部需要使用的时候才进行调用，从而实现惰性求值：

```java
public class User {
    // 用户 id
    private Long uid;

    // 用户的部门，为了保持示例简单，这里就用普通的字符串
    // 需要远程调用 通讯录系统 获得
    private Lazy<String> department;
    // 用户的主管，为了保持示例简单，这里就用一个 id 表示
    // 需要远程调用 通讯录系统 获得
    private Lazy<Long> supervisor;
    // 用户所持有的权限
    // 需要远程调用 权限系统 获得
    private Lazy<Set<String>> permission;

    public Long getUid() {
        return uid;
    }

    public void setUid(Long uid) {
        this.uid = uid;
    }

    public String getDepartment() {
        return department.get();
    }

    /**
     * 因为 department 是一个惰性加载的属性，所以 set 方法必须传入计算函数，而不是具体值
     */
    public void setDepartment(Lazy<String> department) {
        this.department = department;
    }

    public Long getSupervisor() {
        return supervisor.get();
    }

    public void setSupervisor(Lazy<Long> supervisor) {
        this.supervisor = supervisor;
    }

    public Set<String> getPermission() {
        return permission.get();
    }

    public void setPermission(Lazy<Set<String>> permission) {
        this.permission = permission;
    }

    @Override
    public String toString() {
        return "User{" +
                "uid=" + uid +
                ", department=" + department.get() +
                ", supervisor=" + supervisor.get() +
                ", permission=" + permission.get() +
                '}';
    }
}
```

这样做有很多好处：

* 业务建模只需要考虑贴合业务，而不需要考虑底层的性能问题，真正实现业务层和物理层的解耦；
* 业务逻辑与外部调用分离，无论外部接口如何变化，我们总是有一层适配层保证核心逻辑的稳定；
* 业务逻辑看起来就是纯粹的实体操作，易于编写单元测试，保障核心逻辑的正确性。

接着我们构造一个可以自动优化性能的实体：

```java
public class UserFactory {

    // 假设注入
    private DepartmentService departmentService = new DepartmentService();
    private SupervisorService supervisorService = new SupervisorService();
    private PermissionsService permissionsService = new PermissionsService();

    public User build(Long uid) {
        Lazy<String> departmentLazy = Lazy.of(() -> departmentService.getDepartment(uid));
        Lazy<Long> supervisorLazy = departmentLazy.map(supervisorService::getSupervisor);
        Lazy<Set<String>> permissions = departmentLazy.flatMap(department -> supervisorLazy.map(supervisor -> permissionsService.getPermissions(department, supervisor)));

        User user = new User();
        user.setUid(uid);
        user.setDepartment(departmentLazy);
        user.setSupervisor(supervisorLazy);
        user.setPermission(permissions);

        return user;
    }
}
```

再看看实际使用情况：

```java
public class Main {

    public static void main(String[] args) {
        // 假设为注入
        UserFactory userFactory = new UserFactory();

        User user = userFactory.build(1L);

        System.out.println(user.getDepartment());
        System.out.println(user.getSupervisor());
        System.out.println(user.getPermission());
        // 以上结果可以证明惰性求值和值缓存
    }
}
```

控制台打印如下：

```text
11月 04, 2021 10:03:40 下午 com.mayee.service.DepartmentService getDepartment
信息: rpc 调用 department 服务...
云计算部门
11月 04, 2021 10:03:41 下午 com.mayee.service.SupervisorService getSupervisor
信息: rpc 调用 supervisor 服务...
1
11月 04, 2021 10:03:41 下午 com.mayee.service.PermissionsService getPermissions
信息: rpc 调用 permissions 服务...
[管理员]
```

我们可以看到，当获取 department 时，并没有 Rpc 调用获取 supervisor 和 permission，实现了惰性计算；当获取 permission 时，department 和 supervisor 并没有再次进行 Rpc 调用，而是使用了缓存值。

***

Tip：本文完整示例代码已上传至 [GitHub](https://github.com/Blogeee/blog-java/tree/main/_20211102)

***

::: note 本文参考

* [《函数式编程的Java编码实践：利用惰性写出高性能且抽象的代码》](https://mp.weixin.qq.com/s/e-9hrjWK513VJqqyeGLxrQ)

:::
