¿Cómo se hace una GUI para una clase polimórfica?

15

Supongamos que tengo un generador de pruebas, para que los profesores puedan crear un montón de preguntas para una prueba.

Sin embargo, no todas las preguntas son iguales: tiene opciones múltiples, cuadro de texto, coincidencias, etc. Cada uno de estos tipos de preguntas debe almacenar diferentes tipos de datos y una GUI diferente tanto para el creador como para el examinador.

Me gustaría evitar dos cosas:

  1. Verificaciones de tipo o conversión de tipo
  2. Cualquier cosa relacionada con la GUI en mi código de datos.

En mi intento inicial, termino con las siguientes clases:

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

Sin embargo, cuando voy a mostrar la prueba, inevitablemente termino con un código como:

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

Esto se siente como un problema muy común. ¿Hay algún patrón de diseño que me permita tener preguntas polimórficas mientras evito los elementos enumerados anteriormente? ¿O es el polimorfismo una idea equivocada en primer lugar?

    
pregunta Nathan Merrill 25.08.2017 - 14:33

6 respuestas

15

Puedes usar un patrón 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);
    }
}

Otra opción es una unión discriminada. Esto dependerá mucho de tu idioma. Esto es mucho mejor si su idioma lo admite, pero muchos idiomas populares no.

    
respondido por el Winston Ewert 25.08.2017 - 14:55
2

En C # / WPF (y, me imagino, en otros lenguajes de diseño centrados en UI), tenemos Plantillas de datos . Al definir plantillas de datos, crea una asociación entre un tipo de "objeto de datos" y una "plantilla de interfaz de usuario" especializada creada específicamente para mostrar ese objeto.

Una vez que proporcione instrucciones para que la IU cargue un tipo específico de objeto, verá si hay alguna plantilla de datos definida para el objeto.

    
respondido por el BTownTKD 25.08.2017 - 19:56
2

Si cada respuesta puede codificarse como una cadena, puede hacer esto:

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

Donde la cadena vacía significa una pregunta que aún no tiene respuesta. Esto permite separar las preguntas, las respuestas y la GUI, pero permite el 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)
        );            
    }
}

El cuadro de texto, la coincidencia, etc. podrían tener diseños similares, todos implementando la interfaz de preguntas. La construcción de la cadena de respuesta pasa en la vista. Las cadenas de respuesta representan el estado de la prueba. Deben ser almacenados a medida que el estudiante progresa. Aplicarlas a las preguntas permite mostrar la prueba y su estado tanto de forma gradual como no gradual.

Al separar la salida en display() y displayGraded() , no es necesario cambiar la vista y no es necesario realizar bifurcaciones en los parámetros. Sin embargo, cada vista es libre de reutilizar toda la lógica de visualización que pueda cuando se muestra. Cualquiera que sea el esquema que se diseñe para hacer eso, no es necesario filtrarlo en este código.

Sin embargo, si desea tener un control más dinámico de cómo se muestra una pregunta, puede hacer esto:

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

y esto

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

Esto tiene el inconveniente de que requiere vistas que no pretenden mostrar score() o answerKey para depender de ellas cuando no las necesiten. Pero significa que no tiene que reconstruir las preguntas de la prueba para cada tipo de vista que desee utilizar.

    
respondido por el candied_orange 25.08.2017 - 15:28
1

En mi opinión, si necesita una característica tan genérica, disminuiría el acoplamiento entre las cosas en el código. Intentaría definir el tipo de pregunta lo más genérico posible, y luego crearía diferentes clases para los objetos del renderizador. Por favor, vea los ejemplos a continuación:

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

Luego, para la parte de representación, eliminé la comprobación de tipo implementando una comprobación simple de los datos dentro del objeto de la pregunta. El código a continuación trata de lograr dos cosas: (i) evitar la verificación de tipos y evitar la violación del principio "L" (sustitución de Liskov en SOLID) al eliminar el subtipo de clase de pregunta; y (ii) haga que el código sea extensible, nunca cambiando el código de representación central a continuación, simplemente agregando más implementaciones de QuestionView y sus instancias a la matriz (este es realmente el principio "O" en SOLID: abierto para extensión y cerrado para modificación).

///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));
    }
  }
}
    
respondido por el Emerson Cardoso 25.08.2017 - 16:01
0

No estoy seguro de que esto cuente como "evitar las comprobaciones de tipos", según cómo te sientas con reflexión .

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

Una fábrica debería poder hacer esto. El mapa reemplaza la declaración de cambio, que se necesita únicamente para emparejar la pregunta (que no sabe nada sobre la 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)
}

Con esto, la vista utiliza el tipo específico de pregunta que puede mostrar, y el modelo permanece desconectado de la vista.

La fábrica podría llenarse por reflexión o manualmente al inicio de la aplicación.

    
respondido por el Xtros 26.08.2017 - 01:19

Lea otras preguntas en las etiquetas