Як зробити графічний інтерфейс для поліморфного класу?


17

Скажімо, у мене є конструктор тестів, щоб вчителі могли створити купу питань для тесту.

Однак не всі питання однакові: у вас є вибір декількох, текстове поле, відповідність тощо. Кожен із цих типів запитань повинен зберігати різні типи даних та потребувати різного графічного інтерфейсу як для творця, так і для учасника тесту.

Я хотів би уникати двох речей:

  1. Чекові типи або кастинг типів
  2. Все, що стосується графічного інтерфейсу в моєму коді даних.

У першій моїй спробі я закінчую такі класи:

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

Однак, коли я йду показувати тест, я неминуче закінчуюсь кодом типу:

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

Це відчувається як справді поширена проблема. Чи існує якась схема дизайну, яка дозволяє мені мати поліморфні питання, уникаючи перелічених вище предметів? Або поліморфізм - це неправильна ідея в першу чергу?


6
Непогано запитувати про речі, з якими у вас є проблеми, але мені це питання, як правило, занадто широке / незрозуміле, і, нарешті, ви ставите питанням ...
kayess

1
Взагалі, я намагаюся уникати перевірки типів / кастингу типів, оскільки це, як правило, призводить до меншої перевірки часу компіляції і в основному "обходить" поліморфізм, а не використовувати його. Я принципово не проти них, але намагаюся шукати рішення без них.
Натан Меррілл

1
Що ви шукаєте, це в основному DSL для опису простих шаблонів, а не ієрархічної об'єктної моделі.
user1643723

2
@NathanMerrill "Я, безумовно, хочу полімофізм", - чи не повинно бути навпаки? Ви б скоріше досягли своєї реальної мети чи «використали полімофізм»? ІМО, полімофізм добре підходить для побудови складних API та моделювання поведінки. Він менш придатний для моделювання даних (це те, що ви зараз робите).
user1643723

1
@NathanMerrill "кожен таймблок виконує дію, або містить інші часові блоки та виконує їх, або вимагає підказки користувача", - ця інформація є дуже цінною, я пропоную вам додати її до питання.
user1643723

Відповіді:


15

Ви можете використовувати шаблон відвідувача:

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

interface Question {
    void visit(QuestionVisitor);
}

class MultipleChoice implements Question {

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

Інший варіант - дискримінаційний союз. Це дуже залежатиме від вашої мови. Це набагато краще, якщо ваша мова це підтримує, але багато популярних мов цього не роблять.


2
Хм .... це не страшний варіант, проте інтерфейс QuestionVisitor повинен буде додавати метод щоразу, коли виникає інший тип запитань, що не є надзвичайно масштабованим.
Натан Меррілл

3
@NathanMerrill, я не думаю, що це насправді сильно змінює вашу масштабованість. Так, вам доведеться впроваджувати новий метод у кожному екземплярі QuestionVisitor. Але це код, який вам доведеться написати в будь-якому випадку, щоб обробити графічний інтерфейс для нового типу питань. Я не думаю, що це дійсно додає багато коду, який ви інакше не мали б права, але він перетворює пропущений код у помилку компіляції.
Вінстон Еверт

4
Правда. Однак, якби я коли-небудь хотів дозволити комусь зробити свій власний тип питання + рендерінг (якого я ні), я не думаю, що це було б можливим.
Натан Меррілл

2
@NathanMerrill, це правда. Цей підхід передбачає, що лише одна база коду визначає типи питань.
Вінстон Еверт

4
@WinstonEwert це добре використовувати шаблон відвідувачів. Але ваша реалізація не зовсім відповідає шаблону. Зазвичай методи у відвідувача не називають типи, вони зазвичай мають одне ім’я та відрізняються лише типами параметрів (перевантаження параметрів); загальна назва visit(відвідувачі відвідують). Також зазвичай називається метод в об'єктах, які відвідуються accept(Visitor)(об'єкт приймає відвідувача). Дивіться oodesign.com/visitor-pattern.html
Віктор Сейферт

