百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

规则引擎消除 If-Else 语句的新库

toyiye 2024-06-21 11:55 13 浏览 0 评论


我的同事最近给我发了一条消息,指向托管名为“RulesEngine”的 Microsoft C# 库的 GitHub 存储库的链接。

有消息称,该库已经引起了我们开发团队的注意。

果然,它没有引起我的注意——我从来没有听说过这样的图书馆。

但由于我总是热衷于跟上最新的软件开发趋势,所以我决定深入研究。

事实证明我做了一个很好的决定。

如果使用得当,“规则引擎”绝对会大放异彩!

多亏了它,我可以消除端到端项目中复杂的 if-else 语句的整个语料库。

在本文中,我将引导您完成我完成的完全相同的项目,展示规则引擎的卓越功能。

Table of Contents

    Rules Engine: Definition?
    Rules Engine: Architecture?
    Project Time:
        #1. Requirements
        #2. Data
        #3. Business Rules
        #4. Friendly Advice
    Design
    Implementation
        #1. Input/Output
        #2. Rules Handling
        #3. Students and Classes
        #4. Allocations
    Testing
    Code Quality: Cyclomatic Complexity
    Final Thoughts

由于项目源代码采用 Python,我们将使用“RulesEngine”Python 邻居,rule_engine,它由ZeroSteiner积极维护。


规则引擎:定义?

规则引擎是一种旨在编码、执行和管理业务规则的设计模式。这些规则通常存储在数据库中,但也可以适应其他存储机制。

规则引擎在复杂软件中特别有吸引力,因为系统的行为需要灵活并能够响应不断变化的业务需求(规则)。

传统上,开发人员需要在代码库和不断变化的业务需求之间来回切换,以便两者保持同步。

这种方法存在三个问题:

首先,从用户体验的角度来看,它很平凡且非常有限——无法从 UI 管理需求。

其次,它不能由非开发人员管理,因为业务需求的变化需要将更改推送到代码库。

第三,它违反了 SOLID 原则之一。开闭原则:

“软件实体……应该对扩展开放,但对修改关闭。”

这很重要,因为主代码库的错误修改可能会破坏系统。

然而,规则引擎满足上述所有要求。

规则引擎:架构?



一个功能齐全的规则引擎系统需要以下组件:

规则:一个树状的条件序列,在这些条件下可以执行一组操作——可以说是一堆 if-else 语句。

输入:根据规则评估的数据。

上下文:将输入与规则联系起来的相关信息。

引擎:将输入与规则进行匹配的核心系统。

操作:引擎找到匹配项后触发的一组指令。

项目时间

一、要求

该项目旨在开发一个学生分类系统,根据一组预定义的规则将学生分配到适当的班级。

该系统将具有以下要求:

  • 系统需要接收学生列表,每个学生都有一个唯一的IDsexageIQassiduity score、 和school作为输入。
  • 系统需要能够对每个学生应用一组预定义的规则。
  • 系统需要为每个学生应用第一个匹配规则。
  • 需要根据学生的属性来定义规则。例如,规则可能要求学生达到特定年龄、性别,或者具有最低智商或勤奋分数才能分配到特定班级。
  • 系统必须提供添加、修改、删除规则的接口。
  • 系统应该能够生成一份报告,显示每个学生分配的班级。
  • 该系统旨在有效地处理大量学生。
  • 该系统应该易于使用,并且需要最少的技术专业知识即可操作。
  • 该系统应该是可扩展的、灵活的,并且能够轻松适应规则或学生属性的变化。

总体而言,该系统将确保每个学生被分配到最合适的班级,这将有助于提高他们的学业成绩和整体在校经历。

2. 数据

我们该项目的数据集是一个包含 400 名学生的 CSV文件。CSV 上的条目是虚构的,是使用 Python脚本随机生成的。

3、业务规则

