再谈软件设计中的抽象思维(下),从FizzBuzz到规则引擎

news2024/11/24 1:51:39

作为《程序员的底层思维》出版两年之后的再回顾,在上一篇《再谈软件设计中的抽象思维(上),从封装变化开始》中,我介绍了抽象设计的本质是发现变化点,结合问题域,提炼共性,沉淀领域知识。今天这篇我们通过实现一个通用的规则引擎,进一步讲解抽象思维在软件设计中的运用。

1. FizzBuzz游戏

FizzBuzz是一个在欧美非常流行的小游戏,类似于我们中国人酒桌上玩的“敲7游戏”,游戏规则是这样的,假设体育老师带着100名学生做一个报数游戏,从1开始按顺序报数,要求:

  1. 如果学生的序号是3的倍数,要说“Fizz

  2. 如果学生的序号是5的倍数,就说“Buzz

  3. 如果学生的序号同时是3和5的倍数,就说“FizzBuzz

  4. 如果学生的序号是其他数字, 就说 原来数字

2. 战术性编程,快速实现

这个问题本身并不难,  战术龙卷风很快就能写出代码:

public class FizzBuzz {
    public static String count(int n){
        if (((n % 3) == 0) && ((n % 5) == 0))
            return "FizzBuzz";
        if ((n % 3) == 0)
            return "Fizz";
        if ((n % 5) == 0)
            return "Buzz";
        return String.valueOf(n);
    }
}

如果你是用TDD的话,也可能会先写下下面的测试代码。不管是哪种情况,对于原始需求,写完功能+测试就算是完成工作了。

public class FizzBuzzTest {

    @Test
    public void num_given_1() {
        //given
        int input = 1;
        //when
        String result = FizzBuzz.count(input);
        //then
        Assertions.assertEquals("1", result);
    }

    @Test
    public void fizz_given_3() {
        //given
        int input = 3;
        //when
        String result = FizzBuzz.count(input);
        //then
        Assertions.assertEquals("Fizz", result);

    }

    @Test
    public void buzz_given_5() {
        //given
        int input = 5;
        //when
        String result = FizzBuzz.count(input);
        //then
        Assertions.assertEquals("Buzz", result);
    }

    @Test
    public void fizz_buzz_given_15() {
        //given
        int input = 15;
        //when
        String result = FizzBuzz.count(input);
        //then
        Assertions.assertEquals("FizzBuzz", result);
    }
}

这和很多的业务代码类似,初始场景简单,代码不复杂也很clean。但随着应用场景的变化,各种逻辑分支开始冲击原来的代码结构,最初的clean code就会慢慢地变得dirty,最后变成shit。
还是以我们的FizzBuzz为例,后续的需求演进可能是:

  1. 增加更多的花样:比如,如果学生的序号是7的倍数,就说“Whizz”

  2. 增加更多的规则:比如,如果是3的倍数,那么忽略其它规则

3. 分析变化点,抽象概念

为了让我们的程序更加通用,我们需要首先分析变化点,对变化的地方进行抽象,封装变化,从而让程序OCP。针对该问题,如下图所示,我们不难发现,变化点一个是绿色虚框内的部分,另一个是蓝色虚框内的部分。

9473f79e3b67e1f412ccd093cb956156.jpeg

关于前半部分的判断条件我们可以抽象成Condition这个概念,后半部分的执行动作我们可以用抽象成Action这个概念。而一整条“如果…..就…..”我们称之为Rule。

实际上,这里我们已经能基本看出规则引擎的端倪了,因为根据Martin Fowler对规则引擎的定义,规则引擎的核心是Rule,而所谓的Rule 就是 if(Condition) then do(Action)。

e83ad27d174b5465391cd63eb4ead6aa.jpeg

至于上图右边那条“3和5的倍数”规则有些特殊,它既可以被看成是一条单独的Rule;也可以被看成是左边“3的倍数”和“5的倍数”两条Rule的组合(Composite);亦或是“3的倍数”和“5的倍数”两个condition谓词逻辑的and。对于我们FizzBuzz这个简单的问题而言,各种选项都可以。但是对于我们后续要实现的更加通用的规则引擎而言,我们会选择一种更加通用的方式去实现(具体,我们后文再说)。

根据上面的分析,我们不难写出一个相对通用简易的“规则引擎”来解决FizzBuzz问题,首先我们要对核心抽象概念进行接口定义。
Condition接口定义:

