本文以惰性加载为例一步步介绍函数型编程的各种概念,所以读者不需要函数型编程的基础,对Java 8稍加了解就可以了。
抽象肯定会降低代码的性能吗? 程序员的梦想是能写出“高凝聚、低耦合”的代码,但根据经验,越是抽象的代码往往意味着性能越低。 机器可以直接执行的程序集的性能最高,而Java的性能较低,因为其抽象级别仅次于C语言。 业务系统也受到同样的规则约束,底层数量的增删检查接口性能最高,上层业务接口由于增加了各种业务检查和消息发送,性能较低。 对性能的担忧制约了程序员对模块更合理的抽象。 从典型的系统抽象来看,“用户”是系统中常见的实体,为了统一系统中的“用户”抽象,定义了一个通用区域模型User,该模型除了用户标识外,还包括部门信息、用户管理员等。 这些属性始终是系统中一起使用的属性。
公共类用户{//用户id private Long uid; //用户部署为了方便维护样本,这里需要远程调用普通的字符串//通讯录系统获取私有字符串部署; //为了简化示例,管理员在此用id表示//必须远程调用通讯簿系统以获取私有长服务; //需要远程调用权限系统来获取用户拥有的权限private Set String permission这看起来非常棒。 “用户”常用的所有属性都集中在一个实体中,如果将此User作为方法的参数,则此方法基本上不需要查询其他用户信息。 但是一旦实施就会发现问题,部门和主管信息需要远程调用地址簿系统获取,权限需要远程调用权限系统获取,每次构建User都要付出这两次远程调用的代价。 例如,以下方法说明了这种情况。 确定一个用户是否是另一个用户的管理员。
publicbooleanissupervisor ( useru 1,User u2 ) { return objects.equals ( u1.get supervisor ),u2.getUid ) }; }在上述方法参数中使用通用User实例需要额外的成本。 远程调用获取不完全可用的权限信息,如果权限系统有问题,也会影响无关接口的稳定性。 想到这里,我们可能会想放弃通用实体的方案。 使裸露的uid漂浮在系统上,在系统各处散布用户信息查询代码。 实际上,稍微改进一下就可以继续使用上述抽象。 只需将department、supervisor和permission都设置为惰性加载字段,然后在需要时进行外部调用以进行检索,就可以获得非常多的好处。
在业务建模中,应该只考虑匹配业务,而不是基本的性能问题。 业务层和物理层解耦实际实现业务逻辑和外部调用的分离。 无论外部接口如何变化,都有适配层保证核心逻辑稳定的业务逻辑在纯物理操作中可见,便于编写单元测试,保证核心逻辑的正确性,但在实践过程中可能会出现一些问题。 本文结合Java和函数型编程技巧,实现惰性加载工具类。
(二)严格和惰性) Java 8假脱机的本质Java 8引入了全新的函数接口假脱机。 从旧的Java程序员的角度来理解,它只是一个可以获取任意值的接口。 Lambda只是这种接口实现系统的语法糖。 这是站在语言而不是计算的角度上的理解。 理解了严格( strict )和惰性)的区别后,可能会有接近计算本质的看法。 由于Java和c都是严格的编程语言,所以我们习惯在定义变量的地方完成计算。 事实上,还有另一种编程语言流派,如函数式编程语言Haskell,在使用变量时进行计算。
因此,Supplier的本质是在Java语言中引入了惰性计算机制。 为了在Java中实现等价的惰性计算,可以写如下。
Supplier Integer a=( )- 10 1; int b=a.get ( ) 1; 三假脱机程序的进一步优化: Lazy假脱机程序还存在一个问题,即每次在get中获取值时都要重新计算。 真正的惯性计算应该在第一个get之后缓存值。 包装一点Supplier就可以了。
/**为了便于与标准Java函数类型接口进行交互,Lazy支持supplier */publicclasslazytimplementssuppliert { privatefinalsupplierextendstsuppliert } 用value属性缓存supplier计算的值private T value; private lazy ( supplierextendstsupplier ) { this.supplier=supplier; } publicstatictlazytof ( supplierextendstsupplier ) ) returnnewlazy ) supplier; } public T get ( ) if ) value==null ) { T newValue=supplier.get ); new value==null ) thrownewillegalstateexception ( ‘ lazyvaluecannotbenull!’ ); } value=newValue; } return value; }用Lazy编写以前的惯性计算代码:
Lazy Integer a=Lazy.of ( ) ) (-10 ); int b=a.get ( ) 1; //get不重新计算,缓存的值int c=a.get ( ); 使用此惰性加载工具类可以优化以前的常用用户实体。
公共类用户{//用户id private Long uid; //用户部署为了方便维护样本,这里需要用普通字符串//远程调用地址簿系统获取私有字符串; //为了简化示例,管理员在此用id表示//需要远程调用地址簿系统以获取私有lazy long supervisor; //需要远程调用权限系统以获取用户拥有的权限//privatelazysetstringpermission; 公共长getuid ( ) { return uid; }publicvoidsetuid(longuid ) { this.uid=uid; }公共字符串获取部署( ) { return department.get ); }/***department是惰性负载的属性,因此set方法是具体值*/publicvoidsetdepartment ( lazystringdepartment ) { this.department
Long uid=1L; 用户用户=new user (; user.setuid(uid; //departmentService是rpc调用user.setdepartment(lazy.of ( )-departmentservice.get department ) ); //…这个看起来很好,但是如果你继续深入使用,你会发现一些问题。 用户的两个属性部门和管理员是相关的,必须通过rpc界面获取用户部门,然后通过另一个rpc界面按部门获取管理员。 代码如下所示。
string department=departmentservice.get department ( uid; long supervisor=supervisorservice.get supervisor ( department ); 但是,现在department不是计算出的值,而是通过惯性计算出的Lazy对象。 上面的代码应该怎么写呢? “信”是为了解决这个问题
四Lazy实现函式( Functor )类似于Java的流API和选项映射方法。 函子可以理解为一个接口,map可以理解为接口中的方法。
1函子的计算对象Java的Collection T,Optional T,还有我们刚实现Lazy T,有一个共同的特征。 他们都有,只有一个通用参数。 我们在这篇文章里暂且称为箱子,记住Box T。 他们都像万能的容器,所以什么种类都可以打包。
双函数的定义函数运算可以将t映射到s的函数应用于Box T,使其成为Box S。 将Box数字转换为字符串的示例如下所示。
箱子里装的是模具而不是1和’1’的理由是,箱子里不一定是单一的值,比如集合,甚至是更复杂的多值映射关系。 需要注意的是,如果随便定义签名并满足boxsmap(functiont,S function ),Box T就不会成为函子。 以下是反例。
//是反例,不是函数。 这是因为,该方法在箱子中没有忠实地反映function的映射关系publicboxsmap(functiont,S function ) {returnnewbox ) null}。 }因此,函子比map法定义得更严密,他还要求map满足以下法则,称为函子法则。 (法则的本质是保障map法能够忠实地反映参数function所定义的映射关系)。
单位律: Box T应用恒等函数后,值不变。 即box.equals ( box.map ( function.identity ) ) )始终成立。 这里的equals是想表达的数学上相同的意思。 假设有两个函数f1和f2
3 Lazy函子介绍了这么多理论,实现起来非常简单:
publicslazysmap(functionsupert,extends S function ) { return lazy.of (-function.apply ) get ) }; }很容易证明它满足函子定律。 使用map可以轻松解决以前面临的挑战。 可以在获取了部门信息的情况下计算从map传递的函数。
lazystringdepartmentlazy=lazy.of ( )-departmentservice.get department ( uid ); lazylongsupervisorlazy=深度层.映射( department-supervisorservice.get supervisor )深度); 4遇到更麻烦的情况我们现在不仅可以制作惰性的值,还可以使用一个惰性的值计算另一个惰性的值,看起来很完美。 但是,如果使用得更深,就发现了更棘手的问题。 现在,调用权限系统获取权限需要部门和管理员两个参数,但部门和管理员的值都是惰性值。 首先用嵌套图试试:
lazylazysetstringpermissions=深度层.映射( department-supervisor lazy.map )深度层返回类型似乎有点奇怪。 我们期待的是Lazy Set String,但这里得到的是更多的层成为Lazy Lazy Set String。 然后,随着嵌套贴图层数的增加,Lazy的通用层也同样增加。 以下是三个参数的示例。
lazylongparam1lazy=lazy.of ( (-2l ); lazylongparam2lazy=lazy.of ( (-2l ); lazylongparam3lazy=lazy.of ( (-2l ); lazylazylazylongresult=param1lazy.map ( param1- param2lazy.map ) param2-param3lazy.map ) param3-param1param2param
五Lazy实现单子( Monad )的快速理解:与Java stream api和Optional的平面图功能相似
1列表的定义列表和函数的最大区别是接收的函数。 函数一般返回本机值,而列表函数返回装箱值。 下图中的function,如果使用map而不是flatmap的话,就会变成俄罗斯夹克——双层箱。
列表中当然也有列表定律,但比函数定律更复杂,在此不再赘述。 他的作用也类似于函数定律,允许flatmap忠实地反映函数的映射关系。
2 Lazy列表的实现也很简单:
使用publicslazysflatmap ( function super t,lazyextendssfunction ( { return lazy.of ) )-function.apply ( get ) )平面图
lazysetstringpermissions=深度层.平面图( department-supervisor lazy.map )深度层对于三个参数:
lazylongparam1lazy=lazy.of ( (-2l ); lazylongparam2lazy=lazy.of ( (-2l ); lazylongparam3lazy=lazy.of ( (-2l ); lazylongresult=param1lazy.flat map ( param1- param2lazy.flat map ) param2-param3lazy.map ) param3-param1param2param
3题外话:函数式语言列表语法糖看上面的例子会觉得惯性计算很麻烦吧。 为了取里面的惯性值,每次都要经历好几次平面贴图和贴图。 这实际上是因为Java不本机支持函数式编程而做出的妥协行为,Haskell支持用do符号简化Monad的运算。 上面的三个参数的例子使用Haskell写如下。
do param1- param1lazy param2- param2lazy param3- param3lazy–注: do表示法中return的含义与Java完全不同。 这表示把价格打包到箱子里。 -等价的Java表示法是Lazy.of ( )- param1 param2 param3) return param1 param2 param3Java没有语法糖,但上帝关门时会打开窗户。 通过Java可以清楚地看到每个步骤都在做什么,并理解其原理。 如果你读过这篇文章前面的内容,你就会知道这个do符号一直在做flatmap。
六Lazy的最终代码到目前为止,我们写的Lazy代码如下。
publicclasslazytimplementssuppliert { privatefinalsupplierextendstsupplier; 私有t value; private lazy ( supplierextendstsupplier ) { this.supplier=supplier; } publicstatictlazytof ( supplierextendstsupplier ) ) returnnewlazy ) supplier; } public T get ( ) if ) value==null ) { T newValue=supplier.get ); new value==null ) thrownewillegalstateexception ( ‘ lazyvaluecannotbenull!’ ); } value=newValue; } return value; } publicslazysmap ( function super t,extends S function ) { return lazy.of (-function.apply ) get ) ); } publicslazysflatmap ( function super t,Lazy extends S function ) { return Lazy.of ) ( )-function.apply ( get ) ) )
@ componentpublicclassuserfactory//部门服务,rpc界面@ resourceprivatedepartmentservicedepartmentservice; //主管服务,rpc接口@ resourceprivatesupervisorservicesupervisorservice; //权限服务,rpc接口@ resourceprivatepermissionservicepermissionservice; publicuserbuilduser(longuid ) lazystringdepartmentlazy=lazy.of (-departmentservice.get department ) uid; //通过部门主管//department-supervisorlazylongsupervisorlazy=department lazy.map ( department-supervisorservice.getsupet supervisor-permissionlazysetstringpermissionslazy=department lazy.flat map ( department-supervisor lazy.map ) supervison user.setuid(uid; user.set department ( department lazy; user.set supervisor ( supervisor lazy; user.set permissions ( permissions lazy; }工厂类正在构建评估树。 工厂类清楚地显示了用户的每个属性之间的评估依赖关系,并且用户对象可以在运行时自动优化性能。 对节点进行评估后,将缓存路径上所有属性的值。
八异常处理我们惯性地使user.getDepartment ( )看起来像纯内存操作,但由于他实际上还是远程调用,因此可能会出现各种意想不到的异常,如超时。 异常处理不能交给业务逻辑。 影响商业逻辑的纯洁性,让他们在我们面前放弃工作。 理想的方法是交给惰性值的负载逻辑Supplier。 Supllier的计算逻辑会在重试或抛出异常时充分考虑各种异常。 抛出异常可能不那么“函数式”,但与Java的编程习惯很接近。 此外,如果无法获取重要值,则必须通过异常阻止业务逻辑的执行。
九将采用本文方法构建的实体进行总结,可以将业务建模所需的所有属性都放入其中,业务建模只需考虑业务匹配,无需考虑基础性能问题,真正实现业务层与物理层的解耦。 另外,UserFactory本质上是外部界面的适应层,当外部界面发生变更时,只需要修改适应层,就可以保护核心业务代码的稳定性。 业务核心代码由于外部调用大幅减少,代码接近纯粹的运算,便于编写单元测试,单元测试可以保证核心代码的稳定性和无错误。
题外话10 ) Java缺乏的表象化和应用函数库( Applicative )仔细想想,做了这么多,目的只有一个,就是不修改签名为cf(a,b )的函数就可以适用于箱式Box A和Box B 在函数型语言中有更方便的方法。 那就是应用函子。如果将函数应用于框的值,就会得到框的值。 在Lazy中,如果将函数应用于框中的值,则函数将变为框中的值,如下所示:
//此处的function是lazy中的publicslazysapply ( lazyfunctionsupert,extendsfunction({returnlazy.of ) )-function.gen ccen }但是,用Java实现这个也无济于事。 因为Java不支持卡里化。 通过表象化,可以将函数的一些参数固定为新函数。 如果函数被签名为f(a,b ),则在支持表象化的语言中可以直接调用f ) a )。 此时返回值是只接收b的函数。 支持表象化时,只需连续几次应用函数,即可将普通函数应用于盒型。 举Haskell的例子如下。 *是在Haskell中应用函数的语法糖,f是签名为cf(a,b )的函数,语法不完全正确,但表示意思)。
-注释:结果是box cbox f * box a * box b参考资料
Java函数类型类库VAVR提供了类似的Lazy实现。 但是,如果只是为了使用这一个类,要导入整个库还有点沉重,所以可以利用本文的思路直接自己实现函数型编程的升级。 应用函数式编程的文章,本文在一定程度上借鉴了中箱的类比方法: https://juejin.cn/post/6891820537736069134 SPM=ATA.21736010.0.595242 a 7a 98 F3 u 070