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


17

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

No entanto, nem todas as perguntas são iguais: 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 uma GUI diferente para o criador e o responsável pelo teste.

Eu gostaria de evitar duas coisas:

  1. Verificações de tipo ou conversão de tipo
  2. Qualquer coisa relacionada à GUI no meu código de dados.

Na minha tentativa inicial, termino 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 o polimorfismo é a idéia errada em primeiro lugar?


6
Não é uma má idéia para perguntar sobre coisas que você tem problemas com, mas para mim esta questão tende a ser muito ampla / claro e, finalmente, você está questionando a questão ...
kayess

1
Em geral, tento evitar verificações de tipo / conversão de tipo, pois geralmente leva a menos tempo de compilação e é basicamente "contornando" o polimorfismo em vez de usá-lo. Não sou fundamentalmente contra eles, mas tente procurar soluções sem eles.
Nathan Merrill

1
O que você está procurando é basicamente uma DSL para descrever modelos simples, não modelo de objeto hierárquico.
user1643723

2
@ NathanMerrill "Eu definitivamente quero polimofismo", - não deveria ser o contrário? Você prefere atingir seu objetivo real ou "usar o polimofismo"? Na IMO, o polimofismo é adequado para criar APIs complexas e modelar o comportamento. É menos adequado para modelar dados (que é o que você está fazendo no momento).
user1643723

1
@NathanMerrill "cada timeblock executa uma ação, ou contém outros timelocks e os executa, ou solicita o prompt do usuário", - essas informações são altamente valiosas, sugiro, que você as adicione à pergunta.
user1643723

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 vai depender muito do seu idioma. Isso é muito melhor se o seu idioma suportar, mas muitos idiomas populares não.


2
Hmm ... essa não é uma opção terrível, no entanto, a interface do QuestionVisitor precisaria adicionar um método cada vez que houver um tipo diferente de pergunta, que não é super escalável.
Nathan Merrill

3
@ NathanMerrill, não acho que isso mude muito sua escalabilidade. Sim, você precisa implementar o novo método em todas as instâncias do QuestionVisitor. Mas esse é o código que você precisará escrever em qualquer caso para manipular a GUI para o novo tipo de pergunta. Eu não acho que realmente adicione muito código que você não precisaria corrigir, mas transforma o código ausente em um erro de compilação.
Winston Ewert 25/08

4
Verdade. No entanto, se eu quisesse permitir que alguém fizesse seu próprio tipo de pergunta + renderizador (o que não faço), acho que isso não seria possível.
Nathan Merrill

2
@ NathanMerrill, isso é verdade. Essa abordagem pressupõe que apenas uma base de código esteja definindo os tipos de perguntas.
Winston Ewert

4
@WinstonEwert, este é um bom uso do padrão de visitantes. Mas sua implementação não está totalmente de acordo com o padrão. Geralmente, os métodos no visitante não recebem o nome dos tipos, geralmente têm o mesmo nome e diferem apenas nos tipos dos parâmetros (sobrecarga de parâmetro); o nome comum é visit(o visitante visita). Também o método nos objetos que estão sendo visitados geralmente é chamado accept(Visitor)(o objeto aceita um visitante). Veja oodesign.com/visitor-pattern.html
Viktor Seifert

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 verificará se existem modelos de dados definidos para o objeto.


Parece estar movendo o problema para XML, onde você perde toda a digitação estrita em primeiro lugar.
22617 Nathan Merrill

Não tenho certeza se você está dizendo que é uma coisa boa ou ruim. Por um lado, estamos movendo o problema. Por outro lado, parece uma partida feita no céu.
BTownTKD

2

Se todas as respostas puderem ser codificadas como uma sequência, você poderá fazer o seguinte:

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

Onde a cadeia vazia significa uma pergunta sem resposta ainda. Isso permite que as perguntas, as respostas e a GUI sejam separadas, mas permite 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 sequência de respostas acontece na exibição. A sequência de respostas representa o estado do teste. Eles devem ser armazenados à medida que o aluno progride. Aplicá-los às perguntas permite exibir o teste e seu estado, tanto de forma graduada quanto nã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. Qualquer que seja o esquema planejado para fazer isso, não precisa vazar para esse código.

Se, no entanto, você deseja ter um controle mais dinâmico de como uma pergunta é exibida, faça o seguinte:

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

e isto

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 visualizações que não pretendem exibir score()ou answerKeydepender delas quando não precisam delas. Mas isso significa que você não precisa recriar as perguntas do teste para cada tipo de visão que deseja usar.


Portanto, isso coloca o código da GUI na pergunta. Seu "display" e "displayGraded" são reveladores: para todo tipo de "display", eu precisaria ter outra função.
Nathan Merrill

