Como você cria uma GUI para uma classe polimórfica?

15

Digamos que eu tenha um criador de testes, para que os professores possam criar um monte de perguntas para um teste.

No entanto, nem todas as perguntas são as mesmas: você tem várias opções, caixa de texto, correspondência e assim por diante. Cada um desses tipos de perguntas precisa armazenar diferentes tipos de dados e precisa de uma interface gráfica diferente para o criador e para o candidato.

Eu gostaria de evitar duas coisas:

  1. Digite as verificações ou digite a transmissão
  2. Qualquer coisa relacionada à GUI no meu código de dados.

Na minha tentativa inicial, acabo com as seguintes classes:

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

No entanto, quando vou exibir o teste, inevitavelmente acabo com código como:

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

Isso parece um problema muito comum. Existe algum padrão de design que me permita ter perguntas polimórficas, evitando os itens listados acima? Ou é polimorfismo a ideia errada em primeiro lugar?

    
por Nathan Merrill 25.08.2017 / 14:33
fonte

6 respostas

15

Você pode usar um padrão de visitante:

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

interface Question {
    void visit(QuestionVisitor);
}

class MultipleChoice implements Question {

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

Outra opção é uma união discriminada. Isso dependerá muito do seu idioma. Isso é muito melhor se o seu idioma suportar, mas muitas linguagens populares não o fazem.

    
por 25.08.2017 / 14:55
fonte
2

Em C # / WPF (e, imagino, em outras linguagens de design focadas na interface do usuário), temos DataTemplates . Ao definir modelos de dados, você cria uma associação entre um tipo de "objeto de dados" e um "modelo de interface do usuário" especializado criado especificamente para exibir esse objeto.

Depois de fornecer instruções para a interface do usuário carregar um tipo específico de objeto, ele verá se há algum modelo de dados definido para o objeto.

    
por 25.08.2017 / 19:56
fonte
2

Se cada resposta puder ser codificada como uma string, você poderá fazer isso:

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

Onde a string vazia significa uma pergunta sem resposta ainda. Isso permite que as perguntas, as respostas e a GUI sejam separadas, mas permite o 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)
        );            
    }
}

Caixa de texto, correspondência e assim por diante podem ter designs semelhantes, todos implementando a interface da pergunta. A construção da string de resposta acontece na exibição. A string de resposta representa o estado do teste. Eles devem ser armazenados conforme o aluno progride. Aplicá-los às perguntas permite exibir o teste e seu estado de maneira gradual e sem classificação.

Ao separar a saída em display() e displayGraded() , a visualização não precisa ser trocada e nenhuma ramificação precisa ser feita nos parâmetros. No entanto, cada exibição é livre para reutilizar o máximo de lógica de exibição possível durante a exibição. Seja qual for o esquema planejado para fazer isso, não precisa vazar para esse código.

Se, no entanto, você deseja ter mais controle dinâmico de como uma pergunta é exibida, você pode fazer isso:

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

e isso

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

Isso tem a desvantagem de exigir exibições que não pretendem exibir score() ou answerKey para depender delas quando não precisarem delas. Mas isso significa que você não precisa recriar as perguntas do teste para cada tipo de visualização que deseja usar.

    
por 25.08.2017 / 15:28
fonte
1

Na minha opinião, se você precisar de um recurso tão genérico, eu diminuiria o acoplamento entre as coisas no código. Eu tentaria definir o tipo de pergunta o mais genérico possível, e depois disso eu criaria classes diferentes para os objetos renderizadores. Por favor, veja os exemplos abaixo:

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

Em seguida, para a parte de renderização, removi a verificação de tipo implementando uma verificação simples dos dados no objeto da pergunta. O código abaixo tenta realizar duas coisas: (i) evitar a verificação de tipo e evitar a violação do princípio "L" (substituição de Liskov no SOLID) removendo a subtipagem de classe de pergunta; e (ii) tornar o código extensível, nunca alterando o código de renderização principal abaixo, apenas adicionando mais implementações do QuestionView e suas instâncias ao array (na verdade é o princípio "O" no SOLID - aberto para extensão e fechado para modificação).

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

Não sei se isso é uma questão de "evitar verificações de tipo", dependendo de como você se sente sobre reflexão .

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

Uma fábrica deve ser capaz de fazer isso. O mapa substitui a instrução switch, que é necessária apenas para emparelhar a pergunta (que não sabe nada sobre a visão) com o 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)
}

Com isso, a visualização usa o tipo específico de pergunta que é capaz de exibir e o modelo permanece desconectado da exibição.

A fábrica pode ser preenchida por reflexão ou manualmente no início da aplicação.

    
por 26.08.2017 / 01:19
fonte