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

17

Digamos que tengo un creador de exámenes, para que los maestros puedan crear un montón de preguntas para un examen.

Sin embargo, no todas las preguntas son iguales: tiene opción múltiple, cuadro de texto, coincidencia, etc. Cada uno de estos tipos de preguntas necesita almacenar diferentes tipos de datos y una GUI diferente tanto para el creador como para el examinado.

Me gustaría evitar dos cosas:

  1. Verificaciones de tipo o fundición de tipos
  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 terminaré 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 mencionados anteriormente? ¿O es el polimorfismo la idea equivocada en primer lugar?

Nathan Merrill
fuente
66
No es una mala idea para preguntar sobre cosas que tiene problemas con el, pero para mí esta pregunta tiende a ser demasiado amplia / incierto y al fin se está cuestionando la pregunta ...
Kayess
1
En general, trato de evitar verificaciones de tipo / conversión de tipo, ya que generalmente conduce a una verificación en tiempo de compilación menor y básicamente está "trabajando" en torno al polimorfismo en lugar de usarlo. No me opongo fundamentalmente a ellos, pero trato de buscar soluciones sin ellos.
Nathan Merrill
1
Lo que está buscando es básicamente un DSL para describir plantillas simples, no un modelo de objeto jerárquico.
user1643723
2
@NathanMerrill "Definitivamente quiero polimorfismo", ¿no debería ser al revés? ¿Prefieres alcanzar tu objetivo real o "utilizar el polimorfismo"? En mi opinión, el polimorfismo es muy adecuado para construir API complejas y comportamientos de modelado. Es menos adecuado para modelar datos (que es lo que está haciendo actualmente).
user1643723
1
@NathanMerrill "cada bloque de tiempo ejecuta una acción, o contiene otros bloqueos de tiempo y los ejecuta, o solicita el aviso del usuario", sugiero que esta información es muy valiosa para agregarla a la pregunta.
user1643723

Respuestas:

15

Puede 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.

Winston Ewert
fuente
2
Hmm ... esta no es una opción terrible, sin embargo, la interfaz de QuestionVisitor necesitaría agregar un método cada vez que haya un tipo diferente de pregunta, que no es súper escalable.
Nathan Merrill
3
@NathanMerrill, no creo que realmente cambie mucho tu escalabilidad. Sí, debe implementar el nuevo método en cada instancia de QuestionVisitor. Pero ese es el código que tendrá que escribir en cualquier caso para manejar la GUI para el nuevo tipo de pregunta. No creo que realmente agregue mucho código que de otro modo no tendría que corregir, pero convierte el código faltante en un error de compilación.
Winston Ewert
44
Cierto. Sin embargo, si alguna vez quisiera permitir que alguien hiciera su propio Tipo de pregunta + Renderizador (lo cual no hago), no creo que eso sea posible.
Nathan Merrill
2
@NathanMerrill, eso es cierto. Este enfoque supone que solo una base de código está definiendo los tipos de preguntas.
Winston Ewert
44
@ WinstonEwert, este es un buen uso del patrón de visitante. Pero su implementación no está de acuerdo con el patrón. Por lo general, los métodos en el visitante no se nombran después de los tipos, generalmente tienen el mismo nombre y solo difieren en los tipos de los parámetros (sobrecarga de parámetros); El nombre común es visit(el visitante visita). También se suele llamar al método en los objetos que se visitan accept(Visitor)(el objeto acepta un visitante). Ver oodesign.com/visitor-pattern.html
Viktor Seifert el
2

En C # / WPF (y, me imagino, en otros lenguajes de diseño centrados en la interfaz de usuario), tenemos DataTemplates . Al definir plantillas de datos, crea una asociación entre un tipo de "objeto de datos" y una "plantilla de IU" 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.

BTownTKD
fuente
Esto parece estar moviendo el problema a XML, donde se pierde toda la escritura estricta en primer lugar.
Nathan Merrill
No estoy seguro de si estás diciendo que es algo bueno o malo. Por un lado, estamos moviendo el problema. Por otro lado, suena como una combinación hecha en el cielo.
BTownTKD
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 es una pregunta que aún no tiene respuesta. Esto permite que las preguntas, las respuestas y la GUI se separen, 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 ocurre en la vista. Las cadenas de respuesta representan el estado de la prueba. Deben almacenarse a medida que el alumno progresa. Aplicarlas a las preguntas permite mostrar la prueba y su estado de manera calificada y no calificada.

Al separar la salida en display()y displayGraded()la vista no necesita intercambiarse y no es necesario realizar ramificaciones en los parámetros. Sin embargo, cada vista es libre de reutilizar tanta lógica de visualización como sea posible cuando se visualiza. Cualquiera sea el esquema diseñado para hacer eso, no necesita filtrarse 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 answerKeydepender de ellas cuando no las necesitan. Pero significa que no tiene que reconstruir las preguntas de la prueba para cada tipo de vista que desea usar.

