最近在做代码Review的时候,发现了一个非常普遍的问题
前言最近在做代码Review的时候发现了一个非常普遍的问题——一个业务方法里if...else叠了七八层代码行数直接飙到300多行。业务规则的复杂度天然就是不断增长的。今天加一个规则明天改一个规则后天删一个规则——用if...else硬编码就是在给自己挖坑。那有没有什么办法能把业务规则从代码里抽离出来让代码变得干净、可维护、可动态调整有。今天我要介绍的这个工具就是专门干这个事的——Easy Rules。希望对你会有所帮助。更多项目实战在我的技术网站susan.net.cn/project一、先看看if...else的“七宗罪”在正式介绍Easy Rules之前我们先看一个典型的“if...else地狱”案例java代码解读复制代码public class DiscountService { public double calculateDiscount(User user, Order order) { double discount 0.0; // 规则1VIP用户打9折 if (user.isVip()) { discount 0.9; } // 规则2订单金额超过1000再打95折 if (order.getAmount() 1000) { discount discount * 0.95; } // 规则3新用户首单打8折 if (user.isNewUser() user.getOrderCount() 0) { discount 0.8; } // 规则4双11期间全场9折 if (isDoubleEleven()) { discount discount * 0.9; } // 规则5老用户且订单金额超过5000打85折 if (!user.isNewUser() order.getAmount() 5000) { discount 0.85; } // 规则6规则7规则8... // 越加越多代码越来越长 return discount; } }这段代码有什么问题第一可读性差。规则越多方法越长谁接手谁想骂人。第二可维护性差。改一个规则要找到对应的if分支小心翼翼地改生怕影响到别的逻辑。第三可测试性差。要覆盖所有规则组合的测试用例数量呈指数级增长。第四扩展性差。每加一个新规则就要修改源代码、重新编译、重新部署。第五业务和代码耦合。业务人员想调整规则得求着开发改代码。第六重复逻辑遍地都是。同样的条件判断可能在好几个地方出现。第七修改后必须重启服务。线上规则调整零活度几乎为零。有些小伙伴可能会说“这些规则我写个配置表从数据库里查不就行了”没错配置化确实能解决一部分问题。但真正的规则引擎不只是“把规则存到数据库里”——它还要解决规则的组合、优先级、条件评估、自动执行等一系列问题。把if...else搬到数据库里if...else还是if...else只是换了个地方而已。那怎么办规则引擎就是干这个的。二、Easy Rules是什么Easy Rules是一个简单而强大的Java规则引擎由j-easy团队开源维护。它的设计灵感来源于Martin Fowler提出的“规则引擎”概念。Martin Fowler在一篇非常经典的文章里说过“你可以自己构建一个简单的规则引擎。你只需要创建一组具有条件和操作的对象将它们存储在一个集合中并运行它们来评估条件并执行操作。”Easy Rules做的就是这件事——它提供了Rule抽象来创建带有条件和操作的规则以及RulesEngineAPI来运行一系列规则。2.1 一句话说清Easy RulesEasy Rules就是一个“把if...else从代码里搬出来、用规则对象来管理”的工具。它让你可以这样定义规则java代码解读复制代码Rule(name VIP折扣规则, priority 1) public class VipDiscountRule { Condition public boolean isVip(Fact(user) User user) { return user.isVip(); } Action public void applyDiscount(Fact(order) Order order) { order.setDiscount(0.9); } }然后这样执行java代码解读复制代码RulesEngine engine new DefaultRulesEngine(); engine.fire(rules, facts);是不是清爽多了2.2 核心特点Easy Rules有以下几个核心特点轻量级没有复杂的依赖和配置JAR包仅100KB左右POJO开发用普通的Java类加注解就能定义规则多种定义方式支持注解、流式API、表达式语言MVEL/SpEL/JEXL、YAML/JSON配置复合规则支持将多个简单规则组合成复杂规则易于集成可以轻松集成到Spring Boot等框架中三、核心概念Easy Rules围绕四个核心抽象构建3.1 Rule规则Rule接口是Easy Rules最核心的抽象。一个规则包含name规则的唯一名称description规则的简要说明priority规则的优先级数字越小优先级越高condition条件——返回true时触发规则action动作——条件满足时执行的操作java代码解读复制代码public interface Rule { boolean evaluate(Facts facts); // 评估条件 void execute(Facts facts) throws Exception; // 执行动作 // getters for name, description, priority... }3.2 Facts事实Facts是规则执行时的数据上下文本质是一个键值对容器。你把数据放进去规则从中取数据java代码解读复制代码Facts facts new Facts(); facts.put(user, user); facts.put(order, order); facts.put(rain, true);3.3 Rules规则集合Rules是规则的有序容器负责管理一组规则。规则按照priority自动排序java代码解读复制代码Rules rules new Rules(); rules.register(new VipDiscountRule()); rules.register(new NewUserDiscountRule()); rules.register(new DoubleElevenRule());3.4 RulesEngine规则引擎RulesEngine是执行规则的核心引擎。Easy Rules提供了两种实现引擎类型执行策略适用场景DefaultRulesEngine按优先级顺序执行条件满足就执行大多数常规场景InferenceRulesEngine前向链推理反复执行直到没有规则可触发规则之间存在依赖和连锁反应java代码解读复制代码// 默认引擎 RulesEngine engine new DefaultRulesEngine(); engine.fire(rules, facts);四、四种规则定义方式Easy Rules提供了四种定义规则的方式你可以根据实际场景灵活选择。4.1 方式一注解方式最常用适用场景规则固定、逻辑清晰、大部分常规业务场景。用Rule、Condition、Action、Fact注解来定义规则java代码解读复制代码Rule(name 天气规则, description 如果下雨就带伞, priority 1) public class WeatherRule { Condition public boolean isRaining(Fact(rain) boolean rain) { return rain; } Action public void takeUmbrella() { System.out.println(下雨了记得带伞); } }注解说明Rule标记类为规则可指定name、description和priorityCondition标记条件方法必须返回boolean只能有一个Action标记动作方法可以有多个通过order指定执行顺序Fact标记参数从Facts容器中按名称取值执行代码java代码解读复制代码public class Application { public static void main(String[] args) { // 1. 准备事实 Facts facts new Facts(); facts.put(rain, true); // 2. 注册规则 Rules rules new Rules(); rules.register(new WeatherRule()); // 3. 执行引擎 RulesEngine engine new DefaultRulesEngine(); engine.fire(rules, facts); // 输出下雨了记得带伞 } }4.2 方式二流式API动态规则适用场景规则需要动态生成、规则条件在运行时才能确定。使用RuleBuilder流式API构建规则java代码解读复制代码Rule dynamicRule new RuleBuilder() .name(高温预警规则) .description(温度超过30度时提醒防暑) .priority(2) .when(facts - facts.get(temperature) 30) .then(facts - System.out.println(天气炎热注意防暑)) .build();when()方法接收一个PredicateFactsthen()方法接收一个ConsumerFacts。这种方式非常适合运行时动态生成规则的场景。4.3 方式三表达式语言最灵活适用场景规则频繁变更、希望非开发人员也能修改规则。Easy Rules支持MVEL、SpEL、JEXL三种表达式语言java代码解读复制代码// 使用MVEL表达式 Rule mvelRule new MVELRule() .name(会员折扣规则) .description(会员且订单金额大于100享受9折) .when(user.vip true order.amount 100) .then(order.discount 0.9);表达式语言最大的优势是规则可以以字符串形式存储数据库、配置文件、管理后台修改规则不需要重新编译和部署。4.4 方式四YAML/JSON配置配置化适用场景希望把规则完全抽离到配置文件、由业务人员维护。通过YAML或JSON文件定义规则yaml代码解读复制代码# rules.yml - name: 新用户首单折扣 description: 新用户第一笔订单打8折 priority: 3 condition: user.newUser true user.orderCount 0 actions: - order.discount 0.8通过MVELRuleFactory或SpELRuleFactory加载配置文件java代码解读复制代码MVELRuleFactory ruleFactory new MVELRuleFactory(new YamlRuleDefinitionReader()); Rule rule ruleFactory.createRule(new FileReader(rules.yml));五、底层原理5.1 整体架构Easy Rules的架构可以分为四个层次5.2 DefaultRulesEngine执行流程DefaultRulesEngine是最常用的引擎实现它的执行流程如下5.3 InferenceRulesEngine前向链推理InferenceRulesEngine实现了前向链推理算法。它的核心逻辑是进入推理循环在当前Facts下找出所有条件为true的规则候选规则如果没有候选规则退出循环否则执行所有候选规则可能修改Facts回到步骤2继续循环这种模式特别适合规则之间存在依赖关系的场景——一个规则的执行结果可能成为另一个规则的触发条件。5.4 引擎参数配置可以通过RulesEngineParameters精细控制引擎行为java代码解读复制代码RulesEngineParameters parameters new RulesEngineParameters() .skipOnFirstAppliedRule(true) // 第一个规则执行后跳过后续 .skipOnFirstFailedRule(true) // 第一个规则失败后跳过后续 .skipOnFirstNonTriggeredRule(false) // 第一个规则未触发时是否跳过 .priorityThreshold(10); // 只执行优先级10的规则 RulesEngine engine new DefaultRulesEngine(parameters);5.5 监听器机制Easy Rules提供了监听器机制可以在规则执行的各个阶段插入自定义逻辑java代码解读复制代码public class LoggingRuleListener implements RuleListener { Override public boolean beforeEvaluate(Rule rule, Facts facts) { System.out.println(开始评估规则 rule.getName()); return true; // 返回false可跳过该规则 } Override public void afterEvaluate(Rule rule, Facts facts, boolean evaluationResult) { System.out.println(规则 rule.getName() 评估结果 evaluationResult); } Override public void onSuccess(Rule rule, Facts facts) { System.out.println(规则 rule.getName() 执行成功); } Override public void onFailure(Rule rule, Facts facts, Exception exception) { System.err.println(规则 rule.getName() 执行失败 exception.getMessage()); } }监听器可以用于日志记录、性能监控、统计规则命中率、调试等多种场景。六、复合规则有些小伙伴可能会问单个规则只能处理一个条件那复杂的业务逻辑怎么办Easy Rules提供了复合规则机制可以把多个简单规则组合成复杂的规则。它提供了三种复合规则类型6.1 UnitRuleGroup与逻辑“所有规则都满足才执行”使用场景需要多个前置条件全部满足才能执行某个操作。比如“用户是VIP且订单金额超过1000且商品有库存”三个条件缺一不可。java代码解读复制代码UnitRuleGroup unitGroup new UnitRuleGroup(全部满足规则组); unitGroup.addRule(new VipRule()); unitGroup.addRule(new AmountRule()); unitGroup.addRule(new StockRule()); // 只有三个规则全部满足group才会执行6.2 ActivationRuleGroup“只要有一个满足就执行第一个”使用场景多个互斥的折扣规则只能选一个生效。比如“新用户首单8折”和“VIP用户9折”同时满足时只执行优先级高的那个。java代码解读复制代码ActivationRuleGroup activationGroup new ActivationRuleGroup(折扣规则组); activationGroup.addRule(new NewUserDiscountRule()); // priority3 activationGroup.addRule(new VipDiscountRule()); // priority1 // 只会执行优先级最高的那个VIP折扣6.3 ConditionalRuleGroup条件逻辑“第一个满足的规则决定是否执行后续规则”使用场景根据第一个规则的结果决定是否执行后续规则链。java代码解读复制代码ConditionalRuleGroup conditionalGroup new ConditionalRuleGroup(条件规则组); conditionalGroup.addRule(new CheckUserTypeRule()); // 第一个规则判断用户类型 conditionalGroup.addRule(new VipDiscountRule()); // 后续规则根据类型执行 conditionalGroup.addRule(new NormalDiscountRule()); // 只有第一个规则满足才会继续执行后续规则七、Maven依赖在项目中引入Easy Rules非常简单xml代码解读复制代码!-- Easy Rules 核心库 -- dependency groupIdorg.jeasy/groupId artifactIdeasy-rules-core/artifactId version4.1.0/version /dependency !-- 支持YAML/JSON规则定义 -- dependency groupIdorg.jeasy/groupId artifactIdeasy-rules-support/artifactId version4.1.0/version /dependency !-- 支持MVEL表达式语言 -- dependency groupIdorg.jeasy/groupId artifactIdeasy-rules-mvel/artifactId version4.1.0/version /dependency注意Easy Rules项目自2020年12月起进入维护模式最新稳定版本为4.1.x建议所有用户升级到此版本。八、优缺点优点1. 极轻量级JAR包仅100KB左右没有任何复杂的依赖和配置。启动速度从秒级降至毫秒级非常适合Kubernetes环境中的Pod快速启动。2. 学习成本极低基于POJO和注解的编程模型Java开发者几乎零门槛上手。不需要学习复杂的DSL领域特定语言。3. 多种规则定义方式支持注解、流式API、表达式语言MVEL/SpEL/JEXL、YAML/JSON四种方式覆盖从“代码硬编码”到“完全配置化”的全谱系需求。4. 复合规则支持支持与逻辑、排他或逻辑、条件逻辑三种复合规则可以从简单规则组合出复杂业务逻辑。5. 监听器机制提供了完整的规则执行生命周期监听方便日志记录、性能监控和调试。6. 零配置开箱即用不需要任何外部配置文件引入依赖即可开始使用。7. 与Spring Boot无缝集成可以轻松集成到Spring Boot项目中通过Bean注册规则和引擎。缺点1. 规则修改仍需重启业务规则变更后仍然需要修改Java代码并重启服务除非结合动态类加载或表达式语言方案。2. 不支持复杂规则链和决策表相比Drools等重量级规则引擎Easy Rules在复杂规则链、决策表等高级场景上能力有限。3. 项目进入维护模式Easy Rules自2020年12月起进入维护模式目前仅支持版本4.1.x。新功能开发基本停止但现有功能足够稳定。4. 不适用于超大规模规则集当规则数量达到数千条时性能可能不如采用Rete算法的Drools。5. 无可视化管理界面Easy Rules本身不提供可视化规则管理界面需要自行开发。九、适用场景强烈推荐使用的场景场景典型应用电商促销规则满减、折扣、优惠券、会员特权风控系统用户评分、异常检测、反欺诈数据验证表单校验、数据完整性检查条件判断用户注册、登录等场景的条件分支动态决策智能推荐、个性化服务游戏AINPC行为规则、智能决策配置化管理通过YAML/JSON文件管理业务规则不太适合的场景场景原因超大规模规则集数千条性能不如Drools等专业引擎需要可视化规则管理Easy Rules不提供可视化界面需要实时热更新规则需要结合动态类加载或表达式语言自行实现非常复杂的决策表Easy Rules能力有限一个实用的选型建议如果你不确定未来规则的复杂度会发展到什么程度可以从Easy Rules开始待规则变复杂后再平滑迁移到Drools。更多项目实战在我的技术网站susan.net.cn/project十、写在最后回到最初的问题为什么要干掉if...else不是if...else本身有问题——它是编程语言最基本的控制结构没有任何问题。问题在于当业务规则不断增长时if...else会变成一座“代码垃圾山”——越堆越高、越堆越乱、越堆越没人敢动。Easy Rules做的事情就是把if...else从代码里“搬”出来变成一个个独立的、可组合的、可管理的规则对象。它不是一个“万能银弹”——它解决不了所有问题它只是把“规则管理”这件事从“写死在代码里”变成了“用对象来管理”。但就这一步已经能让你的代码从“if...else地狱”变成“规则清晰、可维护、可扩展”的优雅架构。如果你正在维护一个被if...else撑爆的老系统或者正在设计一个规则频繁变更的新系统不妨花10分钟把Easy Rules的官方示例跑一遍。