ポリモーフィッククラスのGUIはどのように作成しますか?


17

テストビルダーがあり、教師がテスト用の一連の質問を作成できるようにしたとします。

ただし、すべての質問が同じというわけではありません。複数の選択肢、テキストボックス、一致などがあります。これらの質問タイプはそれぞれ、異なるタイプのデータを保存する必要があり、作成者と受験者の両方に異なるGUIが必要です。

私は2つのことを避けたいです:

  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

回答:


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、それは本当です。このアプローチでは、1つのコードベースのみが質問タイプを定義していると想定しています。
ウィンストンユワート

4
@WinstonEwertこれは、訪問者パターンの適切な使用方法です。しかし、実装はパターンに完全には一致していません。通常、ビジター内のメソッドはタイプにちなんで命名されず、通常同じ名前を持ち、パラメーターのタイプのみが異なります(パラメーターのオーバーロード)。一般名はvisit(訪問者が訪問する)です。また、通常、訪問中のオブジェクトのメソッドが呼び出されますaccept(Visitor)(オブジェクトは訪問者を受け入れます)。参照してくださいoodesign.com/visitor-pattern.html
ヴィクトル・ザイフェルト

2

C#/ WPF(および、私が想像するように、他のUI中心の設計言語)には、DataTemplatesがあります。データテンプレートを定義することにより、1つのタイプの「データオブジェクト」と、そのオブジェクトを表示するために特別に作成された特殊な「UIテンプレート」との間に関連付けを作成します。

特定の種類のオブジェクトを読み込むためのUIの指示を提供すると、オブジェクトにデータテンプレートが定義されているかどうかがわかります。


これにより、問題が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コードを入れます。あなたの「ディスプレイ」と「displayGraded」は明らかになっています。「ディスプレイ」のすべてのタイプについて、別の機能が必要です。
ネイサンメリル

完全ではありませんが、これはポリモーフィックなビューへの参照を置きます。GUI、Webページ、PDFなど、どんなものでもかまいません。これは、レイアウトのないコンテンツが送信される出力ポートです。
candied_orange

@NathanMerrill編集に注意してください
candied_orange

新しいインターフェースは機能しません。「Question」インターフェース内に「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
}

次に、レンダリング部分について、質問オブジェクト内のデータに簡単なチェックを実装することにより、タイプチェックを削除しました。以下のコードは、2つのことを達成しようとします。(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はありません。質問のタイプは1つだけで、一般に定義しようとした情報のリストがあります(このリストに複数の選択肢を格納できます。必要に応じて定義できます)。したがって、キャストはなく、1つのタイプの質問と、この質問をレンダリングできるかどうかをチェックする複数のオブジェクトがあり、オブジェクトがそれをサポートしている場合、レンダリングメソッドを安全に呼び出すことができます。
エマーソンカルドーソ

私の例では、GUIと特定のQuestionクラスの強い型付けされたプロパティとの結合を減らすことを選択しました。代わりに、これらのプロパティを汎用プロパティに置き換えます。GUIは、文字列キーまたは他の何か(疎結合)によってアクセスする必要があります。これはトレードオフであり、おそらくこの疎結合はシナリオでは望ましくありません。
エマーソンカルドーソ

1

工場はこれを行うことができるはずです。マップはswitchステートメントを置き換えます。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の答えにかなり似ているようだ:あなたはまだキャストする必要があるとしているQuestionMultipleChoiceQuestionは、作成MultipleChoiceView
ネイサンメリル

少なくともC#では、キャストなしでこれを行うことができました。getViewメソッドでは、(Activator.CreateInstance(questionViewType、question)を呼び出して)ビューインスタンスを作成するとき、CreateInstanceの2番目のパラメーターはコンストラクターに送信されるパラメーターです。私の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には、2つのクラス階層を並行して保持する優れた方法がありません。C ++ではtemplate <typename Q> struct question_traits;、適切な専門分野をお勧めします
カレス

@Caleth、その情報に動的にアクセスできますか?インスタンスが与えられた場合、正しい型を構築するためにあなたがしなければならないと思う。
ウィンストンユワート

また、ファクトリにはおそらく渡された質問インスタンスが必要です。通常、いキャストが必要なため、残念ながらこのパターンは面倒です。
ウィンストンイーバート
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.