views:

121

answers:

3

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?

+4  A: 

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.

spmason
+1  A: 

Interesting Problem..

My comments/thoughts:

  • As Steve said - get rid of that nasty QuestionType enum!
  • 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? :)

Rob Cooper
I agree on the general structure, but I think you've exposed a problem with having IAnswer as a generic class.In your example DateTimeAnswer isn't an Answer so couldn't go into an AnswersCollection. You'd have to have it inheriting from Answer as well, and implement the object version of the Response property as well as your strongly-typed one.As from the Question you would only ever be iterating through Answers the strongly-typed DateTime Response seems like a waste.
spmason
It does help, thanks. I'll give it some thought and try it out.
harriyott
Ah, Steve - good spot, the Answer should subclass Answer and implement the interface. I will correct. The strong typing is more for peace-of-mind on creation, you can't create the "object" version since it is abstract.. It's the classic problem of "would like the strong typing, but actually only *really* need it 10% of the time"..
Rob Cooper
A: 

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.

ShaneC