dimanche 21 novembre 2021

Selecting a corresponding strategy based on the subtype of the instance

I have an interface IResponse that represents a response to a Question and a few concrete response types that implement IResponse:

interface IResponse<out TQuestion> where TQuestion: Question
{
     TQuestion Question { get; }
     Candidate Candidate { get; }
     string Answer { get; }
}

class MultipleChoiceResponse : IResponse<MultipleChoiceQuestion>
{
    public MultipleChoiceQuestion Question { get; }
    public Candidate Candidate { get; }
    public string Answer { get; }
}

class TextResponse : IResponse<TextQuestion>
{
    public TextQuestion Question { get; }
    public Candidate Candidate { get; }
    public string Answer { get; }
}
...

abstract class Question {..}
class MultipleChoiceQuestion : Question {..}
class TextQuestion : Question {..}
...

For each response type, I have a corresponding response evaluator that implements IResponseEvaluator:

interface IResponseEvaluator<TQuestion, TResponse> 
    where TQuestion : Question
    where TResponse : IResponse<TQuestion>
{
    decimal Evaluate(TResponse response);
}

class DefaultMultipleChoiceResponseEvaluator : IResponseEvaluator<MultipleChoiceQuestion, MultipleChoiceResponse>
{
    public decimal Evaluate(MultipleChoiceResponse response)
    {
        ...
    }
}

class DefaultTextResponseEvaluator : IResponseEvaluator<TextQuestion, TextResponse>
{
    public decimal Evaluate(TextResponse response)
    {
        ...
    }
}
...

Finally, I have an EvaluationAggregator with a method EvaluateAll that provides a common interface to accept a collection of all subtypes of IResponse using IEnumerable<IResponse<Question>>:

class EvaluationAggregator
{
    public IResponseEvaluator<MultipleChoiceQuestion, MultipleChoiceResponse> ChoiceEvaluator { get; set; }   

    public IResponseEvaluator<TextQuestion, TextResponse> TextEvaluator { get; set; }

    ...
    
    public decimal EvaluateAll(IEnumerable<IResponse<Question>> responses)
    {
         decimal totalScore = 0;
         foreach (var response in responses)
         {
              decimal score = response switch
              {
                  MultipleChoiceResponse mcr => ChoiceEvaluator.Evaluate(mcr),
                  TextResponse tr => TextEvaluator.Evaluate(tr),
                  ...
              };
              totalScore += score;
         }    
         return totalScore;
    }
}

Since, TQuestion in IResponse<TQuestion> is covariant(marked with out), I can pass responses to all question subtypes as a single IEnumerable to EvaluateAll. However, to select the right evaluator for the type , I'm still having to switch on the type of the response. If I add more response types in the future, the code will have to be modified. Is there a design pattern or a cleaner way to do this?

This is how I would call it:

IEnumerable<IResponse<Question>> responses = FetchAllTypesOfResponses();

var aggregator = new EvaluationAggregator 
{
    ChoiceEvaluator = new DefaultMultipleChoiceResponseEvaluator(),
    TextEvaluator = new DefaultMultipleChoiceResponseEvaluator()
};

decimal totalScore = aggregator.EvaluateAll(responses);

Aucun commentaire:

Enregistrer un commentaire