使用场景规则匹配模式代替复杂的if else条件判断
缘起
在业务处理程序中, 经常需要按照不同的场景有不同的处理方式, 在代码库中也充斥着大量的复杂的 if/else 语句, 这类代码可维护性非常差, 底层原因有:
- 每个场景缺少定义,
- 将场景识别和场景的应对代码耦合在一起。
场景化的解决方案
在代码中将场景明确化,将识别场景的条件与应对场景做隔离开来。 底层相当于有一个精简版规则引擎。 另外,跑题说一下规则引擎,我认为使用DSL脚本语言设定规则, 虽说脚本语言可实现规则的即时修改即时生效,但这应该是一个最重要的特性,我认为引入规则引擎最大的优势是,它可将各种规则集中在一起,方便理解规则,使用编译语言定义规则在修改规则时也不复杂,而且还有很多语法检查特性,相比脚本DSL定义规则优势更多。
- Scenario 类, 定义场景的基本信息, 比如场景名称、场景识别条件等。
- ScenarioSelectionPolicyEnum 场景选择策略枚举, 比如选择高优先级场景, 还是低优先级场景, 还是返回所有符合条件的场景。
- ISencarioRepository 接口:所有场景规则的存储库
- ScenarioSelectionManager 类,按照场景选择策略, 将业务对象传入场景存储库中进行场景匹配。
示例代码
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Diagnostics.CodeAnalysis;
class Test
{
public static void Main()
{
ISencarioRepository ruleRepository = new BookDiscountRuleRepository();
var bizRuleSelectionManager = new ScenarioSelectionManager(ruleRepository, ScenarioSelectionPolicyEnum.HighestPriorty);
Book book1 = new Book()
{
Name = "book1",
Category = "category1",
PressHouse = "zhongxin",
Price = 100
};
Book book2 = new Book()
{
Name = "book2",
Category = "book2",
PressHouse = "xxx",
Price = 5
};
string traceMessage1;
List<Scenario> selectedScenarioList1 = bizRuleSelectionManager.Select(book1, out traceMessage1);
Console.WriteLine(book1.Name);
Console.WriteLine(traceMessage1);
}
}
/// <summary>
/// 场景定义类
/// </summary>
public class Scenario
{
/// <summary>
/// 场景名, 要求唯一
/// </summary>
/// <value></value>
public string Name { get; set; }
/// <summary>
/// 该场景是否被启用
/// </summary>
/// <value></value>
public bool Enabled { get; set; } = true;
/// <summary>
/// 是否是默认场景
/// </summary>
/// <value></value>
public bool IsDefault { get; set; } = false;
/// <summary>
/// 场景优先级
/// </summary>
/// <value></value>
public int Priority { get; set; }
/// <summary>
/// 场景识别条件
/// 函数传入一个业务对象, 返回值为boolean型,如果符合该场景则返回true
/// </summary>
/// <value></value>
public Func<object, bool> Condition { get; set; } = null;
/// <summary>
/// 场景规则可以附带的信息, 比如针对打折场景, 可以附上折扣
/// </summary>
/// <value></value>
public object Payload { get; set; } = null;
public override string ToString()
{
return $"Name:{Name}";
}
}
/// <summary>
/// 场景选择策略
/// </summary>
public enum ScenarioSelectionPolicyEnum
{
AllMatched,
HighestPriorty,
LowestPriority,
HighestPriortyOrDefault,
LowestPriorityOrDefault,
}
/// <summary>
/// 定义场景规则库的接口
/// </summary>
public interface ISencarioRepository
{
public List<Scenario> BuildScenarioRepository();
}
/// <summary>
/// 场景选择控制器
/// </summary>
public class ScenarioSelectionManager
{
ScenarioSelectionPolicyEnum _policy;
ISencarioRepository _scenarioRepository;
List<Scenario> _scenarioList;
private void CheckScenarioRepository(ScenarioSelectionPolicyEnum policy)
{
//TODO:
//检查是否有同名的场景
//检查是否有优先级相等的场景
}
private List<Scenario> SortScenarioList()
{
//TODO: 排序
return _scenarioRepository.BuildScenarioRepository();
}
public ScenarioSelectionManager(ISencarioRepository scenarioRepository, ScenarioSelectionPolicyEnum policy)
{
_scenarioRepository = scenarioRepository;
_policy = policy;
CheckScenarioRepository(policy);
_scenarioList = SortScenarioList();
}
/// <summary>
/// 按照策略来选择匹配的场景, 支持匹配多个场景以满足场景叠加需求
/// </summary>
/// <param name="bizObject"></param>
/// <param name="traceMessage"></param>
/// <returns></returns>
public List<Scenario> Select(object bizObject, out string traceMessage)
{
//TODO: 待完善
traceMessage = "";
List<String> notMatchedMsgList = new List<string>();
var selectedScenarioList = new List<Scenario>();
foreach (Scenario scenario in _scenarioList)
{
if (scenario.Enabled == false)
{
notMatchedMsgList.Add($"{scenario} disabled");
}
else if (scenario.Condition(bizObject))
{
selectedScenarioList.Add(scenario);
}
else
{
notMatchedMsgList.Add($"{scenario} not matched");
}
}
var matchedStr = "";
if (selectedScenarioList.Count != 0)
{
matchedStr = string.Join(",", selectedScenarioList);
matchedStr = $"Matched scenarios:{matchedStr}";
}
else
{
matchedStr = "No matched scenario";
}
var notMatchedStr = string.Join(",", notMatchedMsgList);
notMatchedStr = $"Not matched scenarios:{notMatchedStr}";
traceMessage = matchedStr + ", " + notMatchedStr;
return selectedScenarioList;
}
}
/// <summary>
/// 图书类, 即业务对象类
/// </summary>
class Book
{
public string Name { get; set; }
public double Price { get; set; }
public string PressHouse { get; set; }
public string Category { get; set; }
}
/// <summary>
/// 图书打折场景类, 即针对业务对象的场景规则库
/// </summary>
class BookDiscountRuleRepository : ISencarioRepository
{
public List<Scenario> BuildScenarioRepository()
{
List<Scenario> lst = new();
//场景1: 高价图书
Scenario highPriceScenario = new Scenario()
{
Name = nameof(highPriceScenario),
Enabled = true,
Priority = 10,
IsDefault = false,
Condition = (obj) => (obj as Book).Price > 100,
Payload = 0.5
};
lst.Add(highPriceScenario);
//中信出版的图书
Scenario ZhongxinPressScenario = new Scenario()
{
Name = nameof(ZhongxinPressScenario),
Enabled = true,
Priority = 10,
IsDefault = false,
Condition = (obj) => (obj as Book).PressHouse.ToUpper() == "ZHONGXIN",
Payload = 0.6
};
lst.Add(ZhongxinPressScenario);
//普调图书
Scenario SunShineScenario = new Scenario()
{
Name = nameof(SunShineScenario),
Enabled = true,
Priority = 10,
IsDefault = true,
Condition = (object obj) =>
{
return true;
},
Payload = 0.9
};
lst.Add(SunShineScenario);
return lst;
}
}
Scorecard 计算框架的业务用途
我们这里的 Scorecard 框架基于上面的场景化框架, 所以放在一个文章里说明,场景化框架可以为业务对象找到对应的场景,然后基于这些场景做后续的业务处理。
Scorecard 用途是: 当在资源受限情况下, 不仅需要对场景进行选择, 而且可能需要综合个场景给出一个评分, 进而按照这个评分(或参考排名次或百分位情况)进行业务对象优先级的设定,业务情形举例:
- 在机台处理能力受限情况下, 如何合理派货
- 在运力受限情况下, 如何合理派货
- 招投标评分
Scorecard 算法代码
···csharp
///
/// Scorecard 场景定义类
/// 每个业务对象的 finalScore = rawScore * normalizationRatio * weight
///
public class ScorecardScenario : Scenario
{
/// <summary>
/// raw score 的动态数据源
/// </summary>
public Func<object, double> rawScoreDynamicValue { get; set; } = null;
/// <summary>
/// raw score 的静态取值
/// </summary>
public double rawScoreStaticValue { get; set; } = 0;
/// <summary>
/// 归一化比例
/// </summary>
/// <value></value>
public double normalizationRatio { get; set; } = 1;
/// <summary>
/// 权重
/// </summary>
/// <value></value>
public double weight { get; set; } = 1;
}
public enum PercentileScaleEnum
{
Scale10, //十分位
Scale100, //百分位
Scale1000, //千分位
}
///
/// Scorecard 计算器
/// 可以计算业务对象的 finalScore, 以及排名次序, 以及百分位情况, 基于这些数据就可以确定
///
public class ScorecardCalculator
{
private ScenarioSelectionManager _scenarioSelectionManager;
public ScorecardCalculator(ISencarioRepository scenarioRepository)
{
ScenarioSelectionPolicyEnum policy = ScenarioSelectionPolicyEnum.AllMatched;
var scenarioSelectionManager = new ScenarioSelectionManager(scenarioRepository, policy);
}
/// <summary>
/// 计算得分
/// </summary>
/// <param name="bizObject"></param>
/// <param name="selectionMessage"></param>
/// <returns></returns>
public double Calcuate(object bizObject, out string selectionMessage)
{
List<Scenario> selectedScenarioList = _scenarioSelectionManager.Select(bizObject, out selectionMessage);
double finalScore = 0;
foreach (var orignalScenario in selectedScenarioList)
{
ScorecardScenario scenario = orignalScenario as ScorecardScenario;
double rawScore = scenario.rawScoreStaticValue;
if (scenario.rawScoreDynamicValue != null)
{
rawScore = scenario.rawScoreDynamicValue(bizObject);
}
finalScore += rawScore * scenario.normalizationRatio * scenario.weight;
}
return finalScore;
}
/// <summary>
/// 获取当前分数在所有分钟的排名,排名从1开始算起
/// </summary>
/// <param name="denseRank">是按照稀疏还稠密算法来排名</param>
/// <param name="thisScore">当前分数值</param>
/// <param name="allScoreList">所有的分数值</param>
/// <returns></returns>
public int Rank(bool denseRank, double thisScore, List<double> allScoreList)
{
//TODO:
// 稀疏排名算法: 按照 Linq 做排名,然后取 index
// 稠密排名算法: 按照 Linq distinct, 然后做排名,然后取 index
return 1;
}
/// <summary>
/// 计算百分位、千分位、十分位中的位置
/// </summary>
/// <param name="percentileScale"></param>
/// <param name="thisScore"></param>
/// <param name="allScoreList"></param>
/// <returns></returns>
public double CalcPercentilePosition(PercentileScaleEnum percentileScale, double thisScore, List<double> allScoreList)
{
//TODO:
// 算法: 先按照稀疏排名算出排名, 然后和总数量做对比
return 1;
}
}
···

浙公网安备 33010602011771号