Wie erstellt man eine GUI für eine polymorphe Klasse?

17

Nehmen wir an, ich habe einen Testbuilder, damit die Lehrer eine Reihe von Fragen für einen Test erstellen können.

Es sind jedoch nicht alle Fragen gleich: Sie haben mehrere Auswahlmöglichkeiten, Textfelder, Übereinstimmungen usw. Jeder dieser Fragetypen muss unterschiedliche Datentypen speichern und sowohl für den Ersteller als auch für den Testteilnehmer eine unterschiedliche Benutzeroberfläche benötigen.

Ich möchte zwei Dinge vermeiden:

  1. Typprüfungen oder Typprüfungen
  2. Alles, was mit der GUI in meinem Datencode zu tun hat.

Bei meinem ersten Versuch lande ich in den folgenden Klassen:

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

Wenn ich jedoch zum Anzeigen des Tests gehe, erhalte ich zwangsläufig folgenden Code:

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

Dies scheint ein sehr häufiges Problem zu sein. Gibt es ein Entwurfsmuster, das es mir ermöglicht, polymorphe Fragen zu stellen und dabei die oben aufgeführten Punkte zu vermeiden? Oder ist Polymorphismus überhaupt die falsche Idee?

Nathan Merrill
quelle
6
Es ist keine schlechte Idee, nach Dingen zu fragen, mit denen Sie Probleme haben, aber für mich ist diese Frage in der Regel zu umfassend / unklar, und schließlich stellen Sie die Frage ...
Kayess
1
Im Allgemeinen versuche ich, Typüberprüfungen / Typumwandlungen zu vermeiden, da dies im Allgemeinen zu einer geringeren Überprüfung der Kompilierungszeit führt und im Grunde genommen den Polymorphismus "umgeht", anstatt ihn zu verwenden. Ich bin nicht grundsätzlich dagegen, sondern suche nach Lösungen ohne sie.
Nathan Merrill
1
Was Sie suchen, ist im Grunde genommen ein DSL zur Beschreibung einfacher Vorlagen, kein hierarchisches Objektmodell.
user1643723
2
@ NathanMerrill "Ich will auf jeden Fall Polymophismus", - sollte das nicht umgekehrt sein? Möchten Sie lieber Ihr eigentliches Ziel erreichen oder "Polymophismus benutzen"? IMO, Polymophismus ist gut geeignet, um komplexe APIs und Modellierungsverhalten zu erstellen. Es ist weniger gut für die Modellierung von Daten geeignet (was Sie gerade tun).
user1643723
1
@NathanMerrill "Jeder Zeitblock führt eine Aktion aus oder enthält andere Zeitblöcke und führt sie aus oder fordert Benutzeraufforderung an", - diese Informationen sind sehr wertvoll, ich schlage vor, dass Sie sie der Frage hinzufügen.
user1643723

Antworten:

15

Sie können ein Besuchermuster verwenden:

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

interface Question {
    void visit(QuestionVisitor);
}

