问题
I'm trying to find a tidy solution to a questionnaire problem. Let us say that I have a Questionnaire class which has a collection of Answers, e.g.
public class Questionnaire
{
public virtual ISet<Answer> Answers {get;set;}
}
Answers need to be of different types depending on the question, e.g. date of birth, marks out of ten, why do you think etc.
My first thought was something like this:
public class Question
{
public virtual QuestionType TypeOfQuestion {get;set;}
public virtual string PromptText {get;set;}
}
public class Answer
{
public virtual Question Question {get;set;}
}
public class DateTimeAnswer : Answer
{
public virtual DateTime Response {get;set;}
}
public class IntegerAnswer : Answer
{
public virtual int Response {get;set;}
}
// etc.
The obvious problem would be that from the questionnaire, there is no access to the Response property:
questionnaire.Answers[0].Response; // compile error
The same would apply to an interface. It would be nicer to use a generic interface, such as:
public interface IAnswer<T>
{
public virtual Question Question {get;set;}
public virtual T Response {get;set;}
}
public class DateTimeAnswer : IAnswer<DateTime> {}
The problem then comes in the Questionnaire class, as the type of IAnswer must be supplied:
public class Questionnaire
{
public virtual ISet<IAnswer<???>> Answers {get;set;}
}
Clearly I don't want to have many collections of IAnswer each with different types. I could use
ISet<IAnswer<dynamic>>
but then NHibernate wouldn't like it.
I realise a compromise is needed somewhere, but I'm not sure which is the prettiest. What would you do?
回答1:
I think subclassing Question as well as answer is a good plan - get rid of that QuestionType enum.
You can then have a MakeAnswer(string) abstract method on Question which encapsulates a lot of logic for you, and can do type conversion etc if necessary.
My solution to the generic problem on Rob's answer would be something like this:
public interface IAnswer
{
Question Question { get; }
void Respond(IAnswerFormatter formatter);
}
Where IAnswerFormatter looks something like this:
public interface IAnswerFormatter
{
void Format(string value);
void Format(DateTime value);
...
}
And DateTimeAnswer would look like this:
public class DateTimeAnswer : IAnswer
{
public DateTimeAnswer(Question question, DateTime value)
{
Question = question;
}
public Question Question { get; protected set; }
protected DateTime Response {get; set;}
public virtual void Respond(IAnswerFormatter formatter)
{
formatter.Write(Response);
}
}
And DateTimeQuestion might look like:
public class DateTimeQuestion : Question
{
public IAnswer MakeAnswer(string value)
{
// Ignoring error handling here
return new DateTimeAnswer(this, DateTime.Parse(value));
}
}
This way your answer can hold the value in their own way, and you NH mappings can specify how it looks in the database (I'd recommend mapping using Discriminators) your Question can be responsible for creating answers from generic responses.
回答2:
Interesting Problem..
My comments/thoughts:
- As Steve said - get rid of that nasty
QuestionTypeenum! - Remove the
ISet<T>- I don't think it adds any value..
I would be thinking along the lines of something like:
public class Questionnaire
{
public AnswerCollection Answers { get; set; }
}
public class AnswerCollection : Collection<Answer>
{
// Subclassed Collection<T> for Add/Remove Semantics etc.
}
public abstract class Answer : IAnswer<object>
{
public override object Response { get { // Base Impl. Here }; }
public abstract string CommonOperation()
{
// This is the key, the "common operation" - likely ToString?
// (for rendering the answer to the screen)
// Hollywood Principle - let the answers figure out how they
// are to be displayed...
}
}
public class DateTimeAnswer : Answer, IAnswer<DateTime>
{
public override DateTime Response { get { // Do Stuff }; }
public override string CommonOperation() { return "I can haz DateTime"; }
}
The idea being here, we need to get to the essence of what you are doing to ALL of the objects, which is likely just displaying the answer.. We add type safety by way of generics so we can be sure that we can't create new responses without a type parameter..
We can then be pretty sure that what is going in and coming out is confined to the types of answers that we implement. NHib should have no real problem dealing with this since it knows what it needs.
While it sucks we have the "object" version of the Answer coming back from the collection, that is the what the collection is, Answers.
Does this help? :)
回答3:
Does it really make sense to store the answers in a full on data model? What are you going to be doing with them?
The most likely candidate seems to be reporting in which case you may want to look into the CQRS (Command Query Responsibility Separation) style. You would instead have a QuestionnaireCompletedCommand which would contain a list of answers you would then persist in some way that a reports could be ran against them.
Data models are great when you have business logic (which you might) but if you don't have any business logic you are likely just unnecessarily complicating the solution. Speaking of complicating when I say look at CQRS I don't necessarily mean the event sourcing part. That is a huge complication that very few people need.
来源:https://stackoverflow.com/questions/3232363/questions-with-different-types-of-answer-in-nhibernate