如何为多态类创建GUI?


17

假设我有一个测试构建器,以便老师可以为测试创建很多问题。

但是,并非所有问题都相同:您有多项选择,文本框,匹配项等等。这些问题类型中的每一个都需要存储不同类型的数据,并且对于创建者和应试者都需要不同的GUI。

我想避免两件事:

  1. 类型检查或类型转换
  2. 我的数据代码中与GUI有关的所有内容。

在最初的尝试中,我最终获得了以下课程:

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“我绝对想要多语言学”,-难道不是相反吗?您是要实现自己的实际目标还是“使用多语言”?IMO,多态性非常适合构建复杂的API和建模行为。它不太适合建模数据(这是您当前正在执行的操作)。
user1643723

1
@NathanMerrill“每个时间块执行一个动作,或者包含其他时间块并执行它们,或者请求用户提示”,—我建议将此信息添加到问题中,这非常有价值。
user1643723

Answers:


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的每个实例中实现新方法。但这是您在任何情况下都必须编写的代码,以处理新问题类型的GUI。我不认为它确实会增加很多本来不需要的代码,但是它将丢失的代码变成了编译错误。
温斯顿·埃韦特

4
真正。但是,如果我曾经想让某人创建自己的“问题类型” +“渲染器”(我不愿意),我认为那是不可能的。
内森·美林

2
@NathanMerrill,是的。这种方法假定只有一个代码库在定义问题类型。
温斯顿·埃维尔

4
@WinstonEwert,这是对访客模式的很好使用。但是您的实现并不完全符合该模式。通常,访问者中的方法不是以类型命名的,它们通常具有相同的名称,只是在参数的类型上有所不同(参数重载)。通用名称是visit(访客访问)。此外,通常会调用被访问对象中的方法accept(Visitor)(该对象接受访问者)。参见oodesign.com/visitor-pattern.html
维克托·塞弗特

2

在C#/ WPF中(我想,在其他以UI为中心的设计语言中),我们有DataTemplates。通过定义数据模板,可以在一种类型的“数据对象”和专门为显示该对象而创建的专用“ UI模板”之间创建关联。

一旦为UI提供了加载特定种类的对象的说明,它就会查看是否为该对象定义了任何数据模板。


这似乎将问题转移到XML上,在XML中您首先失去了所有严格的键入。
内森·美林

我不确定您是说是好事还是坏事。一方面,我们正在解决问题。另一方面,这听起来像是天上的火柴。
BTownTKD

2

如果每个答案都可以编码为字符串,则可以执行以下操作:

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

空字符串表示问题所在,尚无答案。这样可以将问题,答案和GUI分开,但允许多态。

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代码放入Question中。您的“显示”和“ displayGraded”正在显示:对于每种类型的“显示”,我都必须具有另一个功能。
内森·美林

不完全是,这引用了一个多态的视图。它可能是GUI,网页,PDF等。这是一个输出端口,正在发送自由布局的内容。
candied_orange

@NathanMerrill请注意编辑
candied_orange

新界面不起作用:您正在将“ MultipleChoiceView”放入“问题”界面中。您可以将查看器放入构造函数中,但是大多数情况下,您不知道(或不在乎)创建对象时将使用哪个查看器。(可以通过使用惰性函数/工厂来解决,但注入该工厂的逻辑可能会变得混乱)
Nathan Merrill

@NathanMerrill某处,某处必须知道该在何处显示。构造函数所做的唯一一件事就是让您在构造时决定这一点,然后将其忽略。如果您不想在施工时决定这一点,则必须稍后再决定,并以某种方式记住该决定,直到致电display为止。在这些方法中使用工厂不会改变这些事实。它只是隐藏了您如何做出决定。通常情况不是很好。
candied_orange

1

我认为,如果您需要这种通用功能,则可以减少代码中各个内容之间的耦合。我将尝试尽可能定义更通用的Question类型,然后为渲染器对象创建不同的类。请参阅以下示例:

///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)通过除去Question类的子类型,避免类型检查并避免违反“ L”原则(SOLID中的Liskov替换);(ii)通过从不更改下面的核心渲染代码,而只是将更多QuestionView实现及其实例添加到数组中来使代码可扩展(这实际上是SOLID中的“ O”原理-为扩展而开放,为修改而封闭)。

///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时会发生什么?它需要强制转换。当然,如果我们假设question.Type是唯一的,并且代码是合理的,则它是非常安全的强制转换,但仍然是强制转换:P
Nathan Merrill

如果在我的示例中注明,则没有此类MultipleChoice。只有一个类型为Question的问题,我尝试用一​​种信息列表进行一般定义(您可以在此列表中存储多个选择,也可以根据需要定义它)。因此,没有强制转换,您只有一个类型的Question,并且有多个对象检查它们是否可以呈现此问题,如果该对象支持,则可以安全地调用呈现方法。
艾默生·卡多佐

在我的示例中,我选择减少特定的Question类中的GUI与强类型属性之间的耦合。取而代之的是,我将这些属性替换为通用属性,而GUI需要通过字符串键或其他方式(松耦合)来访问这些属性。这是一个折衷,也许您的方案中不需要这种松散的耦合。
艾默生·卡多佐

1

工厂应该能够做到这一点。该映射将替换switch语句,该语句仅用于将Question(对视图一无所知)与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

这似乎与Caleth的回答非常相似:创建QuestionMultipleChoiceQuestionMultipleChoiceView
Nathan Merrill

至少在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如何使用字典而不是switch语句一样。就是说,我更喜欢这种方式,而不是一系列if语句。
内森·梅里尔

1
@NathanMerrill是的。Java没有让两个类层次结构保持并行的好方法。在c ++中,我建议您template <typename Q> struct question_traits;使用适当的专业
知识-Caleth

@Caleth,您可以动态访问该信息吗?我认为您必须为了给定实例构造正确的类型。
温斯顿·埃维尔

另外,工厂可能需要将问题实例传递给它。不幸的是,这使该模式变得凌乱,因为它通常需要丑陋的投射。
温斯顿·埃维尔
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.