
作者 | 悬衡
本文会以惰性加载为例一步步介绍函数式编程中各种概念,所以读者不需要任何函数式编程的基础,只需要对 Java 8 有些许了解即可。
一 抽象一定会导致代码性能降低?
程序员的梦想就是能写出 “高内聚,低耦合”的代码,但从经验上来看,越抽象的代码往往意味着越低的性能。机器可以直接执行的汇编性能最强,C 语言其次,Java 因为较高的抽象层次导致性能更低。业务系统也受到同样的规律制约,底层的数增删改查接口性能最高,上层业务接口,因为增加了各种业务校验,以及消息发送,导致性能较低。
对性能的顾虑,也制约程序员对于模块更加合理的抽象。
一起来看一个常见的系统抽象,“用户” 是系统中常见的一个实体,为了统一系统中的 “用户” 抽象,我们定义了一个通用领域模型 User,除了用户的 id 外,还含有部门信息,用户的主管等等,这些都是常常在系统中聚合在一起使用的属性:
public class User {
// 用户 id
private Long uid;
// 用户的部门,为了保持示例简单,这里就用普通的字符串
// 需要远程调用 通讯录系统 获得
private String department;
// 用户的主管,为了保持示例简单,这里就用一个 id 表示
// 需要远程调用 通讯录系统 获得
private Long supervisor;
// 用户所持有的权限
// 需要远程调用 权限系统 获得
private Set< String> permission;
}
这看起来非常棒,“用户“常用的属性全部集中到了一个实体里,只要将这个 User 作为方法的参数,这个方法基本就不再需要查询其他用户信息了。但是一旦实施起来就会发现问题,部门和主管信息需要远程调用通讯录系统获得,权限需要远程调用权限系统获得,每次构造 User 都必须付出这两次远程调用的代价,即使有的信息没有用到。比如下面的方法就展示了这种情况(判断一个用户是否是另一个用户的主管):
public boolean isSupervisor(User u1, User u2) {
return Objects.equals(u1.getSupervisor(), u2.getUid());
}
为了能在上面这个方法参数中使用通用 User 实体,必须付出额外的代价:远程调用获得完全用不到的权限信息,如果权限系统出现了问题,还会影响无关接口的稳定性。
想到这里我们可能就想要放弃通用实体的方案了,让裸露的 uid 弥漫在系统中,在系统各处散落用户信息查询代码。
其实稍作改进就可以继续使用上面的抽象,只需要将 department, supervisor 和 permission 全部变成惰性加载的字段,在需要的时候才进行外部调用获得,这样做有非常多的好处:
-
业务建模只需要考虑贴合业务,而不需要考虑底层的性能问题,真正实现业务层和物理层的解耦
-
业务逻辑与外部调用分离,无论外部接口如何变化,我们总是有一层适配层保证核心逻辑的稳定
-
业务逻辑看起来就是纯粹的实体操作,易于编写单元测试,保障核心逻辑的正确性
二 严格与惰性:Java 8 的 Supplier 的本质
Supplier< Integer> a = () -> 10 + 1;
int b = a.get() + 1;
三 Supplier 的进一步优化:Lazy
/**
* 为了方便与标准的 Java 函数式接口交互,Lazy 也实现了 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;
}
}
Lazy< Integer> a = Lazy.of(() -> 10 + 1);
int b = a.get() + 1;
// get 不会再重新计算, 直接用缓存的值
int c = a.get();
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;
}
// ... 后面类似的省略
}
Long uid = 1L;
User user = new User();
user.setUid(uid);
// departmentService 是一个rpc调用
user.setDepartment(Lazy.of(() -> departmentService.getDepartment(uid)));
// ....
String department = departmentService.getDepartment(uid);
Long supervisor = SupervisorService.getSupervisor(department);
四 Lazy 实现函子(Functor)
1 函子的计算对象
2 函子的定义
// 反例,不能成为函子,因为这个方法没有在盒子中如实反映 function 的映射关系
public Box< S> map(Function< T,S> function) {
return new Box< >(null);
}
-
单位元律:Box< T> 在应用了恒等函数后,值不会改变,即 box.equals(box.map(Function.identity()))始终成立(这里的 equals 只是想表达的一个数学上相等的含义)
-
复合律:假设有两个函数 f1 和 f2,map(x -> f2(f1(x))) 和 map(f1).map(f2) 始终等价
3 Lazy 函子
public < S> Lazy< S> map(Function< ? super T, ? extends S> function) {
return Lazy.of(() -> function.apply(get()));
}
Lazy< String> departmentLazy = Lazy.of(() -> departmentService.getDepartment(uid));
Lazy< Long> supervisorLazy = departmentLazy.map(
department -> SupervisorService.getSupervisor(department)
);
4 遇到了更加棘手的情况
Lazy< Lazy< Set< String>>> permissions = departmentLazy.map(department ->
supervisorLazy.map(supervisor -> getPermissions(department, supervisor))
);
Lazy< Long> param1Lazy = Lazy.of(() -> 2L);
Lazy< Long> param2Lazy = Lazy.of(() -> 2L);
Lazy< Long> param3Lazy = Lazy.of(() -> 2L);
Lazy< Lazy< Lazy< Long>>> result = param1Lazy.map(param1 ->
param2Lazy.map(param2 ->
param3Lazy.map(param3 -> param1 + param2 + param3)
)
);
五 Lazy 实现单子 (Monad)
1 单子的定义
2 Lazy 单子
public < S> Lazy< S> flatMap(Function< ? super T, Lazy< ? extends S>> function) {
return Lazy.of(() -> function.apply(get()).get());
}
Lazy< Set< String>> permissions = departmentLazy.flatMap(department ->
supervisorLazy.map(supervisor -> getPermissions(department, supervisor))
);
Lazy< Long> param1Lazy = Lazy.of(() -> 2L);
Lazy< Long> param2Lazy = Lazy.of(() -> 2L);
Lazy< Long> param3Lazy = Lazy.of(() -> 2L);
Lazy< Long> result = param1Lazy.flatMap(param1 ->
param2Lazy.flatMap(param2 ->
param3Lazy.map(param3 -> param1 + param2 + param3)
)
);
3 题外话:函数式语言中的单子语法糖
do
param1 < - param1Lazy
param2 < - param2Lazy
param3 < - param3Lazy
-- 注释: do 记法中 return 的含义和 Java 完全不一样
-- 它表示将值打包进盒子里,
-- 等价的 Java 写法是 Lazy.of(() -> param1 + param2 + param3)
return param1 + param2 + param3
六 Lazy 的最终代码
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());
}
}
七 构造一个能够自动优化性能的实体
@Component
public class UserFactory {
// 部门服务, rpc 接口
@Resource
private DepartmentService departmentService;
// 主管服务, rpc 接口
@Resource
private SupervisorService supervisorService;
// 权限服务, rpc 接口
@Resource
private PermissionService permissionService;
public User buildUser(long uid) {
Lazy< String> departmentLazy = Lazy.of(() -> departmentService.getDepartment(uid));
// 通过部门获得主管
// department -> supervisor
Lazy< Long> supervisorLazy = departmentLazy.map(
department -> SupervisorService.getSupervisor(department)
);
// 通过部门和主管获得权限
// department, supervisor -> permission
Lazy< Set< String>> permissionsLazy = departmentLazy.flatMap(department ->
supervisorLazy.map(
supervisor -> permissionService.getPermissions(department, supervisor)
)
);
User user = new User();
user.setUid(uid);
user.setDepartment(departmentLazy);
user.setSupervisor(supervisorLazy);
user.setPermissions(permissionsLazy);
}
}
八 异常处理
九 总结
十 题外话:Java 中缺失的柯里化与应用函子
// 注意,这里的 function 是装在 lazy 里面的
public < S> Lazy< S> apply(Lazy< Function< ? super T, ? extends S>> function) {
return Lazy.of(() -> function.get().apply(get()));
}
-- 注释: 结果为 box c
box f < *> box a < *> box b
在 Java 函数式类库 VAVR 中提供了类似的 Lazy 实现,不过如果只是为了用这个一个类的话,引入整个库还是有些重,可以利用本文的思路直接自己实现
函数式编程进阶:应用函子 前端角度的函数式编程文章,本文一定程度上参考了里面盒子的类比方法:https://juejin.cn/post/6891820537736069134?spm=ata.21736010.0.0.595242a7a98f3U
《Haskell函数式编程基础》
《Java函数式编程》
精彩推荐
小项目需要前后端分离吗?
分布式系统一致性测试框架 Jepsen 在女娲的实践应用


↓ 点击这里 查看更多技术干货!



