跳至主要內容

Java 函数式编程实现惰性求值

Mayee...大约 6 分钟

前言

最近看了【阿里技术】微信公众号的推文《函数式编程的Java编码实践:利用惰性写出高性能且抽象的代码》open in new window,觉得其编码方式很值得学习,故记录在此。本文侧重讲述实践过程,原文中 “函子”、“单子”、“柯里化” 等概念不做细致探究。

1. 编程语言的严格(Strict)与惰性(Lazy)

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

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

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

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

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

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 的代码:


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

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

2. 实战练习

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 变成懒加载字段,在外部需要使用的时候才进行调用,从而实现惰性求值:

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() +
                '}';
    }
}

这样做有很多好处:

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

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

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;
    }
}

再看看实际使用情况:

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());
        // 以上结果可以证明惰性求值和值缓存
    }
}

控制台打印如下:

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 调用,而是使用了缓存值。

本文是对阿里公众号推文的一次实践记录,可以通过本文顶部的链接去查看原文,本文完整的示例代码已上传至 Giteeopen in new window