Come si crea una GUI per una classe polimorfica?


17

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 uguali: puoi scegliere tra più opzioni, casella di testo, corrispondenza e così via. Ognuno di questi tipi di domande deve memorizzare diversi tipi di dati e necessita di una GUI diversa sia per il creatore che per chi esegue il test.

Vorrei evitare due cose:

  1. Tipo controlli o tipo casting
  2. Tutto ciò che riguarda la GUI nel mio codice dati.

Nel mio tentativo iniziale, finisco con le seguenti classi:

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

Tuttavia, quando vado a visualizzare il test, finirei inevitabilmente con un codice come:

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

Sembra un problema davvero comune. Esiste un modello di progettazione che mi consente di avere domande polimorfiche evitando gli elementi sopra elencati? O il polimorfismo è l'idea sbagliata in primo luogo?


6
Non è una cattiva idea fare domande su cose con cui hai problemi, ma per me questa domanda tende ad essere troppo ampia / poco chiara e alla fine stai mettendo in discussione la domanda ...
kayess

1
In generale, cerco di evitare i controlli del tipo / il cast del tipo in quanto generalmente portano a un minor controllo in fase di compilazione e sostanzialmente "aggira" il polimorfismo piuttosto che usarlo. Non sono fondamentalmente contrario a loro, ma provo a cercare soluzioni senza di loro.
Nathan Merrill,

1
Quello che stai cercando è fondamentalmente un DSL per la descrizione di modelli semplici, non di un modello gerarchico di oggetti.
user1643723,

2
@NathanMerrill "Voglio assolutamente il polimofismo", non dovrebbe essere il contrario? Preferiresti raggiungere il tuo vero obiettivo o "usare il polimofismo"? IMO, il polimofismo è adatto per la creazione di API complesse e il comportamento di modellazione. È meno adatto per i dati di modellazione (che è quello che stai facendo attualmente).
user1643723,

1
@NathanMerrill "ogni timeblock esegue un'azione, o contiene altri timeblock e li esegue, o richiede il prompt dell'utente", - questa informazione è estremamente preziosa, suggerisco, di aggiungerla alla domanda.
user1643723,

Risposte:


15

È possibile utilizzare un modello 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.


2
Hmm .... questa non è un'opzione terribile, tuttavia l'interfaccia QuestionVisitor dovrebbe aggiungere un metodo ogni volta che c'è un diverso tipo di domanda, che non è super scalabile.
Nathan Merrill,

3
@NathanMerrill, non penso che cambi molto la tua scalabilità. Sì, devi implementare il nuovo metodo in ogni istanza di QuestionVisitor. Ma questo è il codice che dovrai scrivere in ogni caso per gestire la GUI per il nuovo tipo di domanda. Non penso che aggiunga davvero molto codice che altrimenti non dovresti correggere, ma trasforma il codice mancante in un errore di compilazione.
Winston Ewert,

4
Vero. Tuttavia, se mai volessi consentire a qualcuno di creare il proprio tipo di domanda + Renderer (cosa che non faccio), non penso che sarebbe possibile.
Nathan Merrill,

2
@NathanMerrill, è vero. Questo approccio presuppone che solo una base di codice stia definendo i tipi di domanda.
Winston Ewert,

4
@ WinstonEwert questo è un buon uso del modello visitatore. Ma la tua implementazione non è del tutto conforme al modello. Di solito i metodi nel visitatore non prendono il nome dai tipi, di solito hanno lo stesso nome e differiscono solo nei tipi di parametri (sovraccarico dei parametri); il nome comune è visit(il visitatore visita). Anche il metodo negli oggetti visitati viene solitamente chiamato accept(Visitor)(l'oggetto accetta un visitatore). Vedi oodesign.com/visitor-pattern.html
Viktor Seifert il

2

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

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


Questo sembra spostare il problema in XML, dove in primo luogo si perde tutta la digitazione rigorosa.
Nathan Merrill,

Non sono sicuro se stai dicendo che è una cosa buona o cattiva. Da un lato, stiamo spostando il problema. D'altra parte, sembra una partita fatta in paradiso.
BTownTKD,

2

Se ogni risposta può essere codificata come 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. Ciò consente di separare le domande, le risposte e la GUI ma 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)
        );            
    }
}

Casella di testo, corrispondenza e così via potrebbero avere progetti simili, tutti implementando l'interfaccia della domanda. La costruzione della stringa di risposta avviene nella vista. La stringa di risposta rappresenta lo stato del test. Dovrebbero essere archiviati man mano che lo studente avanza. Applicandoli alle domande è possibile visualizzare il test ed è lo stato in modo sia graduale sia non classificato.

Separando l'output in display()e displayGraded()la vista non deve essere scambiata e non è necessario eseguire la ramificazione sui parametri. Tuttavia, ogni visualizzazione è libera di riutilizzare quanta più logica di visualizzazione possibile durante la visualizzazione. Qualunque schema sia stato concepito per fare ciò, non è necessario infiltrarsi in questo codice.

Se, tuttavia, desideri avere un controllo più dinamico su come 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)
        );            
    }
}

Ciò ha lo svantaggio di richiedere viste che non intendono visualizzare score()o answerKeydipendere da esse quando non ne hanno bisogno. Ciò significa che non è necessario ricostruire le domande del test per ogni tipo di vista che si desidera utilizzare.


Quindi questo inserisce il codice della GUI nella domanda. Il tuo "display" e "displayGraded" sono rivelatori: per ogni tipo di "display", dovrei avere un'altra funzione.
Nathan Merrill,

