教師がテスト用にたくさんの質問を作成できるように、テストビルダーがあるとします。
ただし、すべての質問が同じであるとは限りません。複数の選択肢、テキストボックス、マッチングなどがあります。これらの各質問タイプは、異なるタイプのデータを保存する必要があり、作成者と受験者の両方に異なるGUIが必要です。
私は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
}
これは本当によくある問題のように感じます。上記の項目を回避しながら、多態的な質問をすることができるいくつかのデザインパターンはありますか?または、そもそもポリモーフィズムは間違った考えですか?
ビジターパターンを使用できます。
interface QuestionVisitor {
void multipleChoice(MultipleChoice);
void textBox(TextBox);
...
}
interface Question {
void visit(QuestionVisitor);
}
class MultipleChoice implements Question {
void visit(QuestionVisitor visitor) {
visitor.multipleChoice(this);
}
}
別のオプションは、差別された組合です。これはあなたの言語に大きく依存します。あなたの言語がそれをサポートしている場合、これははるかに優れていますが、多くの一般的な言語はサポートしていません。
すべての回答を文字列としてエンコードできる場合、これを行うことができます:
_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
を表示するつもりのないビューが、それらを必要としないときにそれらに依存する必要があるという欠点があります。ただし、使用するビューの種類ごとにテスト問題を再構築する必要はありません。
C#/ WPF(および、私が想像しているように、他のUIに焦点を当てたデザイン言語)では、 DataTemplates があります。データテンプレートを定義することにより、1種類の「データオブジェクト」と、そのオブジェクトを表示するために特別に作成された専用の「UIテンプレート」との間に関連付けを作成します。
UIが特定の種類のオブジェクトをロードするための指示を提供すると、オブジェクトに対して定義されたデータテンプレートがあるかどうかが確認されます。
私の意見では、このような一般的な機能が必要な場合は、コード内の要素間の結合を減らします。私はできる限り一般的な質問タイプを定義しようとし、その後、レンダラーオブジェクト用に異なるクラスを作成します。以下の例をご覧ください。
///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)質問タイプのサブタイプを削除することにより、型チェックを回避し、「L」原理(SOLIDでのLiskov置換)の違反を回避します。 (ii)以下のコアレンダリングコードを変更せずに、コードに拡張性を持たせる。QuestionViewの実装とそのインスタンスを配列に追加するだけです(これは、実際には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));
}
}
}
工場はこれを行うことができるはずです。マップは、ビュー(ビューについて何も認識していない)とQuestionViewをペアにするためにのみ必要なswitchステートメントを置き換えます。
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)
}
これにより、ビューは表示可能な特定のタイプの質問を使用し、モデルはビューから切断されたままになります。
ファクトリーは、リフレクションを介して、またはアプリケーションの開始時に手動で設定できます。
reflection についてどのように感じているかによって、これが「型チェックを回避する」と見なされるかどうかはわかりません。
// 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()]());
}