naranja confitada
fuente
Entonces esto pone el código GUI en la pregunta. Su "display" y "displayGraded" son reveladores: para cada tipo de "display", tendría que tener otra función.
Nathan Merrill
No del todo, esto pone una referencia a una vista que es polimórfica. PODRÍA ser una GUI, una página web, un PDF, lo que sea. Este es un puerto de salida que se envía contenido libre de diseño.
candied_orange
@NathanMerrill nota editar
candied_orange
La nueva interfaz no funciona: está colocando "MultipleChoiceView" dentro de la interfaz "Pregunta". Usted puede poner al espectador en el constructor, pero la mayoría de las veces no sabe (o atención), que el espectador será cuando usted hace el objeto. (Eso podría resolverse mediante el uso de una función / fábrica perezosa, pero la lógica detrás de la inyección en esa fábrica podría complicarse)
Nathan Merrill
@NathanMerrill Algo, en algún lugar tiene que saber dónde se debe mostrar esto. Lo único que hace el constructor es permitirle decidir esto en el momento de la construcción y luego olvidarse de ello. Si no desea decidir esto en la construcción, debe decidir más tarde y recordar de alguna manera esa decisión hasta que llame a la pantalla. El uso de fábricas en estos métodos no cambiaría estos hechos. Simplemente oculta cómo tomaste la decisión. Por lo general, no en el buen sentido.
candied_orange
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 de representación. 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 verificación de Tipo implementando una verificación simple en los datos dentro del objeto de pregunta. El siguiente código intenta lograr dos cosas: (i) evitar la verificación de tipo y evitar la violación del principio "L" (sustitución de Liskov en SÓLIDO) eliminando el subtipo de clase de Pregunta; y (ii) hacer que el código sea extensible, nunca cambiando el código de representación principal a continuación, simplemente agregando más implementaciones de QuestionView y sus instancias a la matriz (este es en realidad 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));
    }
  }
}
Emerson Cardoso
fuente
¿Qué sucede cuando MultipleChoiceQuestionView intenta acceder al campo MultipleChoice.choices? Requiere un yeso. Claro, si asumimos esa pregunta. El tipo es único y el código es cuerdo, es un elenco bastante seguro, pero sigue siendo un elenco: P
Nathan Merrill
Si observa en mi ejemplo, no existe tal tipo MultipleChoice. Solo hay un tipo de pregunta, que traté de definir genéricamente, con una lista de información (puede almacenar múltiples opciones en esta lista, puede definirla como desee). Por lo tanto, no hay conversión, solo tiene una Pregunta de tipo y múltiples objetos que verifican si pueden hacer esta pregunta, si el objeto la admite, entonces puede llamar con seguridad al método de representación.
Emerson Cardoso
En mi ejemplo, elegí disminuir el acoplamiento entre su GUI y propiedades de tipo fuerte en clase de pregunta específica; en su lugar, reemplazo esas propiedades por propiedades genéricas, a las que la GUI necesitaría acceder mediante una clave de cadena u otra cosa (acoplamiento flojo). Esto es una compensación, tal vez este acoplamiento flojo no se desea en su escenario.
Emerson Cardoso
1

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 el 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 mediante reflexión o manualmente al inicio de la aplicación.

Xtros
fuente
Si estuvieras en un sistema donde el almacenamiento en caché de la vista era importante (como un juego), la fábrica podría incluir un Pool de los QuestionViews.
Xtros
Esto parece bastante similar a la respuesta de Caleth: aún necesitarás lanzar Questionun papel MultipleChoiceQuestioncuando crees elMultipleChoiceView
Nathan Merrill
Al menos en C #, logré hacer esto sin un elenco. En el método getView, cuando crea la instancia de vista (llamando a Activator.CreateInstance (questionViewType, question)), el segundo parámetro de CreateInstance es el parámetro enviado al constructor. Mi constructor MultipleChoiceView solo acepta una pregunta MultipleChoiceQuestion. Sin embargo, quizás solo esté moviendo el yeso al interior de la función CreateInstance.
Xtros
0

No estoy seguro de que esto cuente como "evitar verificaciones de tipo", dependiendo de cómo te sientas con respecto a la 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()]());
}
Caleth
fuente
Esto es básicamente una verificación de tipo, pero pasar de una ifverificación de tipo a una dictionaryverificación de tipo. Como cómo Python usa diccionarios en lugar de declaraciones de cambio. Dicho esto, me gusta esta manera más de una lista de sentencias if.
Nathan Merrill
1
@NathanMerrill Sí. Java no tiene una buena manera de mantener dos jerarquías de clases en paralelo. En c ++, recomendaría una template <typename Q> struct question_traits;con las especializaciones apropiadas
Caleth
@Caleth, ¿puedes acceder a esa información dinámicamente? Creo que tendría que hacerlo para construir el tipo correcto dada una instancia.
Winston Ewert
Además, la fábrica probablemente necesita que le pasen la instancia de la pregunta. Desafortunadamente, esto hace que este patrón sea desordenado, ya que generalmente requiere un reparto feo.
Winston Ewert