Rainbow High (RH):
   Rule 1: Females/Male with IQ score > 116 -> Class A
   Rule 2: Male/Female with IQ < 90 -> Class B
   Rule 3: Male/Female of age < 18 go to -> Class C
   Rule 4: Male with assiduity score <= 2 -> Class D
   Rule 5: Females with assidiuty score <= 2 -> Class E
   Rule 6: rest -> Class F

Waterfalls School for Girls (WSG):
   Rule 1: Female with IQ score > 116 -> Class A
   Rule 2: Female with IQ score < 90 -> Class B
   Rule 3: Female of age < 18 go to -> Class C
   Rule 4: Female with assiduity score <= 2 -> Class D
   Rule 5: rest of females -> Class F

Cape Coral High (CCH):
   Rule 1: age < 18 and IQ > 116 and Home -> Class A
   Rule 2: age < 18 and IQ > 116 and International -> Class B
   Rule 3: age > 18 with IQ < 90 -> Class C
   Rule 4: assiduity score >= 4 -> Class D
   Rule 5: assiduity score <= 2 -> Class E
   Rule 6: rest -> Class F

4. 友善的建议

在继续之前,我强烈建议您仔细考虑一下项目需求。

也许您可以起草自己的解决方案设计,或者更好的是设计和源代码。谁知道呢,你也许能找到比我更好的解决方案。

否则,请坐下来,给我你的注意力。

设计

考虑到项目的要求,StudentClass类显得很突出。每个学生都有一个、、、、、、、,ID而每个班级Name都有一个鞋底。SexAgeIQAssiduity scoreNationalitySchoolName

其他Class属性可能包括size和 ,area但这不会对当前的项目产生影响。

接下来,Allocations班级将学生分配到合适的班级。

出于可读性目的,我们将使用一个DataHandler类来执行所需的数据操作。

最后,我们将封装规则引擎在类中找到匹配项后执行的指令Actionsallocate_students()因此,降低了我们类中方法的复杂性Allocations

从图表上看,设计应如下所示:


执行

1. 输入/输出

此类项目首先要考虑的是输入。

在本例中,我们的输入是一个包含 400 个条目的 CSV 文件,其中包含每个学生的信息。

然而,我们的输出是与分配的类连接的输入数据集。

下面的DataHandler类允许我们执行上述操作:

class DataHandler:
    
    def __init__(self, file_path):
        self.file_path = file_path
    
    def load_students(self):
        """
        Load data into a list of students objects
        """
        students = []
        with open(self.file_path, mode="r") as file:

            reader = csv.DictReader(file)

            # Load data from frile_path 
            for student in reader:

                # Convert columns to appropriate data types
                student = Student(
                    int(student["ID"]),
                    str(student["name"]),
                    str(student["sex"]),
                    int(student["age"]),
                    int(student["IQ"]),
                    int(student["assiduity_score"]),
                    str(student["nationality"]),
                    str(student["school"])
                )

                # Add to students list
                students.append(student)
                
        return students

    def save_students(self, input_path, output_path, allocations):
        """
        Append students dataset with allocated classes and
        save to output_path
        """
        with open(input_path, mode='r') as input_file, \
            open(output_path, mode='w', newline='') as output_file:
            
            reader = csv.DictReader(input_file)
            writer = csv.DictWriter(
                output_file, 
                fieldnames=reader.fieldnames + ["class"]
                )

            # Populate headers
            writer.writeheader()

            # Write data to output_path
            for row in reader:
                
                row["class"] = allocations[int(row["ID"])].name
                writer.writerow(row)

2. 规则处理

现在,我们应该考虑一种适当的方法来处理规则。理想情况下,您希望使用适当的存储机制(例如数据库)来存储规则,以适应未来业务需求的变化。

为了演示,我们将规则存储在列表中:

