คุณสร้าง GUI สำหรับคลาส polymorphic ได้อย่างไร


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
}

นี่เป็นปัญหาที่พบบ่อยมาก มีรูปแบบการออกแบบบางอย่างที่ทำให้ฉันมีคำถามแบบ polymorphic ในขณะที่หลีกเลี่ยงรายการด้านบนหรือไม่? ความหลากหลายหรือเป็นความคิดที่ผิดในตอนแรก?


6
ไม่ใช่ความคิดที่ดีที่จะถามเกี่ยวกับสิ่งที่คุณมีปัญหา แต่สำหรับฉันคำถามนี้มักจะกว้างเกินไป / ไม่ชัดเจนและในที่สุดคุณก็ตั้งคำถาม ...
kayess

1
โดยทั่วไปแล้วฉันพยายามหลีกเลี่ยงการตรวจสอบชนิด / การพิมพ์แบบหล่อเพราะมันมักจะนำไปสู่การตรวจสอบเวลาแบบคอมไพล์น้อยลง ฉันไม่ได้ต่อต้านพวกเขาโดยพื้นฐาน แต่พยายามค้นหาวิธีที่ไม่มีพวกเขา
นาธานเมอร์ริลล์

1
สิ่งที่คุณกำลังมองหานั้นเป็นDSLสำหรับการอธิบายแม่แบบอย่างง่ายไม่ใช่แบบวัตถุแบบลำดับชั้น
user1643723

2
@NathanMerrill "ฉันต้องการ polymophism อย่างแน่นอน" - ไม่ควรเป็นอย่างนั้นใช่มั้ย คุณอยากจะบรรลุเป้าหมายที่แท้จริงของคุณหรือ "ใช้ polymophism" หรือไม่? IMO, polymophism นั้นเหมาะสมอย่างยิ่งสำหรับการสร้าง API ที่ซับซ้อนและพฤติกรรมการสร้างแบบจำลอง มันไม่เหมาะสำหรับการสร้างแบบจำลองข้อมูล (ซึ่งเป็นสิ่งที่คุณกำลังทำอยู่)
user1643723

1
@NathanMerrill "แต่ละ timeblock เรียกใช้การดำเนินการหรือมี timeblocks อื่นและเรียกใช้งานหรือเรียกใช้พรอมต์ผู้ใช้" - ข้อมูลนี้มีค่าสูงมากผมขอแนะนำให้คุณเพิ่มลงในคำถาม
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 จะต้องเพิ่มวิธีการในแต่ละครั้งที่มีคำถามประเภทอื่นซึ่งไม่สามารถปรับขนาดได้สุด ๆ
Nathan Merrill

3
@ NathanMerrill ฉันไม่คิดว่ามันจะเปลี่ยนความสามารถในการปรับขนาดของคุณได้มากนัก ใช่คุณต้องใช้วิธีการใหม่ในทุก ๆ กรณีของ QuestionVisitor แต่นั่นคือรหัสที่คุณจะต้องเขียนในกรณีใด ๆ เพื่อจัดการ GUI สำหรับประเภทคำถามใหม่ ฉันไม่คิดว่ามันจะเพิ่มรหัสจำนวนมากที่คุณไม่จำเป็นต้องทำ แต่มันจะเปลี่ยนรหัสที่หายไปให้เป็นข้อผิดพลาดในการคอมไพล์
Winston Ewert

4
จริง อย่างไรก็ตามหากฉันต้องการอนุญาตให้ใครสักคนสร้างคำถามประเภท + Renderer ของตนเอง (ซึ่งฉันทำไม่ได้) ฉันไม่คิดว่าจะเป็นไปได้
Nathan Merrill

2
@NathanMerrill นั่นเป็นเรื่องจริง วิธีนี้จะถือว่ามีเพียงหนึ่งรหัสฐานเท่านั้นคือการกำหนดประเภทของคำถาม
Winston Ewert

4
@Winston ขอยืนยันว่านี่เป็นการใช้รูปแบบของผู้เข้าชมที่ดี แต่การใช้งานของคุณไม่ได้เป็นไปตามรูปแบบ โดยปกติแล้ววิธีการในผู้มาเยี่ยมไม่ได้ตั้งชื่อตามประเภทพวกเขามักจะมีชื่อเดียวกันและแตกต่างกันในประเภทของพารามิเตอร์ (พารามิเตอร์มากไป) ชื่อสามัญคือvisit(ผู้เยี่ยมชมเยี่ยมชม) นอกจากนี้วิธีการในวัตถุที่กำลังเยี่ยมชมมักจะเรียกว่าaccept(Visitor)(วัตถุยอมรับผู้เข้าชม) ดูoodesign.com/visitor-pattern.html
Viktor Seifert

2

ใน C # / WPF (และผมคิดว่าในภาษาการออกแบบ UI ที่มุ่งเน้นและอื่น ๆ ) เรามีDataTemplates โดยการกำหนดเทมเพลตข้อมูลคุณจะสร้างการเชื่อมโยงระหว่าง "data object" ชนิดหนึ่งกับ "UI template" ที่สร้างขึ้นเป็นพิเศษเพื่อแสดงวัตถุนั้น