@FunctionalInterface
public interface Condition {

    boolean evaluate(int n);

    //谓词and逻辑,参考Predicate
    default Condition and(Condition other) {
        Objects.requireNonNull(other);
        return (n) -> {
            return this.evaluate(n) && other.evaluate(n);
        };
    }

    //谓词or逻辑,参考Predicate
    default Condition or(Condition other) {
        Objects.requireNonNull(other);
        return (n) -> {
            return this.evaluate(n) || other.evaluate(n);
        };
    }
}

Action接口定义:

@FunctionalInterface
public interface Action {
    String execute(int n);
}

Rule接口定义:

@FunctionalInterface
public interface Rule {
    String apply(int n);
}

接下来,我们用这个简易规则引擎,重构之前的FizzBuzz,这里我们把上面提到的Composite Rule和Composite Condition两种方式都实现了一遍:

/**
 * 计算倍数关系的谓词逻辑
 */
public class TimesCondition {
    public static Condition times(int i){
        return n -> n % i == 0;
    }
}

/**
 * 通过原子atom rule,以及atom rule之间的组合解决FizzBuzz问题
 * 这里为了简单使用Rule的组合模式代替了RuleEngine实体
 * 注意:这个SimpleRuleEngine只能解决输入为n,输出为String的FizzBuzz问题
 * 完全不具备通用性
 */
public class SimpleRuleEngine {
    public static Rule atom(Condition condition, Action action){
        return n -> condition.evaluate(n) ? action.execute(n) : "";
    }

    public static Rule anyOf(Rule... rules){
        return n -> stringStream(n, rules).filter(s -> !s.isEmpty()).findFirst().get();
    }

    public static Rule allOf(Rule... rules){
        return n -> stringStream(n, rules).collect(Collectors.joining());
    }

    public static Stream<String> stringStream(int n, Rule[] rules){
        return Arrays.stream(rules).map(r -> r.apply(n));
    }
}

/**
 * 用简易规则引擎重构后的FizzBuzz实现
 */
public class FizzBuzz {
    public static String count(int i){
        //Composite condition
        Rule fizzBuzzRule = atom(times(3).and(times(5)), n -> "FizzBuzz");
        Rule fizzRule = atom(times(3) , n -> "Fizz");
        Rule buzzRule = atom(times(5), n -> "Buzz");
        //Composite rule
        Rule compositeFizzBuzzRule = allOf(fizzRule, buzzRule);
        Rule defaultRule = atom(n -> true, n -> String.valueOf(n));
        Rule rule = anyOf(compositeFizzBuzzRule, fizzRule, buzzRule, defaultRule);
        return rule.apply(i);
    }
}

针对FizzBuzz问题,不管其未来的需求如何演变,我们都可以通过编排上面定义的Rule以及Rule之间的组合来实现。不过正如上面SimpleRuleEngine类注释所言, 我们虽然使用了规则引擎这个概念,但是我们的这个“规则引擎”完全是FizzBuzz specific的,完全不具备通用性。如果我们想打造一个通用的规则引擎,还缺失什么?要如何做呢?

4. 通用规则引擎框架

上面的规则引擎不具备通用性,主要是因为Rule.apply这个函数的入参只能是int,返回值只能是String。作为一个通用规则引擎框架,我们肯定不能限制用户只能对int类型进行条件判断。

在进一步设计之前,我先来介绍一个框架上下文模式,我们使用框架,主要是复用框架的能力(function)。而对于框架而言,他最主要的职责是帮用户处理“数据”,对于用户数据,我们在框架中通常叫它们Context(上下文)。

07a6189091311962c6ff95e7bb588154.jpeg

如上图所示,我们可以将框架中的Context进一步分为Procedure Context(过程上下文)和Global Context(全局上下文):
  1. 所谓Procedure Context,是指用户每次调用框架所需要携带的数据。这个Context一般被设计为函数参数,在框架内部传递,当调用链结束,即被销毁。简单理解就是Context per request。例如,web容器框架中的每一个http请求都会有一个HttpServletRequest,就属于Procedure Context。

  2. 所谓Global Context,一般存储的是用户对框架的配置信息,它是全局共享的,在框架的整个生命周期都有效。比如,web容器中的ServletContext,一个容器只有一个是Global的。

    这个框架上下文模式正是为了解决我们的int入参问题。即我们需要一个更通用的类型来表达Procedure Context,这里我们选择用Fact(事实)这个概念,来表示用户的输入数据。之所以叫Fact,是因为主流的流程引擎都是这么命名的,比如Drools。这就是我在上篇说的,抽象的难就在于有时候,我们要“创造”,“挖掘”合适的概念。像Fact这样的概念,其本身就是纯抽象的存在,不像苹果、香蕉,你还能看得见摸得着。实际上Context的概念也是一样,在软件领域,很多概念都是如此,你在这个“可见”的世界都找不到对应实体,它们只存在于我们的思维中,只能用抽象思维去处理。

