我的同事最近给我发了一条消息,指向托管名为“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 语句。
输入:根据规则评估的数据。
上下文:将输入与规则联系起来的相关信息。
引擎:将输入与规则进行匹配的核心系统。
操作:引擎找到匹配项后触发的一组指令。
项目时间
一、要求
该项目旨在开发一个学生分类系统,根据一组预定义的规则将学生分配到适当的班级。
该系统将具有以下要求:
- 系统需要接收学生列表,每个学生都有一个唯一的ID、sex、age、IQ、assiduity 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. 友善的建议
在继续之前,我强烈建议您仔细考虑一下项目需求。
也许您可以起草自己的解决方案设计,或者更好的是设计和源代码。谁知道呢,你也许能找到比我更好的解决方案。
否则,请坐下来,给我你的注意力。
设计
考虑到项目的要求,Student和Class类显得很突出。每个学生都有一个、、、、、、、,ID而每个班级Name都有一个鞋底。SexAgeIQAssiduity scoreNationalitySchoolName
其他Class属性可能包括size和 ,area但这不会对当前的项目产生影响。
接下来,Allocations班级将学生分配到合适的班级。
出于可读性目的,我们将使用一个DataHandler类来执行所需的数据操作。
最后,我们将封装规则引擎在类中找到匹配项后执行的指令Actions。allocate_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陈述更容易的了。更明智的做法是想出一些方法来封装所有这些案例并委托其执行。
正如我们所见,使用“规则引擎”库绝对是其中一种方法。