Não é bem assim, isso faz uma referência a uma visão polimórfica. Pode ser uma GUI, uma página da web, um PDF, qualquer que seja. Essa é uma porta de saída que está recebendo conteúdo sem layout.
Candied_orange

@NathanMerrill, por favor, note edit
#

A nova interface não funciona: você está colocando "MultipleChoiceView" dentro da interface "Pergunta". Você pode colocar o visualizador no construtor, mas na maioria das vezes você não sabe (ou se importa) qual será o visualizador quando criar o objeto. (Isso pode ser resolvido usando um preguiçoso função / fábrica, mas a lógica por trás injetando que a fábrica poderia ficar confuso)
Nathan Merrill

@ NathanMerrill Algo, em algum lugar, é necessário saber onde isso deve ser exibido. A única coisa que o construtor faz é permitir que você decida isso no momento da construção e depois esqueça. Se você não quiser decidir isso na construção, deverá decidir mais tarde e, de alguma forma, lembrar-se dessa decisão até ligar para exibição. Usar fábricas nesses métodos não mudaria esses fatos. Apenas esconde como você tomou a decisão. Geralmente não de um jeito bom.
Candied_orange

1

Na minha opinião, se você precisar de um recurso genérico, 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, criaria classes diferentes para os objetos do renderizador. 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 de pergunta. O código abaixo tenta realizar duas coisas: (i) evitar a verificação de tipo e violação do princípio "L" (substituição de Liskov no SOLID) removendo a subtipo de classe 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 à matriz (esse é realmente 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));
    }
  }
}

O que acontece quando MultipleChoiceQuestionView tenta acessar o campo MultipleChoice.choices? Requer elenco. Claro, se assumirmos essa pergunta.Tipo é único e o código é sensato, é um elenco bastante seguro, mas ainda é um elenco: P
Nathan Merrill

Se você observar no meu exemplo, não existe esse tipo MultipleChoice. Existe apenas um tipo de pergunta, que tentei definir genericamente, com uma lista de informações (você pode armazenar várias opções nessa lista, pode defini-la como quiser). Portanto, não há conversão, você tem apenas um tipo de pergunta e vários objetos que verificam se eles podem renderizar essa pergunta, se o objeto a suporta, então você pode chamar com segurança o método de renderização.
Emerson Cardoso

No meu exemplo, optei por diminuir o acoplamento entre a GUI e as propriedades de tipo forte na classe de pergunta específica; em vez disso, substituo essas propriedades por propriedades genéricas, às quais a GUI precisaria acessar por uma chave de cadeia ou algo mais (acoplamento flexível). Isso é uma troca, talvez esse acoplamento frouxo não seja desejado no seu cenário.
Emerson Cardoso

1

Uma fábrica deve ser capaz de fazer isso. O mapa substitui a instrução switch, necessária apenas para emparelhar a Pergunta (que nada sabe sobre a exibiçã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 pode exibir, e o modelo permanece desconectado da visualização.

A fábrica pode ser preenchida por reflexão ou manualmente no início do aplicativo.


Se você estivesse em um sistema em que o cache da visualização fosse importante (como um jogo), a fábrica poderia incluir um pool de QuestionViews.
Xtros 25/08

Este parece bastante semelhante a resposta de Caleth: Você ainda está indo para necessidade de elenco Questionem um MultipleChoiceQuestionquando você cria aMultipleChoiceView
Nathan Merrill

Pelo menos em C #, eu consegui fazer isso sem elenco. No método getView, quando ele cria a instância de exibição (chamando Activator.CreateInstance (questionViewType, question)), o segundo parâmetro de CreateInstance é o parâmetro enviado ao construtor. Meu construtor MultipleChoiceView aceita apenas uma MultipleChoiceQuestion. Talvez esteja apenas movendo o elenco para dentro da função CreateInstance.
Xtros 12/09

0

Não sei se isso conta como "evitar verificações de tipo", dependendo de como você se sente em relação à 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()]());
}

Isso é basicamente uma verificação de tipo, mas passando de uma ifverificação de tipo para uma dictionaryverificação de tipo. Assim como o Python usa dicionários em vez de instruções switch. Dito isto, gosto desta maneira mais do que uma lista de declarações if.
Nathan Merrill

1
@NathanMerrill Sim. Java não tem uma boa maneira de manter duas hierarquias de classes em paralelo. Em c ++ Eu recomendo um template <typename Q> struct question_traits;com especializações apropriadas
Caleth

@Caleth, você pode acessar essas informações dinamicamente? Eu acho que você precisaria para construir o tipo certo, dada uma instância.
Winston Ewert

Além disso, a fábrica provavelmente precisa da instância da pergunta passada para ela. Infelizmente, esse padrão é confuso, pois normalmente requer um elenco feio.
Winston Ewert
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.