Jak utworzyć GUI dla klasy polimorficznej?

17

Załóżmy, że mam narzędzie do budowania testów, aby nauczyciele mogli zadać mnóstwo pytań do testu.

Jednak nie wszystkie pytania są takie same: masz wiele możliwości wyboru, pole tekstowe, dopasowanie itd. Każdy z tych typów pytań musi przechowywać różne typy danych i musi mieć inny GUI zarówno dla twórcy, jak i dla osoby wykonującej test.

Chciałbym uniknąć dwóch rzeczy:

  1. Sprawdzanie typu lub rzutowanie typu
  2. Wszystko związane z GUI w moim kodzie danych.

W mojej pierwszej próbie kończę z następującymi klasami:

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

Jednak kiedy idę, aby wyświetlić test, nieuchronnie skończy się na kodzie:

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

To wydaje się być bardzo częstym problemem. Czy jest jakiś wzór, który pozwala mi zadawać pytania polimorficzne, unikając elementów wymienionych powyżej? A może polimorfizm to zły pomysł?

Nathan Merrill
źródło
6
Pytanie o rzeczy, z którymi masz problemy, nie jest złym pomysłem, ale dla mnie to pytanie jest zbyt szerokie / niejasne i w końcu kwestionujesz pytanie ...
kayess
1
Zasadniczo staram się unikać sprawdzania typów / rzutowania typów, ponieważ generalnie prowadzi to do mniejszej kontroli czasu kompilacji i zasadniczo „obchodzi” polimorfizm, a nie go używa. Nie jestem im całkowicie przeciwny, ale staram się szukać rozwiązań bez nich.
Nathan Merrill,
1
To, czego szukasz, to w zasadzie DSL do opisywania prostych szablonów, a nie hierarchiczny model obiektowy.
user1643723,
2
@NathanMerrill „Zdecydowanie chcę polimorfizmu”, - czy nie powinno być odwrotnie? Czy wolałbyś raczej osiągnąć swój rzeczywisty cel lub „zastosować polimorfizm”? IMO, polimorfizm dobrze nadaje się do budowania złożonych interfejsów API i zachowania modelowania. Jest mniej odpowiedni do modelowania danych (co obecnie robisz).
user1643723,
1
@NathanMerrill „każdy blok czasowy wykonuje akcję lub zawiera inne blokady czasowe i wykonuje je lub prosi użytkownika”, - ta informacja jest bardzo cenna, sugeruję, abyś dodał ją do pytania.
user1643723,

Odpowiedzi:

15

Możesz użyć wzorca odwiedzającego:

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

interface Question {
    void visit(QuestionVisitor);
}