2

У C # / WPF (і, я думаю, в інших мовах дизайну, орієнтованих на інтерфейс користувача), у нас є DataTemplates . Визначаючи шаблони даних, ви створюєте зв'язок між одним типом "об'єкта даних" та спеціалізованим "шаблоном інтерфейсу", створеним спеціально для відображення цього об'єкта.

Щойно ви надаєте інструкції для інтерфейсу користувача для завантаження певного виду об’єкта, він побачить, чи є певні шаблони даних, визначені для об'єкта.


Це, здається, переносить проблему на XML, де ви втрачаєте в першу чергу суворе введення тексту.
Натан Меррілл

Я не впевнений, чи ти кажеш, що це гарна чи погана річ. З одного боку, ми рухаємо проблему. З іншого боку, це звучить як сірник, зроблений на небі.
BTownTKD

2

Якщо кожну відповідь можна закодувати як рядок, ви можете зробити це:

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

Там, де порожній рядок означає, це питання, поки на нього немає відповіді. Це дозволяє відокремити питання, відповіді та графічний інтерфейс, але це дозволяє поліморфізм.

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

Текстове поле, відповідність тощо може мати подібні конструкції, які реалізують інтерфейс питання. Побудова рядка відповідей відбувається у вікні перегляду. Рядок відповідей представляє стан тесту. Вони повинні зберігатися по мірі прогресування учня. Застосування їх до питань дозволяє відображати тест і його стан як в градуйованому, так і в неоціненому вигляді.

Відокремлюючи вихідний display()і displayGraded()перегляд, не потрібно замінювати і не потрібно робити розгалуження на параметри. Однак кожне представлення може використовувати повторно стільки логіки відображення, скільки може під час показу. Яка б схема не була створена, не потрібно просочуватися в цей код.

Якщо ви хочете мати більш динамічний контроль над тим, як відображати питання, ви можете зробити це:

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

і це

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

У цього є недолік, що він вимагає представлень, які не мають наміру відображати score()або answerKeyзалежати від них, коли вони їм не потрібні. Але це означає, що вам не доведеться перебудовувати тестові питання для кожного типу перегляду, який ви хочете використовувати.


Таким чином, це ставить код GUI у питанні. Ваші "display" та "displayGraded" виявляють: Для кожного типу "дисплея" я повинен мати іншу функцію.
Натан Меррілл

Не зовсім, це ставить посилання на погляд, який є поліморфним. Це МОЖЕ бути графічним інтерфейсом, веб-сторінкою, PDF, будь-яким іншим. Це вихідний порт, який надсилається вмістом без макета.
candied_orange

@NathanMerrill зверніть увагу на редагування
candied_orange

Новий інтерфейс не працює: ви кладете "MultipleChoiceView" всередину інтерфейсу "Питання". Ви можете помістити глядача в конструктор, але більшу частину часу ви не знаєте (чи не піклуєтесь), який буде переглядач, коли ви робите об'єкт. (Це можна вирішити за допомогою ледачої функції / фабрики, але логіка введення в цю фабрику може стати безладною)
Натан Меррілл

@NathanMerrill Щось, десь треба знати, де це має бути відображено. Єдине, що робить конструктор - це дати вам вирішити це під час будівництва, а потім забути про це. Якщо ви не хочете вирішувати це на будівництві, тоді ви повинні прийняти рішення пізніше і якось запам'ятати це рішення, поки ви не зателефонуєте на дисплей. Використання фабрик у цих методах не змінило б цих фактів. Це просто приховує, як ви прийняли рішення. Зазвичай не в хороший спосіб.
candied_orange

1

На мою думку, якщо вам потрібна така загальна функція, я б зменшив зв'язок між матеріалами в коді. Я б спробував визначити тип питання як більш загальним, і після цього я створив би різні класи для об'єктів візуалізації. Будь ласка, дивіться приклади нижче:

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

