使用場景規則匹配模式代替復雜的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號