rules = {
    # Rainbow High
    "school == 'Rainbow High'": [
        "IQ > 116",
        "IQ < 90",
        "age > 18",
        "sex == 'Male' and assiduity_score <= 2",
        "sex == 'Female' and assiduity_score <= 2",
        "true",
    ],

    # Waterfalls School for Girls 
    "school == 'Waterfalls School for Girls'": [
        "IQ > 116",
        "IQ < 90",
        "age < 18",
        "assiduity_score >= 4",
        "assiduity_score <= 2",
        "true",
    ],

    # Cape Coral High
    "school == 'Cape Coral High'": [
        "age < 18 and IQ > 116 and nationality == 'Home'",
        "age < 18 and IQ > 116 and nationality == 'International'",
        "age > 18 and IQ < 90",
        "assiduity_score >= 4",
        "assiduity_score <= 2",
        "true"
    ],
} 

3. 学生和类

Students和类的实现Class是我们设计中的精确副本:

class Class:

    def __init__(self, name):
        self.name = name

    def __eq__(self, other): 
        return self.name == other.name


class Student:
    
    def __init__(self, ID, name, sex, age, IQ, assiduity_score, nationality, school):
        self.ID = ID
        self.name = name
        self.sex = sex
        self.age = age
        self.IQ = IQ
        self.assiduity_score = assiduity_score
        self.nationality = nationality
        self.school = school

4. 分配

为了将每个学生与相应的班级相匹配,我们可以毫不费力地if围绕每个规则包装一个语句(在进行一些字符串操作之后)。或者更好的是,我们可以使用规则引擎。

我们将两者都做,然后看看!

4.1. 条件分支
将规则转换为一组条件的最简单方法是,在使用函数将其转换为实际条件之前,将规则重新格式化为看起来像条件?的字符串eval()

把所有的东西拼凑在一起,我们的Allocations班级就变成了:

class Allocations:

    def __init__ (self, allocations):
        self.allocations = allocations

    @staticmethod
    def convert_rules_if_statements(rule):
        """
        Convert into rules to be evaluated
        """
        return re.sub(
            r'\b(IQ|age|sex|assiduity_score|nationality|school)\b', 
            r'student.\1',
            rule
        )

    def allocate_students_if_statements(self, rules, students):
        """
        Allocate students to their respective classes using 
        conditional branching
        """
        for student in students:
            
            # Evaluate parent rules
            if (eval(Allocations.convert_rules_if_statements(list(rules.keys())[0]))):

                # Evaluate child rules
                if (eval(Allocations.convert_rules_if_statements(list(rules.values())[0][0]))):
                    self.allocations[student.ID] = Class("A")

                elif (eval(Allocations.convert_rules_if_statements(list(rules.values())[0][1]))):
                    self.allocations[student.ID] = Class("B")

                elif (eval(Allocations.convert_rules_if_statements(list(rules.values())[0][2]))):
                    self.allocations[student.ID] = Class("C")

                elif (eval(Allocations.convert_rules_if_statements(list(rules.values())[0][3]))):
                    self.allocations[student.ID] = Class("D")

                elif (eval(Allocations.convert_rules_if_statements(list(rules.values())[0][4]))):
                    self.allocations[student.ID] = Class("E")

                else:
                    self.allocations[student.ID] = Class("F")

            elif (eval(Allocations.convert_rules_if_statements(list(rules.keys())[1]))):

                if (eval(Allocations.convert_rules_if_statements(list(rules.values())[1][0]))):
                    self.allocations[student.ID] = Class("A")

                elif (eval(Allocations.convert_rules_if_statements(list(rules.values())[1][1]))):
                    self.allocations[student.ID] = Class("B")

                elif (eval(Allocations.convert_rules_if_statements(list(rules.values())[1][2]))):
                    self.allocations[student.ID] = Class("C")

                elif (eval(Allocations.convert_rules_if_statements(list(rules.values())[1][3]))):
                    self.allocations[student.ID] = Class("D")

                elif (eval(Allocations.convert_rules_if_statements(list(rules.values())[1][4]))):
                    self.allocations[student.ID] = Class("E")

                else:
                    self.allocations[student.ID] = Class("F")

            else:

                if (eval(Allocations.convert_rules_if_statements(list(rules.values())[2][0]))):
                    self.allocations[student.ID] = Class("A")

                elif (eval(Allocations.convert_rules_if_statements(list(rules.values())[2][1]))):
                    self.allocations[student.ID] = Class("B")

                elif (eval(Allocations.convert_rules_if_statements(list(rules.values())[2][2]))):
                    self.allocations[student.ID] = Class("C")

                elif (eval(Allocations.convert_rules_if_statements(list(rules.values())[2][3]))):
                    self.allocations[student.ID] = Class("D")

                elif (eval(Allocations.convert_rules_if_statements(list(rules.values())[2][4]))):
                    self.allocations[student.ID] = Class("E")

                else:
                    self.allocations[student.ID] = Class("F")
                    
        return self.allocations

