前言 最近看了【阿里技术】微信公众号的推文《函数式编程的Java编码实践:利用惰性写出高性能且抽象的代码》 ,觉得其编码方式很值得学习,故记录在此。本文侧重讲述实践过程,原文中 “函子”、“单子”、“柯里化” 等概念不做细致探究。
一、编程语言的严格(Strict)与惰性(Lazy) Java 是一门严格的编程语言,我们习惯变量在定义时就完成了初值计算,如:
1 2 int a = 10 + 1 ;int b = a + 1 ;
这里的变量 a 在定义时就已经完成了初值计算,定义变量 b 时使用的变量 a 的值已经计算好了。而其他编程语言,如 Haskell 则是在变量使用时才进行计算,若想在 Java 中实现类似的惰性计算,则可以借助 Java8 以后提供的函数式接口 Supplier 来实现,那么上述代码改写如下:
1 2 Supplier<Integer> a = () -> 10 + 1 ; int b = a.get() + 1 ;
此时,变量 a 在定义的时候不会计算,只有在运行到定义变量 b 的时候才会去计算变量 a 的值,则变量 a 就实现了惰性。但使用 Supplier 存在一个问题,每次调用其 get() 方法时都会重新计算,真正的惰性应该在第一次求值计算后把结果缓存下来,此后再次调用 get() 直接使用缓存,因此对 Supplier 稍作包装:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class Lazy <T> implements Supplier <T> { private final Supplier<? extends T > 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 的代码:
1 2 3 4 5 Lazy<Integer> a = Lazy.of(() -> 10 + 1 ); int b = a.get() + 1 ;int c = a.get();
在第一次调用 get() 方法时,变量 a 会进行一次计算,第二次调用 get() 方法时,就直接从缓存(value) 中取值。
二、实战模拟 1 2 3 4 5 6 7 8 9 10 11 12 13 public class User { private Long uid; private String department; 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 变成懒加载字段,在外部需要使用的时候才进行调用,从而实现惰性求值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 public class User { private Long uid; private Lazy<String> department; 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(); } 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() + '}' ; } }
这样做有很多好处:
业务建模只需要考虑贴合业务,而不需要考虑底层的性能问题,真正实现业务层和物理层的解耦;
业务逻辑与外部调用分离,无论外部接口如何变化,我们总是有一层适配层保证核心逻辑的稳定;
业务逻辑看起来就是纯粹的实体操作,易于编写单元测试,保障核心逻辑的正确性。
接着我们构造一个可以自动优化性能的实体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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; } }
再看看实际使用情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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()); } }
控制台打印如下:
1 2 3 4 5 6 7 8 9 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 调用,而是使用了缓存值。
本文是对阿里公众号推文的一次实践记录,可以通过本文顶部的链接去查看原文,本文完整的示例代码已上传至 Gitee 。