【问题标题】:Implementing level by level fallback逐级实施回退
【发布时间】:2023-03-17 03:58:01
【问题描述】:

我有一个课程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; }
}

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

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

public class QuestionScoreStrategy : ScoreStrategy
{ 
     [Required]
     public Question Question { get; set; }
}

我的问题有三个难度级别(EasyMediumHardQuestionDifficulty 是一个枚举)。 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
    );
} 

)。

总结

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

Strategy Type Priority Maximum instances per quiz
QuestionScoreStrategy 1st (highest) As many as there are questions in the quiz
DifficultyScoreStrategy 2nd 4, one for each difficulty level
ScoreStrategy 3rd Only one
Fallback strategy
(Default { Correct = 1, Incorrect = 0, Unattempted = 0})
4th (lowest) Configured for the entire app. Shared by all quizzes

我有一个名为 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; }
}

我尝试过的:

我在上面的同一个EvaluationStrategy 类中添加了一个名为GetStrategyByQuestion() 的方法(注意它被声明为partial),它实现了这个回退行为,并且还添加了一个伴随索引器,它反过来调用这个方法。我已经声明了两个HashSets 类型DifficultyScoreStrategyQuestionScoreStrategy 和一个Initialize() 方法实例化它们。然后将所有评分策略按类型切换并添加到相应的HashSet,每个测验只能有一个ScoreStrategy,将存储在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;
    }
}

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


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

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

我有一个单一的基地DbSet&lt;ScoreStrategy&gt; ScoreStrategies 和另一个DbSet&lt;EvaluationStrategy&gt; EvaluationStrategies。由于EvaluationStrategy 是一个 EF Core 类,我对向其添加逻辑(GetStrategyByQuestion())也持怀疑态度。

