Search code examples
c#entity-frameworkinheritancearchitectureabstract-class

How to force a subclass to have a specific subclass of superclass property?


I am working with EF to build an MVC application in C#. I want different types of exams to have different types of questions. Here are my abstract classes:

public abstract class Exam
{
    public int Id { get; set; }
    public string Description { set; get; }

    public abstract ICollection<Question> GetQuestions();
    public abstract void SetQuestions(ICollection<Question> questions);
}

public abstract class Question
{
    public int Id { get; set; }
    public string Description { set; get; }

    public abstract Exam getExam();
    public abstract void setExam(Exam exam);
}

Notice that instead of the typical public virtual ICollection<Question> in the Exam class declaration, I created an abstract setter and getter. So is the case for the Exam property in the Question class.

Here are my concrete Exam classes:

[Table("SingleExam")]
public class SingleExam : Exam
{
    public virtual ICollection<SingleQuestion> Questions { get; set; }

    public override ICollection<Question> GetQuestions() { return Questions as ICollection<Question>; }
    public override void SetQuestions(ICollection<Question> questions)
    {
        if (!(questions is ICollection<SingleQuestion>))
            throw new ArgumentException("You must set single questions.");

        Questions = questions as ICollection<SingleQuestion>;
    }
}

[Table("MultipleExam")]
public class MultipleExam : Exam
{
    public virtual ICollection<MultipleQuestion> Questions { get; set; }

    public override ICollection<Question> GetQuestions() { return Questions as ICollection<Question>; }
    public override void SetQuestions(ICollection<Question> questions)
    {
        if (!(questions is ICollection<MultipleQuestion>))
            throw new ArgumentException("You must set multiple questions.");

        Questions = questions as ICollection<MultipleQuestion>;
    }
}

...And my concrete Question classes:

[Table("SingleQuestion")]
public class SingleQuestion : Question
{
    public int ExamId { get; set; }
    public virtual SingleExam Exam { get; set; }

    public override Exam getExam() { return Exam; }
    public override void setExam(Exam exam)
    {
        if (!(exam is SingleExam))
            throw new ArgumentException("You must set a SingleExam");

        Exam = exam as SingleExam;
    }
}

[Table("MultipleQuestion")]
public class MultipleQuestion : Question
{
    public int ExamId { get; set; }
    public virtual MultipleExam Exam { get; set; }

    public override Exam getExam() { return Exam; }
    public override void setExam(Exam exam)
    {
        if (!(exam is MultipleExam))
            throw new ArgumentException("You must set a MultipleExam");

        Exam = exam as MultipleExam;
    }
}

I did all this because a MultipleExam should only have MultipleQuestions, and a SingleExam should only have SingleQuestions, the same way that MultipleQuestion should have a MultipleExam and Single question should have a SingleExam.

Is there a better way to ensure that a subclass of a class 'A' contains or has a specific subclass of class 'B' (As is the case with my Exams and Questions), and have access to it through the abstract class without the abstract getters and setters?


Solution

  • As other have mentioned I think you are over complicating your problem. However; your question is about type guarantees and I will try to answer that.

    First the code:

    public interface IExam<out T> where T:IQuestion {
      int Id { get; set; }
      string Description { set; get; }
      IEnumerable<T> GetQuestions();
    }
    
    public interface IQuestion{
      int Id { get; set; }
      string Description { set; get; }
      IExam<IQuestion> Exam { get; }
    }
    
    public class SingleQuestion:IQuestion {
      public string Description { get; set; }
      public int Id { get; set; }
      IExam<IQuestion> IQuestion.Exam {
        get { return Exam; }
      }
      public SingleExam Exam { get; set; }
    }
    
    public class SingleExam:IExam<SingleQuestion> {
      public int Id { get; set; }
      public string Description { get; set; }
    
      private IEnumerable<SingleQuestion> _questions;
      public IEnumerable<SingleQuestion> GetQuestions() {
        return _questions;
      }
    
      public void SetQuestions(IEnumerable<SingleQuestion> questions) {
        _questions = questions;
      }
    }
    

    First of all we have replaced the abstract classes with interfaces. This is required because we want to make IExam covariant on IQuestion and covariance can only be defined in an interface. This is also why we change to an IEnumerable for the collection.

    Note we do not define the SetQuestions method in IExam in short this is because we can't. In long it is because that would make T contravarient as well as contravarient which would in turn lead to circumstances where type guarantees could not be made.

    IQuestions is fairly straight forward no real changes here. You could, I suppose, leave it as an abstract type though.

    Now the implementations: In SingleQuestion we must explicitly implement Exam which expects an IExam then shadow it with a property that returns a SingleExam. This allows us to return the most exact type of exam possible.

    SingleQuestion sq = new SingleQuestion();
    IQuestion q = sq; //Upcast
    sq.Exam; //returns a SingleExam
    q.Exam; //returns a IExam<IQuestion>
    

    In SingleExam you can now set the questions and restrict it so that only SingleQuestions may be added.

    As an aside it is now easier to see why SetQuestions cannot be defined in IExam. Consider the following:

    SingleExam se = new SingleExam();
    IExam<IQuestion> singleUpcast = se;
    //What type of question can we set on singleUpcast?
    

    All we know is that singleUpcast contains IQuestions but we can't just add IQuestions because singleUpcast is ultimately an instance of SingleExam which promised that only SingleQuestions could be set so it. In short it is not possible to know what types can be added to IExam without potentially breaking type guarantees