按照我之前一直推荐的核心领域词汇表的做法,我们将通用规则引擎的核心概念整理如下:

英文名中文名含义
RuleEngine规则引擎规则引擎是有一组Rule组成,是框架的执行入口
fire触发触发RuleEngine执行的函数
Rule规则一条Rule是一个Condition和一组Action的组合
apply应用apple一个Rule,相当于 if(Condition) then do (Actions)
fire触发触发RuleEngine执行的函数
Condition条件规则的判断条件,核心扩展点
evaluate评估Condition对应的函数
Action动作当判断条件为true时,执行的动作
execute执行Action对应的函数
Fact事实数据用户的输入数据,Procdure Context的承载体
RuleEngineConfig规则引擎配置Global Conext,比如maxAllowedRules:允许的最大规则数

结合上一节我们对规则引擎的基础实现,再加上新加入的Fact,RuleEngine等新概念,我们不难得出通用规则引擎的领域模型如下图所示

9082f67d0e565b2894baa1704d5ee839.jpeg

基于新的模型,我们将核心接口调整如下:

//Condition接口
@FunctionalInterface
public interface Condition {
    boolean evaluate(Facts facts);
    ...
}

//Action接口
@FunctionalInterface
public interface Action {
    void execute(Facts facts);
}

//Rule接口
public interface Rule {
    boolean evaluate(Facts facts);
    void execute(Facts facts);
    boolean apply(Facts facts);
}

主要区别就在于将int类型,替换为更加通用的Facts,从而让我们的RuleEngine成为一个能支持任何场景的通用规则引擎。

另外,我们发现RuleEngine执行的是一组Rules,这些Rules之间的关系非常重要,因为RuleEngine如何执行这些Rules就取决于它们之间的关系。面对一组Rules,有两种类型的关系:逻辑关系和优先级关系,关于逻辑关系无外乎有以下三种:

  1. “或”关系(And):Rules之间是互斥的,只要有一个满足,就执行短路操作,其它的Rule就不执行了。

  2. “与”关系(Or):Rule之间存在逻辑与关系,即要么全部满足都执行,要么都不执行。

  3. 自然关系(Natural):Rule之间是平等的,RuleEngine会执行所有满足Condition的Rule。

    这些关系之间,可能会组合成比较复杂的树形关系,比如下图所给的示例表示,rule1和右节点之间是Natural关系,所以如果rule1满足条件就会被执行,然后继续检查右节点。rule2和rule3任何一个满足条件则执行之,然后退出。如果都不满足,会继续查看rule4和rule5,如果rule4和rule5同时满足条件都执行,否则都不执行。

    b89f9986fe4c7efa0cccc339bb012fe2.jpeg

像这样的树形结构,特别合适使用组合模式(Composite Pattern),因为组合模式可以抹平整体和个体的差异,让处理这种分级递归树形结构变得简单。尽管有这样的灵活性,在实际使用中,还是不建议Rule嵌套太深,会把自己绕晕。

使用了组合模式之后,我们会将RuleEngine和Rule之间的关系调整为:

0f5f0cf12ba8b839e397a9c8e94a1592.jpeg
在前面的树形结构例子中,不知道你有没有这样的疑问,rule2和rule3之间是OR的关系,如果rule2和rule3都满足条件,是选择执行rule2还是rule3呢?前面我们只定义了rules之间的逻辑关系,并没有定义优先级关系。为此,我们需要一个新概念priority,来指定rule之间执行的优先级,如下图所示:
ed791d046ac65d54fc233ea226baded7.jpeg
rule3的优先级更高,会优先评估(evaluate)和执行(execute)rule3。同样,右节点的优先级更高,会优先评估和执行右节点。这些新概念,同样需要被添加到领域词汇表:
英文名中文名含义
CompositRule组合规则组合模式,表示一组Rules的组合
NaturalRules自然关系组合顺序执行所有的Rules
AnyRules“或”关系组合执行第一个满足条件的Rule
AllRules“与”关系组合所有rules都满足条件,全部执行,否则都不执行
priority优先级当有多个rules需要执行,指定rule的优先级