Non del tutto, questo fa riferimento a una visione polimorfica. Potrebbe essere una GUI, una pagina web, un PDF, qualunque cosa. Questa è una porta di output che viene inviata a contenuto libero da layout.
candied_orange,

@NathanMerrill per favore nota modifica
candied_orange

La nuova interfaccia non funziona: stai inserendo "MultipleChoiceView" all'interno dell'interfaccia "Domanda". È possibile mettere lo spettatore al costruttore, ma il più delle volte non si sa (o cura), che sarà spettatore quando si effettua l'oggetto. (Ciò potrebbe essere risolto usando una funzione / fabbrica pigra ma la logica dietro l'iniezione in quella fabbrica potrebbe diventare confusa)
Nathan Merrill

@NathanMerrill Qualcosa, da qualche parte deve sapere dove deve essere visualizzato. L'unica cosa che fa il costruttore è farti decidere al momento della costruzione e poi dimenticartene. Se non vuoi decidere questo durante la costruzione, devi decidere in seguito e in qualche modo ricordare quella decisione fino a quando non chiami display. L'uso delle fabbriche in questi metodi non cambierebbe questi fatti. Nasconde solo come hai preso la decisione. Di solito non in senso positivo.
candied_orange,

1

Secondo me, se hai bisogno di una funzionalità così generica, diminuirei l'accoppiamento tra elementi nel codice. Vorrei provare a definire il tipo di domanda il più generico possibile e, successivamente, creerei diverse classi per gli oggetti renderer. Si prega di vedere gli esempi seguenti:

///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 il controllo del tipo implementando un semplice controllo sui dati all'interno dell'oggetto della domanda. Il codice seguente cerca di realizzare due cose: (i) evitare la verifica del tipo ed evitare la violazione del principio "L" (sostituzione di Liskov in SOLID) rimuovendo il sottotipo della classe Domanda; e (ii) rendere estensibile il codice, non modificando mai il codice di rendering di base riportato di seguito, semplicemente aggiungendo più implementazioni di QuestionView e le sue istanze all'array (questo è in realtà il principio "O" in SOLID - aperto per l'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));
    }
  }
}

Cosa succede quando MultipleChoiceQuestionView tenta di accedere al campo MultipleChoice.choices? Richiede un cast. Certo, se assumiamo questa domanda, il tipo è unico e il codice è sano, è un cast abbastanza sicuro, ma è ancora un cast: P
Nathan Merrill,

Se noti nel mio esempio, non esiste tale tipo MultipleChoice. C'è solo un tipo di domanda, che ho cercato di definire genericamente, con un elenco di informazioni (è possibile memorizzare più scelte in questo elenco, è possibile definirlo come si desidera). Pertanto, non esiste alcun cast, hai solo una domanda di tipo e più oggetti che controllano se possono eseguire il rendering di questa domanda, se l'oggetto lo supporta, puoi tranquillamente chiamare il metodo di rendering.
Emerson Cardoso,

Nel mio esempio, ho scelto di ridurre l'accoppiamento tra la tua GUI e le proprietà tipizzate con forza in una specifica classe di domande; invece sostituisco quelle proprietà con proprietà generiche, a cui la GUI dovrebbe accedere con una chiave di stringa o qualcos'altro (accoppiamento libero). Questo è un compromesso, forse questo accoppiamento libero non è desiderato nel tuo scenario.
Emerson Cardoso,

1

Una fabbrica dovrebbe essere in grado di farlo. La mappa sostituisce l'istruzione switch, necessaria solo per accoppiare la domanda (che non sa nulla della vista) con 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)
}

In questo modo la vista utilizza 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.


Se ti trovassi in un sistema in cui la memorizzazione nella cache della vista era importante (come in un gioco), la fabbrica potrebbe includere un Pool of the QuestionViews.
Xtros,

Questo sembra abbastanza simile alla risposta di Caleth: dovrai ancora lanciarti Questionin un MultipleChoiceQuestionquando crei ilMultipleChoiceView
Nathan Merrill

Almeno in C #, sono riuscito a farlo senza un cast. Nel metodo getView, quando crea l'istanza della vista (chiamando Activator.CreateInstance (questionViewType, domanda)), il secondo parametro di CreateInstance è il parametro inviato al costruttore. Il mio costruttore MultipleChoiceView accetta solo una domanda MultipleChoice. Forse sta semplicemente spostando il cast all'interno della funzione CreateInstance.
Xtros,

0

Non sono sicuro che questo valga come "evitare i controlli del tipo", a seconda di come ti senti riguardo alla 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()]());
}

Questo è fondamentalmente un controllo del tipo, ma passa da un ifcontrollo del tipo a un dictionarycontrollo del tipo. Ad esempio come Python utilizza i dizionari anziché le istruzioni switch. Detto questo, mi piace in questo modo più di un elenco di istruzioni if.
Nathan Merrill,

1
@NathanMerrill Sì. Java non ha un buon modo di mantenere in parallelo due gerarchie di classi. In c ++ raccomanderei un template <typename Q> struct question_traits;con specializzazioni appropriate
Caleth,

@Caleth, puoi accedere a tali informazioni in modo dinamico? Penso che dovresti per costruire il giusto tipo dato un'istanza.
Winston Ewert,

Inoltre, la fabbrica probabilmente ha bisogno che l'istanza della domanda gli sia passata. Ciò rende questo modello sfortunatamente disordinato, in quanto richiede in genere un brutto cast.
Winston Ewert,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.