Tengo una clase ScoreStrategy que describe cómo calcular puntos para un cuestionario:
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; } } Tres propiedades Correct , Incorrect y Unattempted describen cuántos puntos se asignarán a una respuesta. Estos puntos también pueden ser negativos. La estrategia de puntuación se aplica a todas las preguntas de la prueba, por lo que solo puede haber una estrategia de ScoreStrategy por prueba. Tengo dos subclases:
public class DifficultyScoreStrategy : ScoreStrategy { public QuestionDifficulty Difficulty { get; set; } } public class QuestionScoreStrategy : ScoreStrategy { [Required] public Question Question { get; set; } } Mis preguntas tienen tres niveles de dificultad ( Easy , Medium , Hard ; la dificultad de la QuestionDifficulty es una enumeración). DifficultyScoreStrategy especifica si los puntos para las preguntas de una dificultad específica deben asignarse de manera diferente. Esto anula la ScoreStrategy base que se aplica a todo el cuestionario. Puede haber una instancia por nivel de dificultad.
En tercer lugar, tengo una clase QuestionScoreStrategy que especifica si los puntos para una pregunta específica deben otorgarse de manera diferente. Esto anula tanto la ScoreStrategy de toda la prueba como la DifficultyStrategy de toda la DifficultyStrategy . Puede haber una instancia por pregunta.
Mientras evalúo las respuestas del cuestionario, quiero implementar un mecanismo de respaldo nivel por nivel :
Para cada pregunta:
QuestionScoreStrategy para la pregunta y devuelva la estrategia si la encuentra.DifficultyScoreStrategy y verifique si hay una estrategia para el nivel de dificultad de la pregunta que se está evaluando y devuélvala si encuentra una estrategia.ScoreStrategy para todo el cuestionario y verifique si existe y devuélvalo si existe.ScoreStrategy , use el valor predeterminado como { Correct = 1, Incorrect = 0, Unattempted = 0 } (Sería genial si pudiera hacer esto configurable también, algo muy parecido a la manera elegante de .NET: options => { options.UseFallbackStrategy( correct: 1, incorrect: 0, unattempted: 0 ); }).
He resumido la información anterior en una tabla:
| Tipo de estrategia | Prioridad | Instancias máximas por prueba |
|---|---|---|
QuestionScoreStrategy | 1º (más alto) | Tantas como preguntas haya en el cuestionario |
DifficultyScoreStrategy | 2do | 4, uno para cada nivel de dificultad |
ScoreStrategy | 3ro | Sólo uno |
| estrategia alternativa (Predeterminado { Correct = 1, Incorrect = 0, Unattempted = 0} ) | 4to (más bajo) | Configurado para toda la aplicación. Compartido por todos los cuestionarios |
Tengo una clase contenedora llamada EvaluationStrategy que contiene estas estrategias de puntaje entre otra información de evaluación:
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; } } He agregado un método llamado GetStrategyByQuestion() a la misma clase EvaluationStrategy anterior (tenga en cuenta que se declara como partial ) que implementa este comportamiento alternativo y también un indexador complementario que a su vez llama a este método. He declarado dos HashSet s de tipos DifficultyScoreStrategy y QuestionScoreStrategy y un método Initialize() los instancia. Todas las estrategias de puntuación se cambian por tipo y se agregan al HashSet apropiado, solo puede haber una ScoreStrategy por prueba, que se almacenará en 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; } } Este método parece un poco torpe y no me parece correcto. Usar una clase parcial y agregar a EvalutationStrategy tampoco parece correcto. ¿Cómo implemento este comportamiento alternativo nivel por nivel? ¿Hay algún patrón/principio de diseño que pueda usar aquí? Sé muchas cosas en el marco de .NET de respaldo a las convenciones predeterminadas si no está configurado. necesito algo parecido ¿O alguien puede simplemente recomendar una solución más limpia y elegante con una mejor capacidad de mantenimiento?
NOTA/INFORMACIÓN ADICIONAL: ScoreStrategy y EvaluationStrategy para todos los cuestionarios se almacenan en una base de datos administrada por EF Core (.NET 5) con asignación de TPH:
modelBuilder.Entity<ScoreStrategy>() .ToTable("ScoreStrategy") .HasDiscriminator<int>("StrategyType") .HasValue<ScoreStrategy>(0) .HasValue<DifficultyScoreStrategy>(1) .HasValue<QuestionScoreStrategy>(2) ; modelBuilder.Entity<EvaluationStrategy>().ToTable("EvaluationStrategy"); Tengo una sola base DbSet<ScoreStrategy> ScoreStrategies y otra DbSet<EvaluationStrategy> EvaluationStrategies . Dado que EvaluationStrategy es una clase de EF Core, soy un poco escéptico acerca de agregarle lógica ( GetStrategyByQuestion() ) también.
Hay una biblioteca de terceros llamada Polly que define una política llamada Fallback .
Con este enfoque, puede definir fácilmente una cadena de respaldo como esta:
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)); } He extraído la lógica de selección de estrategia para QuestionScoreStrategy y DifficultyScoreStrategy de esta manera:
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 .Si no desea usar una biblioteca de terceros solo para definir y usar una cadena de respaldo, haga algo como esto:
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 .Puede ordenar la secuencia de ScoringMethods según su prioridad.
Primero ordena si str is QuestionScoreStrategy y str.Question.Id == question.Id .
Luego ordena si str is DifficultyScoreStrategy y str.Question.Difficulty == question.Difficulty .
(Tenga en cuenta que dado que false viene antes que true , tendrá que invertir las condiciones)
Entonces puedes hacer FirstOrDefault() ?? defaultStrategy .
Ejemplo:
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; Puede agregar fácilmente más "niveles" a esto agregando más cláusulas ThenBy .