至此,一个相对通用的规则引擎就算设计完成了。完整的代码实现可以在https://github.com/alibaba/COLA/tree/master/cola-components/cola-component-ruleengine 查看。可以看到,整个设计过程,就是不断地分析变化点,抽象概念,沉淀领域知识,让系统更加通用的过程。不同的问题域,解决的问题不同,但这一套从变性入手,分析综合,以领域为核心,以概念为核心,抽象建模的方法论绝对是相通的。也是我们软件设计里最重要的核心能力之一。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1827001.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Golang | Leetcode Golang题解之第151题反转字符串中的单词

题目&#xff1a; 题解&#xff1a; import ("fmt" )func reverseWords(s string) string {//1.使用双指针删除冗余的空格slowIndex, fastIndex : 0, 0b : []byte(s)//删除头部冗余空格for len(b) > 0 && fastIndex < len(b) && b[fastIndex]…

建造者模式(大话设计模式)C/C++版本

建造者模式 C 参考&#xff1a;https://www.cnblogs.com/Galesaur-wcy/p/15907863.html #include <iostream> #include <vector> #include <algorithm> #include <string> using namespace std;// Product Class&#xff0c;产品类&#xff0c;由多个…

【Java】Object类中的toString、equals方法

Object类 所有类都直接或间接的继承自Object类&#xff0c;Object类是所有Java类的根基类。 也就意味着所有的Java对象都拥有Object类的属性和方法。 如果在类的声明中未使用extends关键字指明其父类&#xff0c;则默认继承Object类。 toString()方法 【1】Object类的toStr…

植物大战僵尸杂交版 v2.0.88 mac版 Plants vs. Zombies 杂交版下载

特别注意&#xff1a;该游戏最低系统要求为macOS Sonoma 14.X&#xff0c;低于此系统版本的请勿下载&#xff01; 游戏介绍 植物大战僵尸杂交版是由B站UP主“潜艇伟伟迷”制作的一款结合了《植物大战僵尸》原有元素与创新玩法的游戏。这款游戏以其独特的“杂交”植物概念在B站…

【TypeScript】泛型工具

跟着 小满zs 学 ts&#xff1a;学习TypeScript24&#xff08;TS进阶用法-泛型工具&#xff09;_ts泛型工具-CSDN博客 Partial 所有属性可选的意思Required 所有属性必选的意思Pick 提取部分属性Exclude 排除部分属性emit 排除部分属性并且返回新的类型 Partial 属性变为可选。…

Python基础教程(二十):SMTP发送邮件

&#x1f49d;&#x1f49d;&#x1f49d;首先&#xff0c;欢迎各位来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里不仅可以有所收获&#xff0c;同时也能感受到一份轻松欢乐的氛围&#xff0c;祝你生活愉快&#xff01; &#x1f49d;&#x1f49…

Python 小市值股票模型代码及回测分析

目录 一、模型介绍 二、代码详解 2.1 初始化函数 2.2 股票筛选过滤函数 2.3 止损函数 2.4 开盘时运行函数 2.5 调仓函数 三、回测结果分析 3.1 收益净值图与概述 3.2 模型收益概览 3.3 年度收益图 3.4 月度收益的时间序列 3.5 月度收益热力图 3.6 月度收益频次分…

CorelDraw 2024软件安装包下载 丨不限速下载丨亲测好用

​简介&#xff1a; CorelDRAW Graphics Suite 订阅版拥有配备齐全的专业设计工具包&#xff0c;可以通过非常高的效率提供令人惊艳的矢量插图、布局、照片编辑和排版项目。价格实惠的订阅就能获得令人难以置信的持续价值&#xff0c;即时、有保障地获得独家的新功能和内容、…

conda安装pytorch使用清华源

原命令&#xff0c;例&#xff1a; # CUDA 11.3 conda install pytorch1.11.0 torchvision0.12.0 torchaudio0.11.0 cudatoolkit11.3 -c pytorch使用清华源&#xff0c;例&#xff1a; # CUDA 11.3 conda install pytorch1.11.0 torchvision0.12.0 torchaudio0.11.0 cudatool…

