当前位置: 首页 > 科技观察

函数式编程的Java编码实践:利用惰性写出高性能且抽象的代码

时间:2023-03-13 05:37:18 科技观察

Java函数式编程编码实践:用Laziness编写高性能抽象代码只需要一点Java8知识。1抽象一定会导致代码性能下降?程序员的梦想是写出“高内聚、低耦合”的代码,但从经验来看,代码越抽象,性能越低。可由机器直接执行的汇编性能最强,其次是C语言,Java由于抽象层次较高,性能较低。业务系统也受到相同规则的约束。底层的数据增删改查接口性能最高,上层的业务接口由于增加了各种业务校验和消息发送,性能较低。对性能的担忧也限制了程序员更合理地抽象模块。让我们看一下常见的系统抽象。“用户”是系统中的一个普通实体。为了统一系统中“用户”的抽象,我们定义了一个通用的领域模型User,除了用户的id之外,它还包含了部门信息。用户的主管等,这些是系统中经常聚合在一起使用的属性:publicclassUser{//useridprivateLonguid;//用户的部门,为了示例简单,这里使用普通字符串//需要远程调用通讯录系统获取privateStringdepartment;//用户的supervisor,为了示例简单,这里用一个id来表示//需要远程调用通讯录系统获取privateLongsupervisor;//所持有的权限user//RequiresremotecallpermissionsystemtoobtainprivateSetpermission;}这看起来很不错。“用户”的所有共同属性都集中在一个实体中。只要将这个User作为方法的参数,这个方法基本不需要查询其他用户信息。但是一旦实施,就会发现问题。部门和主管信息需要通过远程调用通讯录系统获取,权限需要通过远程调用权限系统获取。每次构造一个User,都要付出这两次远程调用的代价,即使有些信息是无用的。到达。例如下面的方法就展示了这种情况(判断一个用户是否是另一个用户的主管):publicbooleanisSupervisor(Useru1,Useru2){returnObjects.equals(u1.getSupervisor(),u2.getUid());}为了在上述方法参数中使用通用的User实体必须付出额外的代价:远程调用获取的是完全未使用的权限信息,如果权限系统出现问题,也会影响无关接口的稳定性。想到这里,我们可能要放弃普通的实体方案,让裸uid渗透到系统中,将用户信息查询代码散布在系统各处。事实上,你可以继续使用上面的抽象,稍微改进一下。只需要将department、supervisor、permission变成lazy-loaded字段,需要的时候才通过外部调用获取。这样做有很多好处:业务建模只需要考虑适配业务,不需要考虑底层的性能问题,真正实现了业务层和物理层的解耦。业务逻辑与外部调用分离。无论外部接口如何变化,我们始终有一个适配层来保证核心逻辑。稳定的业务逻辑看似纯粹的实体操作,易于编写单元测试,保证核心逻辑的正确性。但是,在实践过程中经常会遇到一些问题。本文结合Java的一些技巧和函数式编程,实现了一个懒加载工具类。Strictness和Laziness之二:Java8中Supplier的本质Java8引入了一个新的功能接口Supplier。在老Java程序员看来,无非就是一个可以获取任意值的接口。Lambda无非就是实现了这个interface的类的语法糖。这是从语言角度而不是计算角度的理解。当你理解了严格和惰性的区别时,你可能会对计算的本质有一个更接近的看法。因为Java和C是严格的编程语言,所以我们习惯于在定义变量的地方计算变量。事实上,还有一门编程语言是在使用变量时进行计算的,比如函数式编程语言Haskell。所以Supplier的本质就是在Java语言中引入了惰性计算机制。为了在Java中实现等效的惰性计算,可以这样写:Suppliera=()->10+1;intb=a.get()+1;三、Supplier的进一步优化:LazySupplier还有一个问题,就是每次通过get获取到值都会重新计算。真正的惰性计算应该缓存第一次get之后的值。把Supplier稍微包装一下:/***为了方便和标准的Java函数式接口交互,Lazy还实现了Supplier*/publicclassLazyimplementsSupplier{privatefinalSuppliersupplier;//使用value属性缓存供应商privateTvalue的计算值;privateLazy(Suppliersupplier){this.supplier=supplier;}publicstaticLazyof(Suppliersupplier){returnnewLazy<>(supplier);}publicTget(){if(value)==null){TnewValue=supplier.get();if(newValue==null){thrownewIllegalStateException("Lazyvaluecannotbenull!");}value=newValue;}returnvalue;}}通过Lazy写前面的惰性计算code:Lazya=Lazy.of(()->10+1);intb=a.get()+1;//get不会重新计算,直接使用缓存中的值intc=a.get();使用这个懒加载工具类来优化我们之前的通用用户实体:publicclassUser{//useridprivateLonguid;//用户的部门,为了示例简单,这里使用普通字符串//需要远程调用通讯录系统来obtainprivateLazydepartment;//用户的主管,为了示例简单,这里用一个id来表示//需要远程调用通讯录系统获取privateLazysupervisor;//用户包含的权限//需要远程调用权限系统获取privateLazy>权限;publicLonggetUid(){returnuid;}publicvoidsetUid(Longuid){this.uid=uid;}publicStringgetDepartment(){returndepartment.get();}/***因为department是懒加载属性,必须传入set方法计算函数,不是具体值*/publicvoidsetDepartment(Lazydepartment){this.department=department;}//...类似省略后...}一个简单的User实体构造的例子如下如下:Longuid=1L;Useruser=newUser();user.setUid(uid);//departmentService是rpc调用user.setDepartment(Lazy.of(()->departmentService.getDepartment(uid)));//。...这样看起来还可以,但是当你继续深入使用的时候,你会发现一些问题:用户部门和主管这两个属性是相关的,需要通过rpc接口获取用户部门,然后使用另一个rpc接口根据部门获取主管。代码如下:Stringdepartment=departmentService.getDepartment(uid);Longsupervisor=SupervisorService.getSupervisor(department);但是现在department已经不是一个计算值了,而是一个惰性计算的Lazy对象,上面的代码应该怎么写呢?“Functor”就是用来解决这个问题的四个Lazy实现仿函数(Functor),快速理解:类似于Java中的streamapi或者Optional中的map方法。functor可以理解为接口,map可以理解为接口中的方法。1仿函数Collection、Java中的Optional以及我们刚刚实现的Lazy的计算对象都有一个共同的特点,那就是它们都有一个且只有一个泛型参数。我们在本文中将其称为盒子,记住MakeBox,因为它们就像一个万能容器,可以用任何类型包装。2函子的定义Functor操作可以应用一个将T到S映射到Box的函数,使它成为一个Box。将Box中的数字转换为字符串的例子如下:box中包含一个type,而不是1和“1”的原因是box中不一定只有一个值,比如a集合,甚至更复杂的多值映射关系。需要注意的是,并不是仅仅定义一个满足Box映射(Function函数)的签名就可以让Box成为一个函子。下面是一个反例://反例不能成为函子,因为这个方法没有体现在box函数publicBoxmap(Functionfunction){returnnewBox<>(null)的映射关系;}所以functor是比map方法更严格的定义,它也要求map满足下面的法则,称为Functor法则(法则的本质是保证map方法能够忠实反映由定义的映射关系参数function):元元法则:应用恒等函数后,Box的值不会改变,即box.equals(box.map(Function.identity()))永远为真(这里的equals只是想表达一个数学上相等的意义)复合定律:假设有两个函数f1和f2,map(x->f2(f1(x)))和map(f1).map(f2)总是等价的。很明显,Lazy满足以上两条定律。3Lazyfunctor介绍了那么多理论,其实实现起来很简单:publicLazymap(Functionfunction){returnLazy.of(()->function.apply(get()));}很容易证明满足函子定律。通过地图,我们可以轻松解决之前遇到的问题。map中传入的函数可以在获取部门信息的前提下进行操作:LazydepartmentLazy=Lazy.of(()->departmentService.getDepartment(uid));LazysupervisorLazy=departmentLazy。map(department->SupervisorService.getSupervisor(department));4遇到了比较难的情况,我们现在不仅可以构造惰性值,还可以用一个惰性值计算另一个惰性值,看起来很完美。但是当你进一步使用它时,你会发现更棘手的问题。现在我需要department和supervisor两个参数调用权限系统获取权限,department和supervisor这两个值是惰性值。首先尝试使用嵌套映射:Lazy>>permissions=departmentLazy.map(department->supervisorLazy.map(supervisor->getPermissions(department,supervisor)));返回值的类型似乎有点奇怪,我们期待的是Lazy>,但是我们这里得到的是Lazy>>。并且随着嵌套地图层数的增加,Lazy的通用级别也会增加。三个参数的例子如下:Lazyparam1Lazy=Lazy.of(()->2L);Lazyparam2Lazy=Lazy.of(()->2L);Lazyparam3Lazy=Lazy.of(()->2L);Lazy>>result=param1Lazy.map(param1->param2Lazy.map(param2->param3Lazy.map(param3->param1+param2+param3)));这就需要下面的monadic操作来解决。5.单子(Monad)的惰性实现快速理解:类似于Java流api中flatmap的功能和Optional1单子的定义单子和仿函数的主要区别在于接收函数。functor的函数一般返回原始值,而monad函数则返回一个装箱后的值。如果下图中的函数使用map而不是flatmap,结果就会变成俄罗斯套娃——两层盒子。当然monads也有monads的法则,只是比functor的法则复杂,这里就不多解释了。其作用类似于函子定律,确保flatmap能够忠实反映函数的映射关系。2Lazymonad的实现也很简单:publicLazyflatMap(Function>function){returnLazy.of(()->function.apply(get())。get());}使用flatmap解决之前遇到的问题:Lazy>permissions=departmentLazy.flatMap(department->supervisorLazy.map(supervisor->getPermissions(department,supervisor)));三个参数的情况:Lazyparam1Lazy=Lazy.of(()->2L);Lazyparam2Lazy=Lazy.of(()->2L);Lazyparam3Lazy=Lazy.of(()->2L);Lazyresult=param1Lazy.flatMap(param1->param2Lazy.flatMap(param2->param3Lazy.map(param3->param1+param2+param3)));规则是最后一个值使用map获取,其他使用flatmap。3题外话:函数式语言中的monad语法糖看完上面的例子,你一定会觉得惰性计算很麻烦。每次要获取里面的lazy值,都要经过很多次flatmap和map。这实际上是一种妥协,因为Java本身并不支持函数式编程。Haskell支持使用do符号来简化Monad的操作。上面的三参数例子如果在Haskell中使用,可以写成:doparam1<-param1Lazyparam2<-param2Lazyparam3<-param3Lazy--注意:do表示法中return的含义与Java完全不同--表示打包valueintoabox,--相当于Java的写法是Lazy.of(()->param1+param2+param3)returnparam1+param2+param3一个窗口。在Java中,你可以清楚地看到每一步是做什么的,明白其中的原理。如果你看过本文前面的内容,你肯定能明白这个donotation就是不断的做flatmap。6Lazy的最终代码至此,我们写的懒人代码如下:}publicstaticLazyof(Suppliersupplier){returnnewLazy<>(supplier);}publicTget(){if(value==null){TnewValue=supplier.get();if(newValue==null){thrownewIllegalStateException("Lazyvaluecannotbenull!");}value=newValue;}returnvalue;}publicLazymap(Functionfunction){returnLazy.of(()->function.apply(get()));}publicLazyflatMap(Function>function){returnLazy.of(()->function.apply(get())).get());}}7构造一个可以自动优化性能的实体使用Lazy,我们写一个工厂,构造一个普通的User实体:@ComponentpublicclassUserFactory{//部门服务,rpc接口@ResourceprivateDepartmentServicedepartmentService;//Supervisor服务,rpc接口@ResourceprivateSupervisorServicesupervisorService;//对受限服务,rpc接口@ResourceprivatePermissionServicepermissionService;publicUserbuildUser(longuid){LazydepartmentLazy=Lazy.of(()->departmentService.getDepartment(uid));//通过department获取supervisor//department->supervisorLazysupervisorLazy=departmentLazy.map(department->SupervisorService.getSupervisor(department));//通过部门和主管获取权限//department,supervisor->permissionLazy>permissionsLazy=departmentLazy.flatMap(department->supervisorLazy.map(supervisor->permissionService.getPermissions(department,supervisor)));Useruser=newUser();user.setUid(uid);user.setDepartment(departmentLazy);user.setSupervisor(supervisorLazy);user.setPermissions(permissionsLazy);}}工厂类就是构造一个求值树。通过工厂类,可以清楚的看到User的各个属性之间的求值依赖关系。同时,User对象可以在运行时自动优化性能。一旦一个节点被求值,该路径上所有属性的值都会被缓存起来八个异常处理虽然我们通过偷懒的方式让user.getDepartment()看起来是纯内存操作,但实际上是远程调用,所以各种意想不到可能会出现异常,比如超时等。异常的处理一定不能交给业务逻辑,这样会影响业务逻辑的纯度,让我们前功尽弃。理想的方式是交给惰性值加载逻辑Supplier。在Supplier的计算逻辑中,充分考虑了各种异常情况,重试或者抛出异常。抛出异常虽然可能没有那么“函数式”,但更接近Java的编程习惯,当无法获取key值时,业务逻辑的运行应该会被异常阻塞。九、结论使用本文方法构建的实体可以将业务建模所需的所有属性放入其中。业务建模只需要考虑适配业务,无需考虑底层性能问题,真正实现业务层和物理层的解决。夫妻。同时,UserFactory本质上是一个对外接口的适配层。一旦对外接口发生变化,只需要修改适配层,可以保护核心业务代码的稳定性。因为核心业务代码的外部调用大大减少,代码更接近于纯计算,所以很容易编写单元测试。通过单元测试,可以保证核心代码稳定无错误。十个题外话:Java中缺失的currying和applicative仔细想想,我就做了这么多,目的一个,让签名为Cf(A,B)的函数可以应用到OnboxedtypesBox和Box,yieldaBox,函数式语言中有一种更方便的方式,即applicativefunctor。applyfunctor的概念很简单,就是把装箱的函数应用到装箱的值上,最后得到一个装箱的值,在Lazy中可以实现://注意这里的函数是装在lazypublicLazyapply(Lazy>function){returnLazy.of(()->function.get().apply(get()));}但是在Java中ImplementingthisinJava没有帮助,因为Java不支持柯里化。柯里化允许我们将一个函数的几个参数固定到一个新函数中。如果函数签名是f(a,b),支持柯里化的语言允许直接调用f(a),此时的返回值是一个只接受b的函数。当支持柯里化时,普通函数只能通过连续多次应用仿函数来应用于盒装类型。Haskell中的一个例子如下(<*>是Haskell语法糖中的applicationfunctor,f是一个函数,签名为cf(a,b),语法不完全正确,只是表达一个意思):--注:结果为boxcboxf<*>boxa<*>boxb,在JavaFunctionalClassLibrary中引用了VAVR中提供了类似的Lazy实现,但是如果只是为了使用这个类,引入整个还是有点重图书馆。可以利用本文的思路直接自己实现高级函数式编程:应用函子前端视角下的函数式编程,本文一定程度上参考了里面盒子的类比方法:https://juejin.cn/post/6891820537736069134?spm=ata.21736010.0.0.595242a7a98f3U《Haskell函数式编程基础》《Java函数式编程》