16

Let's say I've got a test builder, so that teachers can create a bunch of questions for a test.

However, not all questions are the same: You have multiple choice, text box, matching, and so on. Each of these question types need to store different types of data, and need a different GUI for both the creator and for the test-taker.

I'd like to avoid two things:

  1. Type checks or type casting
  2. Anything related to the GUI in my data code.

In my initial attempt, I end up with the following classes:

class Test{
    List<Question> questions;
}
interface Question { }
class MultipleChoice implements Question {}
class TextBox implements Question {}

However, when I go to display the test, I'd inevitably end up with code like:

for (Question question: questions){
    if (question instanceof MultipleChoice){
        display.add(new MultipleChoiceViewer());
    } 
    //etc
}

This feels like a really common problem. Is there some design pattern that allows me to have polymorphic questions while avoiding the items listed above? Or is polymorphism the wrong idea in the first place?

  • 6
    It's not a bad idea to ask about things you have problems with, but to me this question tends to be too-broad/unclear and at last you are questioning the question... – kayess Aug 25 '17 at 12:40
  • @kayess I'm not attached to any particular method of programming (which is why I ask that last question). I'm looking for a way to join different sets of data to different GUIs in a clean way. I'm pretty sure that means polymorphism, but I'm willing to accept that I can't do it that way. – Nathan Merrill Aug 25 '17 at 12:43
  • 1
    In general, I try to avoid type checks/type casting as it generally leads to less compile-time checking and is basically "working around" the polymorphism rather than using it. I'm not fundamentally opposed to them, but try to look for solutions without them. – Nathan Merrill Aug 25 '17 at 16:54
  • 1
    What you are looking for is basically a DSL for describing simple templates, not hierarchical object model. – user1643723 Aug 26 '17 at 0:22
  • 1
    @NathanMerrill "I definitely want polymophism", — should not that be the other way around? Would you rather achieve your actual goal or "use polymophism"? IMO, polymophism is well suited for building complex APIs and modelling behaviour. It is less well-suited for modelling data (which is what you are currently doing). – user1643723 Aug 26 '17 at 4:20
15

You can use a visitor pattern:

interface QuestionVisitor {
    void multipleChoice(MultipleChoice);
    void textBox(TextBox);
    ...
}

interface Question {
    void visit(QuestionVisitor);
}

class MultipleChoice implements Question {

    void visit(QuestionVisitor visitor) {
        visitor.multipleChoice(this);
    }
}