Qwen2——阿里巴巴最新的多语言模型挑战 Llama 3 等 SOTA

引言 经过几个月的期待&#xff0c; 阿里巴巴 Qwen 团队终于发布了 Qwen2 – 他们强大的语言模型系列的下一代发展。 Qwen2 代表了一次重大飞跃&#xff0c;拥有尖端的进步&#xff0c;有可能将其定位为 Meta 著名的最佳替代品 骆驼3 模型。在本次技术深入探讨中&#xff0c;我…

【redis的基本数据类型】

基本数据类型 Redis的基本数据类型有五种&#xff0c;分别是 StringListHashSetSortedSet 这些基本的数据类型构成了其他数据类型的基石&#xff0c;而这些基本数据类型又对应着不同的底层实现&#xff0c;不同的底层实现往往是针对不同的使用场景做的特殊的优化&#xff0c;…

探索档案未来,尽在ARCHE-2024

2024年第三届上海国际智慧档案展览会暨高峰论坛&#xff08;ARCHE-2024&#xff09;将于2024年6月19日至21日在上海跨国采购会展中心隆重举行。深圳市铨顺宏科技有限公司应邀参展&#xff0c;将以全新形象盛装亮相&#xff0c;展示其在档案管理领域的最新技术和解决方案。 ARC…

Linux系统脚本开机自启动,开机自启动jar包vue前台等

脚本内容jiaobenname.sh #!/bin/bash # 设置环境变量 export JAVA_HOME/usr/local/java/jdk-17.0.10 export CLASSPATH.:$JAVA_HOME/lib/ export PATH.:$JAVA_HOME/bin:$PATHwhile true; doif ps aux | grep -v grep | grep "tomcat" > /dev/null; thenecho &quo…

Vue42-vc与vm的原型对象

一、普通函数与其对象的原型对象 显示原型属性&#xff0c;只有函数才有&#xff01;&#xff01;&#xff01; 实例对象只有隐式原型属性。 普通函数与其对象&#xff0c;指向同一个原型对象&#xff01;&#xff01;&#xff01; 这么写不推荐&#xff0c;建议直接如下格式&a…

C++初学者指南第一步---2. Hello world

C初学者指南第一步—2. Hello world 目录 C初学者指南第一步---2. Hello world1.源文件 “Hello.cpp”2.编译hello.cpp3.术语4.编译器标志5.不要使用 “using namespace std;” &#xff01; 1.源文件 “Hello.cpp” #include <iostream> // our first program int main…

3.多层感知机

目录 1.感知机训练感知机XOR问题&#xff08;Minsky&Papert 1969&#xff09; AI的第一个寒冬总结 2.多层感知机(MLP)学习XOR单隐藏层&#xff08;全连接层&#xff09;激活函数&#xff1a;Sigmoid激活函数&#xff1a;Tanh激活函数&#xff1a;ReLu 最常用的 因为计算速度…

LLM 学习之「向量数据库」

LLM 学习之「向量数据库」 什么是向量数据库&#xff1f; 向量数据库是一种以向量或数据点的数学表示形式存储数据的数据库。 人工智能和机器学习使非结构化数据能够转换为捕获意义和上下文的数字表示&#xff08;向量&#xff09;&#xff0c;这得益于自然语言处理和计算机视…

VSCode格式化插件-prettier

VSCode格式化插件 1.安装插件&#xff1a;prettier 2.设置默认格式化工具 设置中&#xff0c;搜索 “Default Formatter”。 在编辑器设置中&#xff0c;将默认格式化工具设为 Prettier。 3.启用格式化选项&#xff1a; 在设置中搜索 “Format On Save”&#xff0c;并勾选…

git回滚项目至指定版本

用过两种方式 1.使用git命令 进入到项目目录地址&#xff0c;使用git log查看提交版本信息 按q退出 若非对应分支&#xff0c;使用git checkout 分支名 切换分支 这里回退至上一个版本 git reset --hard 版本号 再次推送即可 这里需要使用-f命令 强行推送 2.在pycharm中…

12.容器间的互联(--link 是单方向的!!!)

容器间的互联&#xff08;–link 是单方向的&#xff01;&#xff01;&#xff01;&#xff09; –link意思就是链接容器进行通信 用法&#xff1a;--link 容器名字:随意设置别名&#xff1b;例如&#xff1a;--link nginx:nginx 注释&#xff1a;同一个容器中&#xff0c;可…