【问题讨论】:

    标签: c# .net oop design-patterns fallback


    【解决方案1】:

    我想所有数据(问题、策略、测验都存储在数据库中)。然后我会期望这样的方式来获得每个策略:

    问题策略

    var questionStrategy = dbContext.ScoreStrategies.SingleOrDefault(ss => ss.QuesionId == question.Id);
    

    难度策略:

    var difficultyStrategy = dbContext.ScoreStrategies.SingleOrDefault(ss => ss.Difficulty == question.Difficulty);
    

    测验的默认策略:

    var quizStrategy = dbContext.ScoreStrategies.SingleOrDefault(ss => ss.QuizId == question.QuizId)
    

    基于此以及您已经提供的内容,策略只是三个数字:正确答案的分数,错误和未尝试答案的分数。

    所以这是抽象类的完美候选者,它将为三个实体提供基类 - 三种策略 - 这将是三个表,因为每个表都有不同的关系:

    public abstract class ScoreStrategy
    {
        public double Correct { get; set; }
        public double Incorrect { get; set; }
        public double Unattempted { get; set; }
    }
    // Table with FK relation to Questions table
    public class QuestionScoreStrategy : ScoreStrategy
    {
        public Question { get; set; }
        public int QuestionId { get; set; }
    }
    // If you have table with difficulties, there should be FK relation to it.
    // If you do not have table - it's worth consideration, you could then 
    // easily add more difficulties.
    public class DifficultyStrategy : ScoreStrategy
    {
        public QuestionDifficulty Difficulty { get; set; }
    }
    // FK relation to Quizes table
    public class QuizScoreStrategy : ScoreStrategy
    {
        public Quiz { get; set; }
        public int QuizId { get; set; }
    }
    

    这样您最终会得到只存储相关数据的粒度良好的表。

    那么,用法会变成:

    // Ideally, this method should be in some repoistory (look at repository design pattern) in data access layer
    // and should leverage usage of async / await as well.
    public ScoreStrategy GetScoreStrategy(Question question)
    {
        return dbContext.QuestionScoreStrategies.SingleOrDefault(qs => qs.QuestionId == question.Id)
            ?? dbContext.DifficultyStrategies.SingleOrDefault(ds => ds.Difficulty == question.Difficulty)
            ?? dbContext.QuizScoreStrategies.SingleOrDefault(qs => qs.QuizId == question.QuizId);
    }
    

    那么你可以这样使用这个方法:

    // This should be outside data access layer. Here you perform logic of getting question.
    // This could be some ScoringManager class which should be singleton (one instance only).
    // Then you could define fallback in private fields:
    private readonly double FALLBACK_CORRECT_SCORE;
    private readonly double FALLBACK_INCORRECT_SCORE;
    private readonly double FALLBACK_UNATTEMPTED_SCORE;
    // private constructor, as this should be singleton
    private ScoringManager(double correctScore, double incorrectScore, double unattemptedScore)
        => (FALLBACK_CORRECT_SCORE, FALLBACK_INCORRECT_SCORE, FALLBACK_UNATTEMPTED_SCORE) =
           (correctScore, incorrectScore, unattemptedScore);
     
    public double CalcScoreForQuestion(Question question)
    {
        var scoreStrategy = GetScoreStrategy(question);
        if (question answered correctly) 
            return scoreStrategy?.Correct ?? FALLBACK_CORRECT_SCORE;
        if (question answered incorrectly) 
            return scoreStrategy?.Incorrect ?? FALLBACK_INCORRECT_SCORE;
        if (question unattempted) 
            return scoreStrategy?.Unattempted ?? FALLBACK_UNATTEMPTED_SCORE;
    }
    

    注意

    这只是我如何组织事物的草稿,很可能在编写代码时我会提出改进,但我认为这是前进的方向。例如,ScoringManager 可以有 ConfigureFallbackScore 方法,这将允许动态更改后备分数(这需要使各个字段不是 readonly)。

    更新

    定义后备策略,为此定义枚举:

    public enum FallbackLevel
    {
        Difficulty,
        Question,
        Quiz,
    }
    

    然后评分经理可以有方法来配置策略(连同支持字段):

    private FallbackLevel _highPrecedence;
    private FallbackLevel _mediumPrecedence;
    private FallbackLevel _lowPrecedence;
    
    public void ConfigureFallbackStrategy(FallbackLevel highPrecedence, FallbackLevel mediumPrecedence, FallbackLevel lowPrecedence)
    {
        _highPrecedence = highPrecedence;
        _mediumPrecedence = mediumPrecedence;
        _lowPrecedence = lowPrecedence;
    }
    

    然后我们会在管理器中编写获取策略逻辑:

    public ScoreStrategy GetScoreStrategy(Question question)
    {
       var scoreStrategy = GetScoreStrategy(_highPrecedence, question)
           ?? GetScoreStrategy(_mediumPrecedence, question)
           ?? GetScoreStrategy(_lowPrecedence, question);
    }
    
    private ScoreStrategy GetScoreStrategy(FallbackLevel lvl, Question question) => lvl switch
    {
        FallbackLevel.Difficulty => dbContext.DifficultyStrategies.SingleOrDefault(ds => ds.Difficulty == question.Difficulty),
        FallbackLevel.Question => dbContext.QuestionScoreStrategies.SingleOrDefault(qs => qs.QuestionId == question.Id),
        FallbackLevel.Quiz => dbContext.QuizScoreStrategies.SingleOrDefault(qs => qs.QuizId == question.QuizId),
    }
    

    通过这种方式,您可以非常轻松地以任何您想要的方式配置后备策略。当然,还有一些注意事项:

    • 确保所有后备策略都是唯一的,例如,不可能有相同的高、中和低策略,
    • 只能通过存储库模式访问数据库上下文
    • 添加更多健全性检查(如空值等)

    我省略了这些部分,因为我专注于纯粹的功能。

    【讨论】:

    • @AmalK 另外,您应该标记接受的答案(当然,不需要与您授予赏金的答案相同)。
    【解决方案2】:

    和波莉一起

    有一个名为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));
    }
    

    我提取了QuestionScoreStrategyDifficultyScoreStrategy 的策略选择逻辑,如下所示:

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

    优点

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

    缺点

    • 代码真的很重复
    • 您无法使用流畅的 API 创建回退链
    • 您需要使用第 3 方库

    没有波莉

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

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

    优点

    • 只有一个 return 声明
    • 后备链声明和评估是分开的
    • 简洁明了

    缺点

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

    【讨论】:

      【解决方案3】:

      您可以按优先级对ScoringMethods 的顺序进行排序。

      首先你按照str is QuestionScoreStrategystr.Question.Id == question.Id排序。

      然后你按str is DifficultyScoreStrategystr.Question.Difficulty == question.Difficulty排序。

      (请注意,由于false 位于true 之前,因此您必须反转条件)

      那你就可以FirstOrDefault() ?? defaultStrategy了。

      例子:

      var defaultStrategy = new() { Correct = 1, Incorrect = 0, Unattempted = 0 };
      
      var selectedStrategy = Strategies.OrderBy(str => 
          !(str is QuestionScoreStrategy questionStrat && questionStrat.Question.Id == question.Id)
      ).ThenBy(str =>
          !(str is DifficultyScoreStrategy difficultySrat && difficultySrat.Difficulty == question.Difficulty)
      ).FirstOrDefault() ?? defaultStrategy;
      

      您可以通过添加更多ThenBy 子句来轻松添加更多“级别”。

      【讨论】:

      • 不错的解决方案。但我有很多问题需要评估。在一个循环中,我首先获取集合中每个问题的策略,然后对其进行评估。对每个问题进行排序会不会很昂贵?
      • @AmalK 好吧,这种事情确实应该在数据库上完成,但我对 EF 的了解还不够,无法知道 lambda 是否可以转换为查询。但如果你真的关心性能,这在技术上可以一次性完成,但方式很丑陋。见my answer on this post。我在这里的回答实际上是受到厄立特里亚在那里的回答的启发。理想情况下,Min 将接受 Comparer 作为参数,您可以编写 Comparers,但它不会:(
      • 目前,我正在一次性从数据库中加载所有策略并将其传递给Evalaute 方法。我真的想知道是否有一种模式可以帮助解决这个问题。事实上,我注意到 .NET 框架中的很多东西都是这样实现的,配置使用默认值,同时仍然允许我们自定义它。我说的是实现convention over configuration。但也许这应该是一个单独的问题。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-11-06
      • 1970-01-01
      • 1970-01-01
      • 2013-06-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多