dimanche 30 mai 2021

Implementing level by level fallback in C#

I have a class ScoreStrategy that describes how to calculate points for a quiz:

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

Three properties Correct, Incorrect and Unattempted describe how many points to be assigned for a response. These points can also be negative. The score strategy applies to all questions in the quiz, thus there can only be one ScoreStrategy per quiz. I have two subclasses:

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

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

My questions have three difficulty levels(Easy, Medium, Hard; QuestionDifficulty is an enum). The DifficultyScoreStrategy specifies if points for questions of a specific difficulty need to be assigned differently. This overrides the base ScoreStrategy that applies to the entire quiz. There can be one instance per difficulty level.

Thirdly, I have a QuestionScoreStrategy class that specifies if points for a specific question have to be awarded differently. This overrides both the quiz-wide ScoreStrategy and the difficulty-wide DifficultyStrategy. There can be one instance per question.

While evaluating the responses of the quiz, I want to implement a level-by-level fallback mechanism:

For each question: Check if there a QuestionScoreStrategy for the question, if not, fallback to DifficultyScoreStrategy and check if there is one for the difficulty level of the question, if not, fallback to the quiz-wide ScoreStrategy, if there is no ScoreStrategy either, use default as { Correct = 1, Incorrect = 0, Unattempted = 0 }(It would be great if I can make this configurable as well).

This is how I'm currently implementing response evaluation for each respondent. This method takes in all the questions in a quiz, all the responses of one respondent and all the strategies in an exam:

public List<Result> Evaluate(IEnumerable<Question> questions, IEnumerable<Response> responses, IEnumerable<ScoreStrategy> strategies)
{
     List<Result> results = new();
     // Convert responses to a dictionary with Question as key. Question overrides Equals and GetHashCode()
     var responseDict = responses.ToDictionary(r => r.Question);
     foreach (var question in questions)
     {
         Result result = new();
         ScoreStrategy strategy = {?}; // I need the right strategy to be selected here. 
 
         if (responseDict.TryGetValue(question, out Response response))
         {
            if (IsCorrect(response))   // IsCorrect() is another method that checks if a response is correct
            {
                result.Status = ResponseStatus.Correct;
                result.Score = stategy.Correct; 
            }
            else 
            {
                result.Status = ResponseStatus.Incorrect;
                result.Score = stategy.Incorrect;
            }
         }
         else
         {
             result.Status = ResponseStatus.Unattempted;
             result.Score = strategy.Unattempted;
         }
         results.Add(result);
     }
     return results;
}

NOTE: The strategies are stored in a database managed by EF Core(.NET 5) with TPH mapping:

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

I have a single base DbSet<ScoreStrategy> Strategies.

What I have tried:

Separate out the strategies by type at the beginning of the method:

ScoreStrategy defaultStrategy = null;
HashSet<DifficultyScoreStrategy> dStrategies = new();
HashSet<QuestionScoreStrategy> qStrategies = new();

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

Then do this for each question in the loop:

ScoreStrategy strategy = new() { Correct = 1, Incorrect = 0, Unattempted = 0 };

if (var q = qStrategies.FirstOrDefault(str => str.Question.Id == question.Id) != null)
{
    strategy = q;
}
else if (var d = dStrategies.FirstOrDefault(str => str.Question.Difficulty == question.Difficulty)
{
    strategy = d;
}
else if (defaultStrategy is not null)
{
   strategy = defaultStrategy;
}

This method is a bit clumsy and doesn't quite feel right to me. How do I implement this level-by-level fallback behavior? Is there a pattern/principle I can use here? Or can someone simply recommend a cleaner solution?

Aucun commentaire:

Enregistrer un commentaire