这就是我的小脑袋能想出的最好结果了。

4.2. 规则引擎
在基于条件的实现中,我们将要执行的指令放在if-else块内,对于这种方法来说这是很自然的事情。

然而,在规则引擎实现中,您希望将指令封装在一个单独的类中——Actions可以说是一个类。因此,可以实现更好的可维护性和可扩展性。

从编程上来说,我们的Actions外观如下:

class Actions:

    def __init__ (self, logic):
        self.logic = logic # A rule_index/class map

    def compute_class(self, rules, rule):
        """
        Find the class that corresponds to a rule
        """
        return self.logic[rules.index(rule)]

    def match_ID_class(self, allocations, match_ID, rules, rule):
        """
        Add ID/class pairs to allocations dictionary
        """
        return allocations.setdefault(
            match_ID, self.compute_class(rules, rule)
        )

一旦您记下操作,规则必须转换为规则对象,以便库可以解释它们rule_engine。这就是上下文发挥作用的地方。

您可以将其视为向规则引擎提供上下文。

然后,规则引擎可以提取这些规则并将其与每个学生进行匹配。

将所有内容拼接起来会产生以下代码:

class Allocations:

    def __init__ (self, allocations):
        self.allocations = allocations

    @staticmethod
    def convert_rules_rule_engine(rules):
        """
        Convert into rule objects to be parsed by rule engine 
        """

        # Declare context
        context = Context(resolver=resolve_attribute)
        
        # Convert rules
        reng_rules = {}
        for parent_rule, child_rules in rules.items():

            # convert child rules to rules_engine rule
            reng_child_rules = []
            for child_rule in child_rules:
                reng_child_rules.append(Rule(child_rule, context=context))

            # convert child rules to rules_engine rule
            reng_rules[Rule(parent_rule, context=context)] = reng_child_rules
        
        return reng_rules

    def allocate_students_rule_engine(self, actions, rules, students):
        """
        Allocate students to their respective classes using 
        rule engine
        """

        # Convert rule to "rule_engine" rule
        reng_rules = Allocations.convert_rules_rule_engine(rules)

        # Allocate students
        for parent_rule, child_rules in reng_rules.items():

            # Match students to parent rule
            matche1 = list(parent_rule.filter(students))
            for child_rule in child_rules:

                # Match students to child rules
                for match2 in child_rule.filter(matche1):

                    # Execute actions
                    actions.match_ID_class(
                        self.allocations, match2.ID, child_rules, child_rule
                    )

        return self.allocations

测试

最后,我们要确保我们的代码能够完成它应该做的事情。这就是测试发挥作用的地方。

由于我们的代码严重依赖于分支,因此我们可以构建测试,使每个测试套件对应于一所学校。

在编写测试用例时,您最好希望获得更高的代码覆盖率(95% 以上)。

