函数式编程:用BiFunction消除多类型分支的代码重复
下面拿一个我在生产环境用过的一个库存变更的场景作为例子演示一下如何使用JDK自带的BiFunction。也是函数编程的一种使用。问题场景一个库存日报表模块支持三种变更类型期初库存、在途库存、入库库存。每种类型对应DailyInventoryRecord上一个不同的更新方法updateOpeningInventory、updateInTransitInventory、updateReceivedInventory。业务逻辑分两条路径新增一条库存记录或者更新已有记录。两条路径里都要根据类型判断该调哪个方法。先看新增路径if (TYPE_OPENING.equals(type)) { record.updateOpeningInventory(param); } else if (TYPE_IN_TRANSIT.equals(type)) { record.updateInTransitInventory(param); } else if (TYPE_RECEIVED.equals(type)) { record.updateReceivedInventory(param); }更新路径里一模一样的if-else再来一遍只是record对象换了个变量名。问题出在哪两个路径里的if-else结构完全一样只是调用位置不同。如果后面要加一种「出库库存」类型新增路径要改更新路径也要改。改两处本身不费事但容易漏。能不能把「调哪个方法」这个决策提前做一次后面的两条路径直接拿着结果去调用解法switch表达式 BiFunction 方法引用BiFunction是JDK 8引入的函数式接口在java.util.function包下接收两个参数返回一个结果。用在这个场景刚好合适更新方法需要接收记录对象和变更参数两个入参返回更新后的记录对象。核心就这几行BiFunctionDailyInventoryRecord, InventoryChangeParam, DailyInventoryRecord fn switch (type) { case TYPE_OPENING - DailyInventoryRecord::updateOpeningInventory; case TYPE_IN_TRANSIT - DailyInventoryRecord::updateInTransitInventory; case TYPE_RECEIVED - DailyInventoryRecord::updateReceivedInventory; };这里用了Java 14引入的switch表达式配合方法引用。DailyInventoryRecord::updateOpeningInventory是一个未绑定的方法引用编译器会自动把调用对象本身当作第一个参数。所以它的实际签名是(DailyInventoryRecord, InventoryChangeParam) - DailyInventoryRecord和BiFunction的泛型完全匹配。有了这个fn新增和更新两条路径都不需要再写if-else了。新增路径里buildNewRecords方法接收fn作为参数内部统一调用// 构建新增记录 private ListDailyInventoryRecord buildNewRecords( BiFunctionDailyInventoryRecord, InventoryChangeParam, DailyInventoryRecord fn, SetString newMaterialCodes, MapString, InventoryChangeParam paramMap) { return newMaterialCodes.stream().map(code - { DailyInventoryRecord record new DailyInventoryRecord(code); return fn.apply(record, paramMap.get(code)); }).toList(); }更新路径更直接循环里调一下// 更新已有记录 for (DailyInventoryRecord record : existingRecords) { fn.apply(record, paramMap.get(record.getMaterialCode())); }两条路径的代码里没有任何类型判断都是直接fn.apply()。if-else被集中到了fn赋值的那一个地方。这种写法用起来很顺但它能成立有一个硬性前提。前提条件能被同一个BiFunction统一的方法入参个数、参数类型、返回值类型必须完全一致。拿这个例子来说三个更新方法都是接收一个InventoryChangeParam返回DailyInventoryRecord自身。签名一致才能用方法引用统一赋值。如果其中某个方法多了一个参数或者返回类型不同BiFunction的泛型就约束不住了编译直接报错。所以这不是一个万能的模式。方法签名不一致的时候别硬往上套老老实实用if-else反而更清晰。回到整体来看改前和改后的差异到底在哪改前改后对比维度改前if-else分散改后BiFunction统一类型判断代码两条路径各写一遍if-elseswitch只写一次新增类型时要改的地方两个路径各改一处共2处switch里加一行共1处漏改风险高改一处容易忘改另一处低只有一个入口使用路径的代码每个路径内部都有分支逻辑统一fn.apply()无分支判断要不要用这个模式的标准很具体看那几个分支里调用的方法签名是否一致。入参个数、参数类型、返回值都相同用函数式接口统一就很自然。签名不一致强统一反而要写额外的适配代码不如保持if-else。小结Java的函数式接口在实际项目里最有用的地方不是替代所有条件分支而是处理「结构相同、方法不同」这一类重复代码。BiFunction、Consumer、Function这些接口在JDK里已经存在了很多年问题不在于知不知道它们而在于遇到合适场景的时候能不能想起来用。判断标准只有一个分支里各方法的签名能不能统一。能统一用函数式接口收拢到一个变量里调用方只管apply()不用关心调的是哪个具体方法。不能统一就保持if-else别勉强。这种模式算不上什么高级技巧用多了就成了习惯。