Come si crea una GUI per una classe polimorfica?

15

Diciamo che ho un costruttore di test, in modo che gli insegnanti possano creare una serie di domande per un test.

Tuttavia, non tutte le domande sono le stesse: hai scelta multipla, casella di testo, corrispondenza e così via. Ognuno di questi tipi di domande deve memorizzare diversi tipi di dati e ha bisogno di una GUI diversa sia per il creatore che per il fruitore del test.

Vorrei evitare due cose:

  1. Digita i controlli o digita
  2. Qualunque cosa relativa alla GUI nel mio codice dati.

Nel mio primo tentativo, finisco con le seguenti classi:

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

Tuttavia, quando vado a mostrare il test, finirei inevitabilmente con un codice del tipo:

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

Questo sembra un problema molto comune. C'è qualche schema di progettazione che mi permette di avere domande polimorfiche evitando gli articoli sopra elencati? O il polimorfismo è un'idea sbagliata in primo luogo?

    
posta Nathan Merrill 25.08.2017 - 14:33
fonte

6 risposte

15

Puoi utilizzare un modello di visitatore:

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

interface Question {
    void visit(QuestionVisitor);
}

class MultipleChoice implements Question {

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

Un'altra opzione è un'unione discriminata. Questo dipenderà molto dalla tua lingua. Questo è molto meglio se la tua lingua lo supporta, ma molte lingue popolari no.

    
risposta data 25.08.2017 - 14:55
fonte
2

In C # / WPF (e, immagino, in altri linguaggi di progettazione incentrati sull'interfaccia utente), abbiamo DataTemplates . Definendo modelli di dati, si crea un'associazione tra un tipo di "oggetto dati" e un "modello di interfaccia utente" specializzato creato appositamente per visualizzare tale oggetto.

Una volta fornite istruzioni per l'interfaccia utente per caricare un tipo specifico di oggetto, verrà visualizzato se sono definiti modelli di dati per l'oggetto.

    
risposta data 25.08.2017 - 19:56
fonte
2

Se ogni risposta può essere codificata come una stringa puoi farlo:

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

Dove la stringa vuota indica una domanda senza risposta ancora. Ciò consente di separare le domande, le risposte e la GUI e consente il polimorfismo.

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)
        );            
    }
}

La casella di testo, la corrispondenza e così via potrebbero avere disegni simili, tutti implementati nell'interfaccia della domanda. La costruzione della stringa di risposta avviene nella vista. La stringa di risposta rappresenta lo stato del test. Dovrebbero essere archiviati mentre lo studente progredisce. Applicandoli alle domande è possibile visualizzare il test e il suo stato sia in modo graduale che non classificato.

Separando l'output in display() e displayGraded() la vista non ha bisogno di essere scambiata e nessuna ramificazione deve essere eseguita sui parametri. Tuttavia, ciascuna vista è libera di riutilizzare la logica di visualizzazione quanto più possibile durante la visualizzazione. Qualunque schema sia concepito per farlo non ha bisogno di filtrare in questo codice.

Se, tuttavia, desideri avere un controllo più dinamico sul modo in cui viene visualizzata una domanda, puoi farlo:

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

e questo

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)
        );            
    }
}

Questo ha lo svantaggio di richiedere viste che non intendono visualizzare score() o answerKey per dipendere da esse quando non ne hanno bisogno. Ma significa che non devi ricostruire le domande del test per ogni tipo di vista che desideri utilizzare.

    
risposta data 25.08.2017 - 15:28
fonte
1

Secondo me, se hai bisogno di una funzione così generica, diminuirei l'accoppiamento tra le cose nel codice. Proverò a definire il tipo di domanda il più generico possibile, dopodiché creerò diverse classi per gli oggetti del renderer. Per favore, vedi gli esempi qui sotto:

///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
}

Quindi, per la parte di rendering, ho rimosso la verifica del tipo implementando un semplice controllo sui dati all'interno dell'oggetto domanda. Il codice seguente tenta di realizzare due cose: (i) evitare il controllo del tipo ed evitare la violazione del principio "L" (sostituzione di Liskov in SOLID) rimuovendo la sottotipizzazione della classe Question; e (ii) rendere il codice estensibile, non cambiando mai il codice di rendering di base qui sotto, aggiungendo semplicemente più implementazioni di QuestionView e le sue istanze all'array (questo è in realtà il principio "O" in SOLID - aperto per estensione e chiuso per modifica).

///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));
    }
  }
}
    
risposta data 25.08.2017 - 16:01
fonte
0

Non sono sicuro che ciò contenga "evitare i controlli di tipo", a seconda di come ti senti a riflessione .

// 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()]());
}
    
risposta data 25.08.2017 - 14:56
fonte
0

Una fabbrica dovrebbe essere in grado di farlo. La mappa sostituisce l'istruzione switch, che è necessaria esclusivamente per accoppiare la Domanda (che non conosce nulla sulla vista) con il 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)
}

Con questo la vista usa il tipo specifico di Domanda che è in grado di visualizzare, e il modello rimane disconnesso dalla vista.

La fabbrica può essere popolata tramite riflessione o manualmente all'avvio dell'applicazione.

    
risposta data 26.08.2017 - 01:19
fonte

Leggi altre domande sui tag