在这个项目中实现这一点相当简单(尽管很耗时)。您需要做的就是测试每个规则(即每个if语句)。

    def test_rainbow_high_rules(self):

        self.assertEqual(self.alloc[263], Class("A"))
        self.assertEqual(self.alloc[41], Class("B"))
        self.assertEqual(self.alloc[5], Class("C"))
        self.assertEqual(self.alloc[57], Class("D"))
        self.assertEqual(self.alloc[118], Class("E"))
        self.assertEqual(self.alloc[375], Class("F"))


    def test_waterfalls_school_for_girls_rules(self):

        self.assertEqual(self.alloc[374], Class("A"))
        self.assertEqual(self.alloc[42], Class("B"))
        self.assertEqual(self.alloc[348], Class("C"))
        self.assertEqual(self.alloc[282], Class("D"))
        self.assertEqual(self.alloc[30], Class("E"))
    
    def test_cape_coral_high_rules(self):

        self.assertEqual(self.alloc[250], Class("A"))
        self.assertEqual(self.alloc[335], Class("B"))
        self.assertEqual(self.alloc[127], Class("C"))
        self.assertEqual(self.alloc[39], Class("D"))
        self.assertEqual(self.alloc[175], Class("E"))
        self.assertEqual(self.alloc[204], Class("F"))

您可以看到我们所有的测试都通过了:

...
----------------------------------------------------------------------
Ran 3 tests in 0.092s

OK

这适用于条件引擎和规则引擎实现。

代码质量:循环复杂度

圈复杂度是用于衡量程序复杂度的代码质量度量。

它基于程序的控制流,控制流是指程序如何根据不同的条件执行不同的指令。

高圈复杂度通常会导致意大利面条式代码。它表明该程序更加复杂并且更难理解、测试和维护。

在我们的例子中,我们将使用一个名为radon的 Python 库来计算程序的圈复杂度。

我们着眼于以下几点:

  • 对于规则引擎的实现
    C 48:0 Allocations - A
    M 86:4 Allocations.allocate_students_rule_engine - A
    M 54:4 Allocations.convert_rules_rule_engine - A
    C 184:0 DataHandler - A
    C 6:0 Class - A
    C 15:0 Student - A
    C 28:0 Actions - A
    M 189:4 DataHandler.load_students - A
    M 218:4 DataHandler.save_students - A
    M 8:4 Class.__init__ - A
    M 11:4 Class.__eq__ - A
    M 17:4 Student.__init__ - A
    M 30:4 Actions.__init__ - A
    M 33:4 Actions.compute_class - A
    M 39:4 Actions.match_ID_class - A
    M 50:4 Allocations.__init__ - A
    M 186:4 DataHandler.__init__ - A
  • 为了if_statement实施
    M 113:4 Allocations.allocate_students_if_statements - C
    C 48:0 Allocations - B
    C 184:0 DataHandler - A
    C 6:0 Class - A
    C 15:0 Student - A
    C 28:0 Actions - A
    M 189:4 DataHandler.load_students - A
    M 218:4 DataHandler.save_students - A
    M 8:4 Class.__init__ - A
    M 11:4 Class.__eq__ - A
    M 17:4 Student.__init__ - A
    M 30:4 Actions.__init__ - A
    M 33:4 Actions.compute_class - A
    M 39:4 Actions.match_ID_class - A
    M 50:4 Allocations.__init__ - A
    M 76:4 Allocations.convert_rules_if_statements - A
    M 186:4 DataHandler.__init__ - A

请注意,Allocationsif 语句实现的类获得了 B,而与规则引擎实现相关的类获得了 A。该方法的 C 等级allocate_students_if_statements()损害了该类的分数Allocations

最后的想法

有条件的分支继续滋生两极分化的粉丝群。

虽然有些人相信软件开发没有条件就无法顺利进行,但另一些人则认为if-else’s命运和goto’s.

我个人支持前者——if-else声明不会有任何结果。然而,我也认为应该尽可能避免它们,因为它们给代码增加了一层复杂性。

这个项目就是证明。

没有什么比把案例变成if-else陈述更容易的了。更明智的做法是想出一些方法来封装所有这些案例并委托其执行。

正如我们所见,使用“规则引擎”库绝对是其中一种方法。

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码