class MultipleChoice implements Question {

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

Inną opcją jest dyskryminowany związek zawodowy. Zależy to bardzo od twojego języka. Jest to o wiele lepsze, jeśli Twój język to obsługuje, ale wiele popularnych języków tego nie robi.

Winston Ewert
źródło
2
Hmm .... to nie jest straszna opcja, jednak interfejs QuestionVisitor musiałby dodawać metodę za każdym razem, gdy pojawia się inny rodzaj pytania, który nie jest super skalowalny.
Nathan Merrill
3
@NathanMerrill, nie sądzę, że to naprawdę zmienia twoją skalowalność. Tak, musisz zaimplementować nową metodę w każdym przypadku QuestionVisitor. Ale to kod, który musisz napisać, aby obsłużyć GUI dla nowego typu pytania. Nie sądzę, aby naprawdę dodawał dużo kodu, którego inaczej nie musiałbyś poprawiać, ale zamienia brakujący kod w błąd kompilacji.
Winston Ewert,
4
Prawdziwe. Jednakże, jeśli kiedykolwiek chciałbym pozwolić komuś na stworzenie własnego typu Pytanie + Renderer (czego nie robię), nie sądzę, aby było to możliwe.
Nathan Merrill,
2
@NathanMerrill, to prawda. Podejście to zakłada, że ​​tylko jedna baza kodu definiuje typy pytań.
Winston Ewert,
4
@WinstonEwert jest to dobre wykorzystanie wzorca odwiedzającego. Ale twoja implementacja nie jest całkiem zgodna z tym wzorcem. Zazwyczaj metody odwiedzającego nie są nazwane po typach, zwykle mają tę samą nazwę i różnią się jedynie typami parametrów (przeciążenie parametrów); nazwa zwyczajowa to visit(odwiedzający odwiedzający). Zwykle wywoływana jest również metoda w odwiedzanych obiektach accept(Visitor)(obiekt akceptuje gościa). Zobacz oodesign.com/visitor-pattern.html
Viktor Seifert
2

W C # / WPF (i, jak sądzę, w innych językach projektowania skoncentrowanych na interfejsie użytkownika) mamy DataTemplates . Definiując szablony danych, tworzysz powiązanie między jednym typem „obiektu danych” a specjalistycznym „szablonem interfejsu użytkownika” utworzonym specjalnie w celu wyświetlenia tego obiektu.

Gdy podasz instrukcje dla interfejsu użytkownika dotyczące ładowania określonego rodzaju obiektu, zobaczysz, czy dla tego obiektu zdefiniowano jakieś szablony danych.

BTownTKD
źródło
Wydaje się, że przenosi to problem na XML, gdzie tracisz wszystkie ścisłe pisanie w pierwszej kolejności.
Nathan Merrill
Nie jestem pewien, czy mówisz, że to dobrze, czy źle. Z jednej strony przenosimy problem. Z drugiej strony brzmi to jak mecz wykonany w niebie.
BTownTKD
2

Jeśli każdą odpowiedź można zakodować jako ciąg, możesz to zrobić:

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

Gdzie pusty ciąg oznacza pytanie, na które nie ma jeszcze odpowiedzi. Umożliwia to rozdzielenie pytań, odpowiedzi i GUI, a jednocześnie pozwala na polimorfizm.

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

Pole tekstowe, dopasowanie itp. Mogą mieć podobne projekty, wszystkie implementują interfejs pytań. Konstrukcja ciągu odpowiedzi ma miejsce w widoku. Łańcuchy odpowiedzi reprezentują stan testu. Powinny być przechowywane w miarę postępów ucznia. Zastosowanie ich do pytań pozwala wyświetlić test i jego stan zarówno w sposób stopniowany, jak i niesklasyfikowany.

Poprzez oddzielenie wyjście do display()i displayGraded()widok nie muszą być zamienione na zewnątrz, bez rozgałęzień do zrobienia na parametrach. Jednak każdy widok może ponownie wykorzystać tyle logiki wyświetlania, ile może podczas wyświetlania. Niezależnie od tego, jaki plan zostanie stworzony, nie trzeba wyciekać do tego kodu.

Jeśli jednak chcesz mieć bardziej dynamiczną kontrolę nad sposobem wyświetlania pytania, możesz to zrobić:

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

i to

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

Ma to tę wadę, że wymaga widoków, które nie zamierzają wyświetlać score()lub answerKeypolegać na nich, gdy ich nie potrzebują. Oznacza to jednak, że nie musisz odbudowywać pytań testowych dla każdego typu widoku, którego chcesz użyć.

candied_orange
źródło
To stawia kod GUI w pytaniu. Twoje „display” i „displayGraded” ujawniają: Dla każdego rodzaju „display” musiałbym mieć inną funkcję.
Nathan Merrill,
Niezupełnie oznacza to odniesienie do poglądu, który jest polimorficzny. MOŻE to być GUI, strona internetowa, plik PDF, cokolwiek. Jest to port wyjściowy wysyłany bez zawartości układu.
candied_orange
@NathanMerrill zwróć uwagę na edycję
candied_orange
Nowy interfejs nie działa: Umieszczasz „MultipleChoiceView” w interfejsie „Pytanie”. Państwo mogą umieścić widza do konstruktora, ale przez większość czasu nie wiem (lub opieki), które widz będzie w momencie tworzenia obiektu. (Można to rozwiązać za pomocą leniwej funkcji / fabryki, ale logika wstrzykiwania do tej fabryki może się popsuć)
Nathan Merrill
@NathanMerrill Coś, gdzieś musi wiedzieć, gdzie to ma być wyświetlane. Konstruktor może jedynie zdecydować o tym na etapie budowy, a następnie zapomnieć o tym. Jeśli nie chcesz decydować o tym podczas budowy, musisz zdecydować później i jakoś zapamiętać tę decyzję, dopóki nie wywołasz display. Zastosowanie fabryk w tych metodach nie zmieniłoby tych faktów. Po prostu ukrywa, jak podjąłeś decyzję. Zwykle nie w dobry sposób.
candied_orange
1

Moim zdaniem, jeśli potrzebujesz takiej ogólnej funkcji, zmniejszyłbym sprzężenie między elementami w kodzie. Spróbuję zdefiniować typ pytania bardziej ogólny, jak to możliwe, a następnie utworzę różne klasy dla obiektów renderujących. Zobacz poniższe przykłady:

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

Następnie w części dotyczącej renderowania usunąłem sprawdzanie typu, wykonując proste sprawdzenie danych w obiekcie pytania. Poniższy kod próbuje osiągnąć dwie rzeczy: (i) uniknąć sprawdzania typu i uniknąć naruszenia zasady „L” (podstawienie Liskova w SOLID) poprzez usunięcie podtypu klasy pytania; oraz (ii) uczynić kod rozszerzalnym, nigdy nie zmieniając podstawowego kodu renderującego poniżej, po prostu dodając do tablicy więcej implementacji QuestionView i jego instancji (jest to w rzeczywistości zasada „O” w SOLID - otwarta na rozszerzenie i zamknięta na modyfikację).

///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
źródło
Co dzieje się, gdy MultipleChoiceQuestionView próbuje uzyskać dostęp do pola MultipleChoice.choices? Wymaga obsady. Jasne, jeśli założymy, że to pytanie. Typ jest unikalny, a kod jest rozsądny, jest to dość bezpieczna obsada, ale nadal jest obsada: P
Nathan Merrill
Jeśli zauważysz w moim przykładzie, nie ma takiego typu MultipleChoice. Istnieje tylko jeden typ pytania, które próbowałem zdefiniować ogólnie, z listą informacji (możesz przechowywać wiele opcji na tej liście, możesz zdefiniować je tak, jak chcesz). Dlatego nie ma rzutowania, masz tylko jedno pytanie i wiele obiektów, które sprawdzają, czy mogą renderować to pytanie, jeśli obiekt je obsługuje, możesz bezpiecznie wywołać metodę renderowania.
Emerson Cardoso,
W moim przykładzie postanowiłem zmniejszyć sprzężenie między twoim GUI a silnymi właściwościami typowania w konkretnej klasie pytań; zamiast tego zastępuję te właściwości właściwościami ogólnymi, do których GUI musiałby uzyskać dostęp za pomocą klucza łańcuchowego lub czegoś innego (luźne połączenie). Jest to kompromis, być może to luźne połączenie nie jest pożądane w twoim scenariuszu.
Emerson Cardoso,
1

Fabryka powinna być w stanie to zrobić. Mapa zastępuje instrukcję switch, która jest potrzebna wyłącznie do sparowania pytania (które nie wie nic o widoku) z pytaniem.

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

Dzięki temu widok używa określonego rodzaju pytania, które jest w stanie wyświetlić, a model pozostaje odłączony od widoku.

Fabrykę można wypełnić poprzez odbicie lub ręcznie przy uruchomieniu aplikacji.

Xtros
źródło
Jeśli korzystasz z systemu, w którym buforowanie widoku było ważne (np. Gra), fabryka może zawierać pulę pytań.
Xtros
Wydaje się to dość podobne do odpowiedzi Caleth: Nadal będziesz musiał rzucić Questionsię w, MultipleChoiceQuestionkiedy tworzyszMultipleChoiceView
Nathan Merrill
Przynajmniej w języku C # udało mi się to zrobić bez obsady. W metodzie getView, gdy tworzy instancję widoku (przez wywołanie Activator.CreateInstance (questionViewType, pytanie)), drugim parametrem CreateInstance jest parametr wysyłany do konstruktora. Mój konstruktor MultipleChoiceView akceptuje tylko pytanie MultipleChoiceQuestion. Być może po prostu przenosi rzutowanie do wewnątrz funkcji CreateInstance.
Xtros
0

Nie jestem pewien, czy liczy się to jako „unikanie sprawdzania typu”, w zależności od tego, co myślisz o odbiciu .

// 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
źródło
Zasadniczo jest to kontrola typu, ale przejście z ifkontroli typu do dictionarykontroli typu. Podobnie jak Python używa słowników zamiast instrukcji switch. To powiedziawszy, podoba mi się w ten sposób bardziej niż lista instrukcji if.
Nathan Merrill,
1
@NathanMerrill Tak. Java nie ma dobrego sposobu na utrzymanie równoległych dwóch hierarchii klas. W c ++ polecam template <typename Q> struct question_traits;z odpowiednimi specjalizacjami
Caleth
@Caleth, czy możesz dynamicznie uzyskiwać dostęp do tych informacji? Myślę, że musisz to zrobić, aby skonstruować odpowiedni typ dla danej instancji.
Winston Ewert,
Ponadto fabryka prawdopodobnie potrzebuje przesłanej do niej instancji pytania. To sprawia, że ​​ten wzór jest niestety chaotyczny, ponieważ zazwyczaj wymaga brzydkiej obsady.
Winston Ewert,