逐级实施回退
Implementing level by level fallback
我有一个 class 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; }
}
三个属性 Correct
、Incorrect
和 Unattempted
描述了为响应分配多少点。这些点也可以是负面的。评分策略适用于测验中的所有问题,因此每个测验只能有一个ScoreStrategy
。
我有两个子classes:
public class DifficultyScoreStrategy : ScoreStrategy
{
public QuestionDifficulty Difficulty { get; set; }
}
public class QuestionScoreStrategy : ScoreStrategy
{
[Required]
public Question Question { get; set; }
}
我的问题有三个难度级别(Easy
、Medium
、Hard
;QuestionDifficulty
是一个枚举)。 DifficultyScoreStrategy
指定是否需要为特定难度的问题分配不同的分数。这将覆盖适用于整个测验的基础 ScoreStrategy
。每个难度级别可以有一个实例。
第三,我有一个 QuestionScoreStrategy
class 指定特定问题的分数是否必须以不同方式授予。这会覆盖测验范围 ScoreStrategy
和难度范围 DifficultyStrategy
。每个问题可以有一个实例。
在评估测验的回答时,我想实施逐级回退机制:
每个问题:
- 检查问题是否有
QuestionScoreStrategy
,如果找到则 return 策略。
- 如果没有,回退到
DifficultyScoreStrategy
并检查是否有针对正在评估的问题的难度级别的策略
并且 return 如果找到了一个策略,它就会出现。
- 如果不存在,则回退到测验范围
ScoreStrategy
并检查是否存在,return 如果存在,
- 如果也没有
ScoreStrategy
,则使用默认值 { Correct = 1, Incorrect = 0, Unattempted = 0 }
(如果我也可以将其设置为可配置的就好了,就像 .NET 的优雅方式一样:
options => {
options.UseFallbackStrategy(
correct: 1,
incorrect: 0,
unattempted: 0
);
}
).
总结
我已经在 table 中总结了以上信息:
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
的容器 class,其中包含这些评分策略以及其他评估信息:
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
class 中添加了一个名为 GetStrategyByQuestion()
的方法(注意它被声明为 partial
),它实现了这个回退行为,也是一个依次调用此方法的伴随索引器。我已经声明了两个 HashSet
类型 DifficultyScoreStrategy
和 QuestionScoreStrategy
并且 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;
}
}
这个方法似乎有点笨拙,我觉得不太合适。使用部分 class 并添加到 EvalutationStrategy
似乎也不正确。如何实现这种逐级回退行为?有没有设计pattern/principle我可以在这里使用?如果未配置,我知道 .NET Framework 中的很多事情都会回退到默认约定。我需要类似的东西。或者有人可以简单地推荐一个更清洁、更优雅、可维护性更好的解决方案吗?
NOTE/ADDITIONAL 信息: 所有测验的 ScoreStrategy
和 EvaluationStrategy
都存储在由带有 TPH 映射的 EF Core(.NET 5):
modelBuilder.Entity<ScoreStrategy>()
.ToTable("ScoreStrategy")
.HasDiscriminator<int>("StrategyType")
.HasValue<ScoreStrategy>(0)
.HasValue<DifficultyScoreStrategy>(1)
.HasValue<QuestionScoreStrategy>(2)
;
modelBuilder.Entity<EvaluationStrategy>().ToTable("EvaluationStrategy");
我有一个碱基 DbSet<ScoreStrategy> ScoreStrategies
和另一个 DbSet<EvaluationStrategy> EvaluationStrategies
。由于 EvaluationStrategy
是一个 EF Core class,我对向其添加逻辑 (GetStrategyByQuestion()
) 也有点怀疑。
您可以根据优先级对 ScoringMethods
的顺序进行排序。
首先你按 str is QuestionScoreStrategy
和 str.Question.Id == question.Id
排序。
然后你按str is DifficultyScoreStrategy
和str.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
子句轻松地为此添加更多“级别”。
与波莉
有一个名为 Polly which defines a policy called Fallback 的第 3 方库。
使用这种方法,您可以像这样轻松定义后备链:
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));
}
我已经提取了 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);
优点
- 有一个
return
语句
- 策略声明与链接分开
- 每个回退都可以由不同的条件触发
- 主要选择逻辑与备用逻辑分离
缺点
- 代码真的很重复
- 您不能使用流利的 API
创建回退链
- 您需要使用第 3 方库
没有波莉
如果您不想使用第 3 方库来定义和使用回退链,您可以这样做:
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
语句
- 回退链的声明和求值是分开的
- 简洁明了
缺点
- 不太灵活:每个回退选择都由相同的条件触发
- 主要选择不与后备分开
我想象所有数据(问题、策略、测验都存储在数据库中)。然后我会期待这样的方式来获得每个策略:
题攻略
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)
基于此以及您已经提供的内容,策略只有三个数字:正确答案得分,错误和未尝试答案得分。
因此,这成为抽象 class 的完美候选者,它将为三个实体的基础 class 服务 - 三种类型的策略 - 这将是三个表,因为每个表都有不同的关系:
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;
}
然后在manager中写获取策略逻辑:
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),
}
这样可以非常容易地以任何方式配置回退策略。当然,还有一些注意事项:
- 确保所有回退策略都是唯一的,例如高、中、低策略不可能相同,
- 数据库上下文只能通过存储库模式访问
- 添加更多完整性检查(如空值等)
我省略了那些部分,因为我专注于纯粹的功能。
我有一个 class 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; }
}
三个属性 Correct
、Incorrect
和 Unattempted
描述了为响应分配多少点。这些点也可以是负面的。评分策略适用于测验中的所有问题,因此每个测验只能有一个ScoreStrategy
。
我有两个子classes:
public class DifficultyScoreStrategy : ScoreStrategy
{
public QuestionDifficulty Difficulty { get; set; }
}
public class QuestionScoreStrategy : ScoreStrategy
{
[Required]
public Question Question { get; set; }
}
我的问题有三个难度级别(Easy
、Medium
、Hard
;QuestionDifficulty
是一个枚举)。 DifficultyScoreStrategy
指定是否需要为特定难度的问题分配不同的分数。这将覆盖适用于整个测验的基础 ScoreStrategy
。每个难度级别可以有一个实例。
第三,我有一个 QuestionScoreStrategy
class 指定特定问题的分数是否必须以不同方式授予。这会覆盖测验范围 ScoreStrategy
和难度范围 DifficultyStrategy
。每个问题可以有一个实例。
在评估测验的回答时,我想实施逐级回退机制:
每个问题:
- 检查问题是否有
QuestionScoreStrategy
,如果找到则 return 策略。 - 如果没有,回退到
DifficultyScoreStrategy
并检查是否有针对正在评估的问题的难度级别的策略 并且 return 如果找到了一个策略,它就会出现。 - 如果不存在,则回退到测验范围
ScoreStrategy
并检查是否存在,return 如果存在, - 如果也没有
ScoreStrategy
,则使用默认值{ Correct = 1, Incorrect = 0, Unattempted = 0 }
(如果我也可以将其设置为可配置的就好了,就像 .NET 的优雅方式一样:
options => {
options.UseFallbackStrategy(
correct: 1,
incorrect: 0,
unattempted: 0
);
}
).
总结
我已经在 table 中总结了以上信息:
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
的容器 class,其中包含这些评分策略以及其他评估信息:
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
class 中添加了一个名为 GetStrategyByQuestion()
的方法(注意它被声明为 partial
),它实现了这个回退行为,也是一个依次调用此方法的伴随索引器。我已经声明了两个 HashSet
类型 DifficultyScoreStrategy
和 QuestionScoreStrategy
并且 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;
}
}
这个方法似乎有点笨拙,我觉得不太合适。使用部分 class 并添加到 EvalutationStrategy
似乎也不正确。如何实现这种逐级回退行为?有没有设计pattern/principle我可以在这里使用?如果未配置,我知道 .NET Framework 中的很多事情都会回退到默认约定。我需要类似的东西。或者有人可以简单地推荐一个更清洁、更优雅、可维护性更好的解决方案吗?
NOTE/ADDITIONAL 信息: 所有测验的 ScoreStrategy
和 EvaluationStrategy
都存储在由带有 TPH 映射的 EF Core(.NET 5):
modelBuilder.Entity<ScoreStrategy>()
.ToTable("ScoreStrategy")
.HasDiscriminator<int>("StrategyType")
.HasValue<ScoreStrategy>(0)
.HasValue<DifficultyScoreStrategy>(1)
.HasValue<QuestionScoreStrategy>(2)
;
modelBuilder.Entity<EvaluationStrategy>().ToTable("EvaluationStrategy");
我有一个碱基 DbSet<ScoreStrategy> ScoreStrategies
和另一个 DbSet<EvaluationStrategy> EvaluationStrategies
。由于 EvaluationStrategy
是一个 EF Core class,我对向其添加逻辑 (GetStrategyByQuestion()
) 也有点怀疑。
您可以根据优先级对 ScoringMethods
的顺序进行排序。
首先你按 str is QuestionScoreStrategy
和 str.Question.Id == question.Id
排序。
然后你按str is DifficultyScoreStrategy
和str.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
子句轻松地为此添加更多“级别”。
与波莉
有一个名为 Polly which defines a policy called Fallback 的第 3 方库。
使用这种方法,您可以像这样轻松定义后备链:
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));
}
我已经提取了 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);
优点
- 有一个
return
语句 - 策略声明与链接分开
- 每个回退都可以由不同的条件触发
- 主要选择逻辑与备用逻辑分离
缺点
- 代码真的很重复
- 您不能使用流利的 API 创建回退链
- 您需要使用第 3 方库
没有波莉
如果您不想使用第 3 方库来定义和使用回退链,您可以这样做:
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
语句 - 回退链的声明和求值是分开的
- 简洁明了
缺点
- 不太灵活:每个回退选择都由相同的条件触发
- 主要选择不与后备分开
我想象所有数据(问题、策略、测验都存储在数据库中)。然后我会期待这样的方式来获得每个策略:
题攻略
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)
基于此以及您已经提供的内容,策略只有三个数字:正确答案得分,错误和未尝试答案得分。
因此,这成为抽象 class 的完美候选者,它将为三个实体的基础 class 服务 - 三种类型的策略 - 这将是三个表,因为每个表都有不同的关系:
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;
}
然后在manager中写获取策略逻辑:
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),
}
这样可以非常容易地以任何方式配置回退策略。当然,还有一些注意事项:
- 确保所有回退策略都是唯一的,例如高、中、低策略不可能相同,
- 数据库上下文只能通过存储库模式访问
- 添加更多完整性检查(如空值等)
我省略了那些部分,因为我专注于纯粹的功能。