เมื่อคุณให้คำแนะนำสำหรับ UI ในการโหลดวัตถุชนิดใดประเภทหนึ่งมันจะดูว่ามีเทมเพลตข้อมูลใด ๆ ที่กำหนดไว้สำหรับวัตถุนั้นหรือไม่


สิ่งนี้ดูเหมือนว่าจะย้ายปัญหาไปที่ XML ซึ่งคุณเสียการพิมพ์ที่เข้มงวดทั้งหมดตั้งแต่แรก
Nathan Merrill

ฉันไม่แน่ใจว่าคุณกำลังพูดว่าเป็นสิ่งที่ดีหรือไม่ดี ในอีกด้านหนึ่งเรากำลังเคลื่อนปัญหา ในทางกลับกันดูเหมือนว่าการแข่งขันที่เกิดขึ้นในสวรรค์
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 ในคำถาม "display" และ "displayGraded" ของคุณถูกเปิดเผย: สำหรับ "display" ทุกประเภทฉันจะต้องมีฟังก์ชั่นอื่น
Nathan Merrill

ไม่มากนักนี่เป็นการอ้างอิงไปยังมุมมองที่เป็น polymorphic อาจเป็น GUI, เว็บเพจ, PDF หรืออะไรก็ได้ นี่คือพอร์ตเอาต์พุตที่ส่งเนื้อหาที่ไม่มีโครงร่าง
candied_orange

@NathanMerrill โปรดทราบการแก้ไข
candied_orange

อินเทอร์เฟซใหม่ใช้งานไม่ได้: คุณกำลังใส่ "MultipleChoiceView" ในส่วนติดต่อ "คำถาม" คุณสามารถใส่วิวเวอร์ลงในคอนสตรัคเตอร์ได้ แต่ส่วนใหญ่คุณไม่รู้ (หรือสนใจ) ว่าวิวเวอร์ใดจะเป็นเมื่อคุณสร้างวัตถุ (สามารถแก้ไขได้โดยใช้ฟังก์ชัน / โรงงานขี้เกียจ แต่ตรรกะที่อยู่เบื้องหลังการฉีดเข้าไปในโรงงานนั้นอาจทำให้เกิดความยุ่งเหยิง)
Nathan Merrill

@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" (การทดแทน Liskov ใน SOLID) โดยการลบประเภทย่อยคำถามในคลาส; และ (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 ของคุณและคุณสมบัติการพิมพ์ที่แข็งแกร่งในคลาสคำถามเฉพาะ แต่ฉันจะแทนที่คุณสมบัติเหล่านั้นด้วยคุณสมบัติทั่วไปซึ่ง GUI จะต้องเข้าถึงโดยใช้คีย์สตริงหรืออย่างอื่น (การเชื่อมต่อหลวม) นี่เป็นข้อเสียเปรียบบางทีคุณอาจไม่ต้องการแต่งงานกันในสถานการณ์นี้
Emerson Cardoso

1

โรงงานควรทำสิ่งนี้ได้ แผนที่แทนที่คำสั่ง switch ซึ่งจำเป็นเพียงเพื่อจับคู่คำถาม (ซึ่งไม่รู้อะไรเกี่ยวกับมุมมอง) กับ 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)
}

ด้วยมุมมองนี้ใช้ประเภทคำถามเฉพาะที่สามารถแสดงผลได้และโมเดลจะยังคงถูกตัดการเชื่อมต่อจากมุมมอง

โรงงานสามารถบรรจุผ่านการสะท้อนหรือด้วยตนเองเมื่อเริ่มต้นแอปพลิเคชัน


หากคุณอยู่ในระบบที่มีการแคชมุมมองสำคัญ (เช่นเกม) โรงงานอาจรวม Pool of the QuestionViews
Xtros

ดูเหมือนว่าจะค่อนข้างคล้ายกับคำตอบของ Caleth: คุณยังต้องQuestionเข้าหาMultipleChoiceQuestionเมื่อคุณสร้างMultipleChoiceView
Nathan Merrill

อย่างน้อยใน C # ฉันจัดการได้โดยไม่ต้องมีนักแสดง ในเมธอด getView เมื่อสร้างอินสแตนซ์มุมมอง (โดยการเรียก Activator.CreateInstance (questionViewType, คำถาม)) พารามิเตอร์ตัวที่สองของ 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 ที่กล่าวว่าฉันชอบวิธีนี้มากกว่ารายการถ้างบ
Nathan Merrill

1
@NathanMerrill ใช่ Java ไม่มีวิธีที่ดีในการรักษาลำดับชั้นของคลาสสองแบบขนาน ใน c ++ ฉันขอแนะนำ a template <typename Q> struct question_traits;ด้วยความเชี่ยวชาญเฉพาะด้าน
Caleth

@Caleth คุณสามารถเข้าถึงข้อมูลนั้นแบบไดนามิกได้หรือไม่ ฉันคิดว่าคุณต้องสร้างประเภทที่ถูกต้องตามตัวอย่าง
Winston Ewert

นอกจากนี้โรงงานอาจต้องการตัวอย่างคำถามผ่านไป นั่นทำให้รูปแบบนี้น่ายุ่งเหยิงเนื่องจากโดยทั่วไปแล้วมันต้องมีการร่ายที่น่าเกลียด
Winston Ewert
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.