逐级实施回退

Ama*_*l K 7 .net c# oop fallback design-patterns

我有一个ScoreStrategy描述如何计算测验分数的课程:

public class ScoreStrategy
{
    public int Id { get; set; }

    public int QuizId { get; set; }

    [Required]
    public Quiz Quiz { get; set; }

    public decimal Correct { get; set; }

    public decimal Incorrect { get; set; }

    public decimal Unattempted { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

三个属性CorrectIncorrectUnattempted描述要为响应分配多少点。这些点也可以是负的。评分策略适用于测验中的所有问题,因此ScoreStrategy每个测验只能有一个。我有两个子类:

public class DifficultyScoreStrategy : ScoreStrategy
{  
    public QuestionDifficulty Difficulty { get; set; }
}

public class QuestionScoreStrategy : ScoreStrategy
{ 
     [Required]
     public Question Question { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

我的问题有三个难度级别(Easy, Medium, Hard;QuestionDifficulty是一个枚举)。该DifficultyScoreStrategy指定如果的具体困难需要问题点进行不同的分配。这将覆盖ScoreStrategy适用于整个测验的基础。每个难度级别可以有一个实例。

第三,我有一个QuestionScoreStrategy课程,指定是否必须以不同的方式授予特定问题的分数。这将覆盖测验范围ScoreStrategy和难度范围DifficultyStrategy。每个问题可以有一个实例。

在评估测验的反应时,我想实现一个逐级回退机制

对于每个问题:

  • 检查问题是否存在QuestionScoreStrategy,如果找到则返回策略。
  • 如果没有,则回退DifficultyScoreStrategy并检查是否存在针对所评估问题的难度级别的策略,如果找到策略则返回该策略。
  • 如果没有,回退到测验范围ScoreStrategy并检查是否存在,如果存在则返回它,
  • 如果ScoreStrategy两者都没有,请使用默认值{ Correct = 1, Incorrect = 0, Unattempted = 0 }(如果我也可以将其设置为可配置,那就太棒了,就像 .NET 的优雅方式:
options => {
    options.UseFallbackStrategy(
        correct: 1, 
        incorrect: 0, 
        unattempted: 0
    );
} 
Run Code Online (Sandbox Code Playgroud)

)。

概括

我已将上述信息汇总在一个表格中:

策略类型 优先事项 每个测验的最大实例数
QuestionScoreStrategy 第一名(最高) 测验中有多少问题
DifficultyScoreStrategy 第二 4、每个难度级别一个
ScoreStrategy 第三名 只有一个
回退策略
(默认{ Correct = 1, Incorrect = 0, Unattempted = 0}
第四名(最低) 为整个应用程序配置。由所有测验共享

我有一个名为的容器类EvaluationStrategy,其中包含这些评分策略以及其他评估信息:

partial class EvaluationStrategy
{
    public int Id { get; set; }

    public int QuizId { get; set; }

    public decimal MaxScore { get; set; }

    public decimal PassingScore { get; get; }

    public IEnumerable<ScoreStrategy> ScoreStrategies { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

我尝试过的:

我添加了一个调用上面GetStrategyByQuestion()相同EvaluationStrategy类的方法(注意它被声明为partial),它实现了这个回退行为,还有一个伴随索引器,它反过来调用这个方法。我已经声明了两个HashSet的种SDifficultyScoreStrategyQuestionScoreStrategyInitialize()方法实例化它们。然后所有的分数策略按类型切换并添加到适当的HashSetScoreStrategy每个测验只能有一个,它将存储在defaultStrategy

partial class EvaluationStrategy
{
    private ScoreStrategy FallbackStrategy = new() { Correct = 1, Incorrect = 0, Unattempted = 0 }; 
    private ScoreStrategy defaultStrategy;
    HashSet<DifficultyScoreStrategy> dStrategies;
    HashSet<QuestionScoreStrategy> qStrategies;


    public void Initialize()
    {
        qStrategies = new();
        dStrategies = new();
        // Group strategies by type
        foreach (var strategy in strategies)
        {
            switch (strategy)
            {
                case QuestionScoreStrategy qs: qStrategies.Add(qs); break;
                case DifficultyScoreStrategy ds: dStrategies.Add(ds); break;
                case ScoreStrategy s: defaultStrategy = s; break;
            }
        }
    }

    public ScoreStrategy this[Question question] => GetStrategyByQuestion(question);
    

    public ScoreStrategy GetStrategyByQuestion(Question question)
    {
        if (qStrategies is null || dStrategies is null)
        {
            Initialize();
        }
        // Check if question strategy exists
        if (qStrategies.FirstOrDefault(str => str.Question.Id == question.Id) is not null and var qs)
        {
            return qs;
        }
        // Check if difficulty strategy exists
        if (dStrategies.FirstOrDefault(str => str.Question.Difficulty == question.Difficulty) is not null and var ds)
        {
            return ds;
        }
        // Check if default strategy exists
        if (defaultStrategy is not null)
        {
            return defaultStrategy;
        }
        // Fallback
        return FallbackStrategy;
    }
}
Run Code Online (Sandbox Code Playgroud)

这种方法看起来有点笨拙,对我来说不太合适。使用部分类并添加到EvalutationStrategy似乎也不正确。如何实现这种逐级回退行为?有没有一种设计模式/原则,我可以在这里使用?如果未配置,我知道 .NET 框架中的许多内容会回退到默认约定。我需要类似的东西。或者有人可以简单地推荐一个具有更好可维护性的更简洁优雅的解决方案?


注意/附加信息:所有测验的ScoreStrategys 和EvaluationStrategy所有测验都存储在由 EF Core(.NET 5) 管理的数据库中,并带有 TPH 映射:

modelBuilder.Entity<ScoreStrategy>()
                .ToTable("ScoreStrategy")
                .HasDiscriminator<int>("StrategyType")
                .HasValue<ScoreStrategy>(0)
                .HasValue<DifficultyScoreStrategy>(1)
                .HasValue<QuestionScoreStrategy>(2)
                ;
modelBuilder.Entity<EvaluationStrategy>().ToTable("EvaluationStrategy");
Run Code Online (Sandbox Code Playgroud)

我有一个基地DbSet<ScoreStrategy> ScoreStrategies和另一个DbSet<EvaluationStrategy> EvaluationStrategies. 因为EvaluationStrategy是一个 EF Core 类,所以我对GetStrategyByQuestion()向它添加 logic( )有点怀疑。

Pet*_*ala 1

与波莉

有一个名为Polly 的第三方库,它定义了名为Fallback的策略。

通过这种方法,您可以轻松定义如下后备链:

public ScoreStrategy GetStrategyByQuestionWithPolly(Question question)
{
    Func<ScoreStrategy, bool> notFound = strategy => strategy is null;

    var lastFallback = Policy<ScoreStrategy>
        .HandleResult(notFound)
        .Fallback(FallbackStrategy);

    var defaultFallback = Policy<ScoreStrategy>
        .HandleResult(notFound)
        .Fallback(defaultStrategy);

    var difficultyFallback = Policy<ScoreStrategy>
        .HandleResult(notFound)
        .Fallback(() => GetApplicableDifficultyScoreStrategy(question));

    var fallbackChain = Policy.Wrap(lastFallback, defaultFallback, difficultyFallback);
    fallbackChain.Execute(() => GetApplicableQuestionScoreStrategy(question));
}
Run Code Online (Sandbox Code Playgroud)

QuestionScoreStrategy我提取了如下的策略选择逻辑DifficultyScoreStrategy

private ScoreStrategy GetApplicableQuestionScoreStrategy(Question question)
    => qStrategies.FirstOrDefault(str => str.Question.Id == question.Id);

private ScoreStrategy GetApplicableDifficultyScoreStrategy(Question question)
    => dStrategies.FirstOrDefault(str => str.Difficulty == question.Difficulty);
Run Code Online (Sandbox Code Playgroud)

优点

  • 有一个单一的return声明
  • 策略声明与链接分离
  • 每个回退都可以由不同的条件触发
  • 主要选择逻辑与后备逻辑分离

缺点

  • 代码确实重复了
  • 您无法利用流畅的 API 创建后备链
  • 您需要使用第三方库

没有波莉

如果您不想仅使用第三方库来定义和使用后备链,您可以执行以下操作:

public ScoreStrategy GetStrategyBasedOnQuestion(Question question)
{
    var fallbackChain = new List<Func<ScoreStrategy>>
    {
        () => GetApplicableQuestionScoreStrategy(question),
        () => GetApplicableDifficultyScoreStrategy(question),
        () => defaultStrategy,
        () => FallbackStrategy
    };

    ScoreStrategy selectedStrategy = null;
    foreach (var strategySelector in fallbackChain)
    {
        selectedStrategy = strategySelector();
        if (selectedStrategy is not null)
            break;
    }

    return selectedStrategy;
}
Run Code Online (Sandbox Code Playgroud)

优点

  • 有一个单一的return声明
  • Fallback链声明和评估是分开的
  • 简单又简洁

缺点

  • 它不太灵活:每个后备选择都是由相同的条件触发的
  • 主要选择与后备选择并不分开