使用场景规则匹配模式代替复杂的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 用途是: 当在资源受限情况下, 不仅需要对场景进行选择, 而且可能需要综合个场景给出一个评分, 进而按照这个评分(或参考排名次或百分位情况)进行业务对象优先级的设定,业务情形举例:

  1. 在机台处理能力受限情况下, 如何合理派货
  2. 在运力受限情况下, 如何合理派货
  3. 招投标评分

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;
}

}
···

posted @ 2025-09-28 06:55  harrychinese  阅读(23)  评论(0)    收藏  举报