Another option is a discriminated union. This will depend very much on your language. This is much better if your language supports it, but many popular languages don't.

  • 2
    Hmm....this isn't a terrible option, however the QuestionVisitor interface would need to add a method each time there's a different type of question, which isn't super scalable. – Nathan Merrill Aug 25 '17 at 12:59
  • 3
    @NathanMerrill, I don't think it actually changes your scalability much. Yes, you have to implement the new method in every instance of QuestionVisitor. But that's code you'll have to write in any case to handle the GUI for the new question type. I don't think it really adds much code you wouldn't otherwise have to right, but it turns missing code into a compile error. – Winston Ewert Aug 25 '17 at 13:09
  • 3
    True. However, if I ever wanted to allow somebody to make their own Question type + Renderer (which I don't), I don't think that would be possible. – Nathan Merrill Aug 25 '17 at 13:12
  • 1
    @NathanMerrill, that's true. This approach assumes that only one code base is defining the question types. – Winston Ewert Aug 25 '17 at 13:21
  • 4
    @WinstonEwert this is a good use of the visitor pattern. But your implementation is not quite according to the pattern. Usually the methods in the visitor are not named after the types, they usually have the same name and only differ in the types of the parameters (parameter overloading); the common name is visit (the visitor visits). Also the method in the objects being visited is usually called accept(Visitor) (the object accepts a visitor). See oodesign.com/visitor-pattern.html – Viktor Seifert Aug 25 '17 at 14:20
2

In C#/WPF (and, I imagine, in other UI-focused design languages), we have DataTemplates. By defining data templates, you create an association between one type of "data object" and a specialized "UI template" created specifically to display that object.

Once you provide instructions for the UI to load a specific kind of object, it will see if there are any data templates defined for the object.

  • This seems to be moving the problem to XML where you lose all strict typing in the first place. – Nathan Merrill Aug 26 '17 at 3:01
  • I'm not sure if you're saying that's a good thing or a bad thing. On one hand, we're moving the problem. On the other hand, it sounds like a match made in heaven. – BTownTKD Aug 26 '17 at 18:30
2

If every answer can be encoded as a string you can do this:

interface Question {
    int score(String answer);
    void display(String answer);
    void displayGraded(String answer);
}

Where the empty string signify's a question with no answer to it yet. This allows the questions, the answers, and the GUI to be separated yet allows for polymorphism.

class MultipleChoice implements Question {
    MultipleChoiceView mcv;
    String question;
    String answerKey;
    String[] choices;

    MultipleChoice(
            MultipleChoiceView mcv, 
            String question, 
            String answerKey, 
            String... choices
    ) {
        this.mcv = mcv;
        this.question = question;
        this.answerKey = answerKey;
        this.choices = choices;
    }

    int score(String answer) {
        return answer.equals(answerKey); //Or whatever scoring logic
    }

    void display(String answer) {
        mcv.display(question, choices, answer);            
    }

    void displayGraded(String answer) {
        mcv.displayGraded(
            question, 
            answerKey, 
            choices, 
            answer, 
            score(answer)
        );            
    }
}

Text box, matching, and so on could have similar designs, all implementing the question interface. The construction of the answer string happens in the view. The answer string's represent the state of the test. They should be stored as the student progresses. Applying them to the questions allows displaying the test and it's state in both a graded and non graded way.

By separating output into display() and displayGraded() the view doesn't need to be swapped out and no branching needs to be done on parameters. However, each view is free to reuse as much display logic as it can when displaying. Whatever scheme is devised to do that doesn't need to leak into this code.

If, however, you wish to have more dynamic control of how a question is displayed you can do this:

interface Question {
    int score(String answer);
    void display(MultipleChoiceView mcv, String answer);
}

and this

class MultipleChoice implements Question {
    String question;
    String answerKey;
    String[] choices;

    MultipleChoice(
            String question, 
            String answerKey, 
            String... choices
    ) {
        this.question = question;
        this.answerKey = answerKey;
        this.choices = choices;
    }

    int score(String answer) {
        return answer.equals(answerKey); //Or whatever scoring logic
    }

    void display(MultipleChoiceView mcv, String answer) {
        mcv.display(
            question, 
            answerKey, 
            choices, 
            answer, 
            score(answer)
        );            
    }
}

This does have the drawback that it requires views that don't intend to display the score() or answerKey to depend on them when they don't need them. But it means you don't have to rebuild the test questions for each type of view you wish to use.

  • So this puts GUI code in the Question. Your "display" and "displayGraded" is revealing: For every type of "display", I would have to have another function. – Nathan Merrill Aug 25 '17 at 14:14
  • Not quite, this puts a reference to a view that is polymorphic. It MIGHT be a GUI, a web page, a PDF, whatever. This is an output port being sent layout free content. – candied_orange Aug 25 '17 at 14:30
  • @NathanMerrill please note edit – candied_orange Aug 25 '17 at 17:14
  • The new interface does not work: You are putting "MultipleChoiceView" inside of the "Question" interface. You can put the viewer into the constructor, but most of the time you don't know (or care) which viewer will be when you make the object. (That could be resolved by using a lazy function/factory but the logic behind injecting into that factory could get messy) – Nathan Merrill Aug 25 '17 at 17:45
  • @NathanMerrill Something, somewhere has to know where this is meant to be displayed. The only thing the constructor does is let you decide this at construction time and then forget about it. If you don't want to decide this at construction then you must decide later and somehow remember that decision until you call display. Using factories in these methods wouldn't change these facts. It just hides how you made the decision. Usually not in a good way. – candied_orange Aug 25 '17 at 19:30
1

In my opinion, if you need such a generic feature, I'd decrease the coupling between stuff in the code. I would try to define the Question type the more generic as I can, and after that I would create different classes for the renderer objects. Please, see the examples below:

///Questions package

class Test {
  IList<Question> questions;
}

class Question {
  String Type;   //example; could be another type
  IList<QuestionInfo> Info;  //Simple array of key/value information
}

Then, for the rendering part, I removed the Type checking by implementing a simple check on the data within the question object. The code below tries to accomplish two things: (i) avoid the type checking and avoid violation of the "L" principle (Liskov substitution in SOLID) by removing Question class subtyping; and (ii) make the code extensible, by never changing the core rendering code below, just adding more QuestionView implementations and its instances to the array (this is actually the "O" principle in SOLID - open for extension and closed for modification).

///GUI package

interface QuestionView {
  Boolean SupportsQuestion(Question question);
  View CreateView(Question question);
}

class MultipleChoiceQuestionView : QuestionView {
  Boolean SupportsQuestion(Question question){
    return question.Type == "multiple_coice";
  }

  //...more implementation
}
class TextBoxQuestionView : QuestionView { ... }
//...more views

//Assuming you have an array of QuestionView pre-configured
//with all currently available types of questions
for (Question question : questions) {
  for (QuestionView view : questionViews) {
    if (view.SupportsQuestion(question)) {
        display.add(view.CreateView(question));
    }
  }
}
  • What happens when MultipleChoiceQuestionView tries to access the field MultipleChoice.choices? It requires a cast. Sure, if we assume that question.Type is unique and the code is sane, its a pretty safe cast, but it's still a cast :P – Nathan Merrill Aug 25 '17 at 14:08
  • If you note in my example, there is no such type MultipleChoice. There is only one type Question, which I tried to define generically, with a list of information (you could store multiple choices in this list, you can define it like you want). Therefore, there is no cast, you only have One type Question, and multiple objects that check if they can render this question, if the object supports it, then you can safely call the rendering method. – Emerson Cardoso Aug 25 '17 at 14:43
  • In my example, I chose to decrease coupling between your GUI and strong typed properties in specific Question classe; instead I replace those properties by generic properties, to which the GUI would need to access by a string key or something else (loose coupling). This is a tradeoff, perhaps this loose coupling is not desired in your scenario. – Emerson Cardoso Aug 25 '17 at 14:45
0

I'm not sure this counts as "avoiding type checks", depending on how you feel about reflection.

// Either statically associate or have a register(Class, Supplier) method
Dictionary<Class<? extends Question>, Supplier<? extends QuestionViewer>> 
viewerFactory = // MultipleChoice => MultipleChoiceViewer::new etc ...

// ... elsewhere

for (Question question: questions){
    display.add(viewerFactory[question.getClass()]());
}
  • This is basically a type check, but moving from a if type check to a dictionary type check. Like how Python uses dictionaries instead of switch statements. That said, I like this way more than a list of if statements. – Nathan Merrill Aug 25 '17 at 13:00
  • @NathanMerrill Yes. Java doesn't have a nice way of keeping two class hierarchies in parallel. In c++ I'd recommend a template <typename Q> struct question_traits; with appropriate specialisations – Caleth Aug 25 '17 at 13:05
  • @Caleth, can you access that information dynamically? I think you'd have to in order to construct the right type given an instance. – Winston Ewert Aug 25 '17 at 13:10
  • Also, the factory probably needs the question instance passed to it. That makes this pattern unfortunately messy, as it typically requires an ugly cast. – Winston Ewert Aug 25 '17 at 13:23
0

A factory should be able to do this. The map replaces the switch statement, which is needed solely to pair the Question (which knows nothing about the view) with the QuestionView.

interface QuestionView<T : Question>
{
    view();
}

class MultipleChoiceView implements QuestionView<MultipleChoiceQuestion>
{
    MultipleChoiceQuestion question;
    view();
}
...

class QuestionViewFactory
{
    Map<K : Question, V : QuestionView<K>> map;

    register<K : Question, V : QuestionView<K>>();
    getView(Question)
}

With this the view uses the specific type of Question that it is able to display, and the model remains disconnected from the view.

The factory could be populated via reflection or manually at the application start.

  • If you were in a system where caching the view was important (like a game), the factory could include a Pool of the QuestionViews. – Xtros Aug 25 '17 at 23:21
  • This seems pretty similar to Caleth's answer: You're still going to need to cast Question into a MultipleChoiceQuestion when you create the MultipleChoiceView – Nathan Merrill Aug 26 '17 at 2:51
  • In C# at least, I managed to do this without a cast. In the getView method, when it creates the view instance (by calling Activator.CreateInstance(questionViewType, question)), the second parameter of CreateInstance is the parameter sent to the constructor. My MultipleChoiceView constructor only accepts a MultipleChoiceQuestion. Perhaps it is just moving the cast to inside the CreateInstance function though. – Xtros Sep 12 '17 at 17:24

protected by gnat Aug 26 '17 at 12:41

Thank you for your interest in this question. Because it has attracted low-quality or spam answers that had to be removed, posting an answer now requires 10 reputation on this site (the association bonus does not count).

Would you like to answer one of these unanswered questions instead?

Not the answer you're looking for? Browse other questions tagged or ask your own question.