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