Потім для частини візуалізації я видалив перевірку типу, здійснивши просту перевірку даних усередині об’єкта запитання. Нижче наведений код намагається виконати дві речі: (i) уникнути перевірки типу та уникнути порушення принципу "L" (заміна Ліскова на SOLID) шляхом видалення підтипу класу Question; та (ii) зробити код розширюваним, ніколи не змінюючи основний код візуалізації нижче, просто додаючи до масиву більше реалізацій QuestionView та його екземпляри (це насправді принцип "O" у SOLID - відкритий для розширення та закритий для модифікації).

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

Що відбувається, коли MultipleChoiceQuestionView намагається отримати доступ до поля MultipleChoice.choices? Тут потрібен акторський склад. Звичайно, якщо припустити, що це питання. Тип унікальний, і код є здоровим, його досить безпечний склад, але це все-таки акторський склад: P
Nathan Merrill

Якщо ви зауважили у моєму прикладі, не існує такого типу MultipleChoice. Існує лише один тип запитання, який я намагався визначити загально, зі списком інформації (ви можете зберігати кілька варіантів у цьому списку, ви можете визначити його так, як хочете). Тому немає жодного складу, у вас є лише питання одного типу, і кілька об'єктів, які перевіряють, чи можуть вони задати це питання, якщо об'єкт підтримує його, то можете сміливо викликати метод візуалізації.
Emerson Cardoso

У своєму прикладі я вирішив зменшити зв’язок між вашим графічним інтерфейсом та сильними типізованими властивостями у конкретному класі запитань; замість цього я замінюю ці властивості на загальні властивості, до яких GUI повинен мати доступ за допомогою рядкового ключа або чогось іншого (вільне з'єднання). Це компромісне рішення, можливо, ця нещільна муфта не бажана у вашому сценарії.
Емерсон Кардосо

1

Фабрика повинна це робити. Карта замінює оператор перемикання, який потрібен виключно для того, щоб зв'язати питання (який нічого не знає про подання) з 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)
}

При цьому представлення використовує певний тип запитань, який він може відображати, а модель залишається відключеною від подання.

Фабрику можна заповнити за допомогою відображення або вручну при запуску програми.


Якщо ви працювали в системі, де кешування подання було важливим (як гра), фабрика могла б включати пул QuestionViews.
Xtros

Це здається досить схожим на відповідь Калета: Вам все одно потрібно буде кинути участь Questionу MultipleChoiceQuestionстворенніMultipleChoiceView
Натан Меррілл

Принаймні, на C # мені вдалося це зробити без акторів. У методі getView, коли він створює екземпляр перегляду (викликаючи Activator.CreateInstance (questionViewType, question)), другим параметром CreateInstance є параметр, надісланий конструктору. Мій конструктор MultipleChoiceView приймає лише MultipleChoiceQuestion. Можливо, це просто переміщення кастингу до функції CreateInstance.
Xtros

0

Я не впевнений, що це розцінюється як "уникнення перевірки типу", залежно від того, як ви ставитесь до роздумів .

// 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()]());
}

Це в основному перевірка типу, але перехід від ifперевірки типу до dictionaryперевірки типу. Як, наприклад, як Python використовує словники замість операторів перемикання. Тим НЕ менше, мені подобається цей шлях більше , ніж список , якщо заяви.
Натан Меррілл

1
@NathanMerrill Так. У Java немає приємного способу паралельного збереження двох ієрархій класів. В template <typename Q> struct question_traits;
мові

@Caleth, чи можете ви динамічно отримати доступ до цієї інформації? Думаю, вам доведеться побудувати потрібний тип, заданий екземпляр.
Вінстон Еверт

Також завод, ймовірно, потребує переданого йому екземпляра питання. Це робить, на жаль, цей зразок безладним, оскільки він, як правило, вимагає потворної ролі.
Вінстон Еверт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.