class MultipleChoice implements Question {

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

Eine andere Option ist eine diskriminierte Gewerkschaft. Dies hängt sehr stark von Ihrer Sprache ab. Dies ist viel besser, wenn Ihre Sprache dies unterstützt, aber viele gängige Sprachen nicht.

Winston Ewert
quelle
2
Hmm ... das ist keine schreckliche Option, aber die QuestionVisitor-Oberfläche müsste jedes Mal eine Methode hinzufügen, wenn es eine andere Art von Frage gibt, die nicht besonders skalierbar ist.
Nathan Merrill
3
@ NathanMerrill, ich glaube nicht, dass es deine Skalierbarkeit wirklich stark verändert. Ja, Sie müssen die neue Methode in jeder Instanz von QuestionVisitor implementieren. Aber das ist Code, den Sie in jedem Fall schreiben müssen, um die GUI für den neuen Fragentyp zu handhaben. Ich denke nicht, dass es wirklich viel Code hinzufügt, den Sie sonst nicht korrigieren müssten, aber es macht fehlenden Code zu einem Kompilierungsfehler.
Winston Ewert
4
Wahr. Wenn ich jedoch jemals jemandem erlauben wollte, seinen eigenen Fragentyp + Renderer zu erstellen (was ich nicht tue), denke ich nicht, dass das möglich wäre.
Nathan Merrill
2
@ NathanMerrill, das stimmt. Bei diesem Ansatz wird davon ausgegangen, dass nur eine Codebasis die Fragetypen definiert.
Winston Ewert
4
@ WinstonEwert Dies ist eine gute Verwendung des Besuchermusters. Aber Ihre Implementierung entspricht nicht ganz dem Muster. Normalerweise werden die Methoden im Besucher nicht nach den Typen benannt, sie haben normalerweise den gleichen Namen und unterscheiden sich nur in den Typen der Parameter (Parameterüberladung); Der gebräuchliche Name ist visit(der Besucher besucht). Auch die Methode in den besuchten Objekten wird normalerweise aufgerufen accept(Visitor)(das Objekt akzeptiert einen Besucher). Siehe oodesign.com/visitor-pattern.html
Viktor Seifert
2

In C # / WPF (und, wie ich mir vorstellen kann, in anderen auf die Benutzeroberfläche ausgerichteten Entwurfssprachen) verfügen wir über DataTemplates . Durch das Definieren von Datenvorlagen erstellen Sie eine Zuordnung zwischen einem Typ von "Datenobjekt" und einer speziellen "UI-Vorlage", die speziell für die Anzeige dieses Objekts erstellt wurde.

Sobald Sie der Benutzeroberfläche Anweisungen zum Laden eines bestimmten Objekttyps gegeben haben, wird angezeigt, ob für das Objekt Datenvorlagen definiert sind.

BTownTKD
quelle
Dies scheint das Problem auf XML zu verlagern, wo Sie an erster Stelle alle strengen Eingaben verlieren.
Nathan Merrill
Ich bin mir nicht sicher, ob Sie sagen, dass das eine gute oder eine schlechte Sache ist. Einerseits bewegen wir das Problem. Auf der anderen Seite klingt es wie ein Match, das im Himmel gemacht wurde.
BTownTKD
2

Wenn jede Antwort als Zeichenfolge codiert werden kann, können Sie dies tun:

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

Wo die leere Zeichenfolge eine Frage bedeutet, auf die es noch keine Antwort gibt. Auf diese Weise können die Fragen, die Antworten und die Benutzeroberfläche getrennt werden, ohne dass ein Polymorphismus auftritt.

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

Textfelder, Übereinstimmungen usw. könnten ähnliche Designs aufweisen und alle die Fragenschnittstelle implementieren. Der Aufbau des Antwortstrings erfolgt in der Ansicht. Die Antwortzeichenfolgen geben den Status des Tests an. Sie sollten im Verlauf des Studiums gespeichert werden. Wenn Sie sie auf die Fragen anwenden, können Sie den Test und seinen Status sowohl benotet als auch unbenotet anzeigen.

Durch das Trennen der Ausgabe in display()und muss displayGraded()die Ansicht nicht ausgetauscht werden und es muss keine Verzweigung der Parameter vorgenommen werden. Es steht jedoch jeder Ansicht frei, so viel Anzeigelogik wie möglich beim Anzeigen wiederzuverwenden. Welches Schema auch immer entwickelt wurde, es muss nicht in diesen Code eindringen.

Wenn Sie jedoch eine dynamischere Steuerung der Anzeige einer Frage wünschen, können Sie dies tun:

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

und das

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

Dies hat den Nachteil, dass Ansichten erforderlich sind, die das nicht anzeigen score()oder answerKeyvon ihnen abhängig sind, wenn sie sie nicht benötigen. Dies bedeutet jedoch, dass Sie die Testfragen nicht für jede Art von Ansicht neu erstellen müssen, die Sie verwenden möchten.

kandierte_orange
quelle
Das setzt also GUI-Code in die Frage. Ihr "display" und "displayGraded" ist aufschlussreich: Für jede Art von "display" müsste ich eine andere Funktion haben.
Nathan Merrill
Nicht ganz, dies verweist auf eine polymorphe Sichtweise. Es KANN eine GUI sein, eine Webseite, ein PDF, was auch immer. Dies ist ein Ausgabeport, an den layoutfreie Inhalte gesendet werden.
candied_orange
@ NathanMerrill beachten Sie bitte bearbeiten
candied_orange
Die neue Benutzeroberfläche funktioniert nicht: Sie fügen "MultipleChoiceView" in die Benutzeroberfläche "Frage" ein. Sie können den Viewer in den Konstruktor einfügen, aber die meiste Zeit wissen Sie nicht (oder kümmern sich nicht darum), welcher Viewer sich beim Erstellen des Objekts befindet. (Dies könnte durch die Verwendung einer Lazy-Funktion / -Factory behoben werden, aber die Logik hinter dem Injizieren in diese Factory könnte chaotisch werden.)
Nathan Merrill
@ NathanMerrill Irgendwas, irgendwo muss man wissen, wo das angezeigt werden soll. Das einzige, was der Konstruktor tut, ist, dass Sie dies zur Konstruktionszeit entscheiden und es dann vergessen. Wenn Sie dies beim Bau nicht entscheiden möchten, müssen Sie sich später entscheiden und sich diese Entscheidung irgendwie merken, bis Sie display aufrufen. Die Verwendung von Fabriken in diesen Methoden würde diese Tatsachen nicht ändern. Es verbirgt nur, wie Sie die Entscheidung getroffen haben. Normalerweise nicht gut.
candied_orange
1

Wenn Sie eine solche generische Funktion benötigen, würde ich meiner Meinung nach die Kopplung zwischen den Elementen im Code verringern. Ich würde versuchen, den Fragentyp so allgemein wie möglich zu definieren, und danach würde ich verschiedene Klassen für die Renderer-Objekte erstellen. Bitte beachten Sie die folgenden Beispiele:

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

Dann entfernte ich für den Rendering-Teil die Typprüfung, indem ich eine einfache Prüfung der Daten im Fragenobjekt implementierte. Der folgende Code versucht, zwei Dinge zu erreichen: (i) Vermeiden Sie die Typprüfung und die Verletzung des "L" -Prinzips (Liskov-Substitution in SOLID), indem Sie die Untertypisierung der Frageklasse entfernen. und (ii) den Code erweiterbar machen, indem der Kern-Rendering-Code unten niemals geändert wird, indem dem Array lediglich weitere QuestionView-Implementierungen und deren Instanzen hinzugefügt werden (dies ist tatsächlich das "O" -Prinzip in SOLID - offen für Erweiterungen und geschlossen für Änderungen).

///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
quelle
Was passiert, wenn MultipleChoiceQuestionView versucht, auf das Feld MultipleChoice.choices zuzugreifen? Es erfordert eine Besetzung. Sicher, wenn wir diese Frage annehmen. Typ ist eindeutig und der Code ist vernünftig, es ist eine ziemlich sichere Besetzung, aber es ist immer noch eine Besetzung: P
Nathan Merrill
Wenn Sie in meinem Beispiel notieren, gibt es keinen solchen Typ MultipleChoice. Es gibt nur eine Art Frage, die ich generisch zu definieren versucht habe, mit einer Liste von Informationen (Sie können mehrere Auswahlmöglichkeiten in dieser Liste speichern, Sie können sie definieren, wie Sie möchten). Daher gibt es keine Umwandlung, Sie haben nur einen Fragentyp und mehrere Objekte, die prüfen, ob sie diese Frage rendern können. Wenn das Objekt sie unterstützt, können Sie die Rendermethode sicher aufrufen.
Emerson Cardoso
In meinem Beispiel habe ich beschlossen, die Kopplung zwischen Ihrer GUI und stark typisierten Eigenschaften in einer bestimmten Fragenklasse zu verringern. Stattdessen ersetze ich diese Eigenschaften durch generische Eigenschaften, auf die die GUI über einen Zeichenfolgenschlüssel oder etwas anderes zugreifen müsste (lose Kopplung). Dies ist ein Kompromiss, möglicherweise ist diese lose Kopplung in Ihrem Szenario nicht erwünscht.
Emerson Cardoso
1

Eine Fabrik sollte dazu in der Lage sein. Die Map ersetzt die switch-Anweisung, die nur zum Koppeln der Frage (die nichts über die Ansicht weiß) mit der QuestionView erforderlich ist.

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

Damit verwendet die Ansicht den spezifischen Fragentyp, den sie anzeigen kann, und das Modell bleibt von der Ansicht getrennt.

Die Factory kann über Reflection oder manuell beim Start der Anwendung aufgefüllt werden.

Xtros
quelle
Wenn Sie sich in einem System befunden haben, in dem das Zwischenspeichern der Ansicht wichtig war (wie in einem Spiel), könnte die Factory einen Pool der Fragenansichten enthalten.
Xtros
Dies scheint ziemlich ähnlich wie Caleth Antwort: Sie sind nach wie vor Bedarf an Guss gehen Questionin ein , MultipleChoiceQuestionwenn Sie das schaffenMultipleChoiceView
Nathan Merrill
Zumindest in C # ist mir dies ohne Besetzung gelungen. Bei der Methode getView wird beim Erstellen der Ansichtsinstanz (durch Aufrufen von Activator.CreateInstance (questionViewType, question)) der zweite Parameter von CreateInstance an den Konstruktor gesendet. Mein MultipleChoiceView-Konstruktor akzeptiert nur eine MultipleChoiceQuestion. Vielleicht wird die Besetzung nur in die Funktion CreateInstance verschoben.
Xtros
0

Ich bin mir nicht sicher, ob dies als "Vermeiden von Typprüfungen" gilt, je nachdem, wie Sie über Reflexion denken .

// 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
quelle
Dies ist im Grunde eine Typprüfung, aber der Übergang von einer ifTypprüfung zu einer dictionaryTypprüfung. So wie Python Wörterbücher anstelle von switch-Anweisungen verwendet. Das heißt, ich mag auf diese Weise mehr als eine Liste von if-Anweisungen.
Nathan Merrill
1
@ NathanMerrill Ja. Java hat keine gute Möglichkeit, zwei Klassenhierarchien parallel zu halten. In c ++ würde ich eine template <typename Q> struct question_traits;mit entsprechenden Spezialisierungen empfehlen
Caleth
@Caleth, können Sie dynamisch auf diese Informationen zugreifen? Ich denke, Sie müssten, um den richtigen Typ für eine Instanz zu konstruieren.
Winston Ewert
Außerdem benötigt die Fabrik wahrscheinlich die an sie übergebene Frageninstanz. Das macht dieses Muster leider unordentlich, da es normalerweise eine hässliche Besetzung erfordert.
Winston Ewert