Làm thế nào để bạn tạo một GUI cho một lớp đa hình?


17

Giả sử tôi đã có một người xây dựng bài kiểm tra, để giáo viên có thể tạo ra một loạt các câu hỏi cho bài kiểm tra.

Tuy nhiên, không phải tất cả các câu hỏi đều giống nhau: Bạn có nhiều lựa chọn, hộp văn bản, kết hợp, v.v. Mỗi loại câu hỏi này cần lưu trữ các loại dữ liệu khác nhau và cần một GUI khác nhau cho cả người tạo và người thử nghiệm.

Tôi muốn tránh hai điều:

  1. Kiểm tra loại hoặc đúc loại
  2. Bất cứ điều gì liên quan đến GUI trong mã dữ liệu của tôi.

Trong nỗ lực ban đầu của tôi, tôi kết thúc với các lớp sau:

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

Tuy nhiên, khi tôi đi hiển thị bài kiểm tra, chắc chắn tôi sẽ kết thúc bằng mã như:

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

Điều này cảm thấy như một vấn đề thực sự phổ biến. Có một số mẫu thiết kế cho phép tôi có câu hỏi đa hình trong khi tránh các mục được liệt kê ở trên? Hoặc là đa hình là ý tưởng sai ở nơi đầu tiên?


6
Không phải là ý kiến ​​tồi khi hỏi về những điều bạn gặp vấn đề, nhưng với tôi câu hỏi này có xu hướng quá rộng / không rõ ràng và cuối cùng bạn đang đặt câu hỏi cho câu hỏi ...
kayess

1
Nói chung, tôi cố gắng tránh kiểm tra kiểu / đúc kiểu vì nó thường dẫn đến kiểm tra thời gian biên dịch ít hơn và về cơ bản là "làm việc xung quanh" đa hình hơn là sử dụng nó. Tôi về cơ bản không phản đối họ, nhưng cố gắng tìm kiếm giải pháp mà không có họ.
Nathan Merrill

1
Những gì bạn đang tìm kiếm về cơ bản là một DSL để mô tả các mẫu đơn giản, không phải mô hình đối tượng phân cấp.
dùng1643723

2
@NathanMerrill "Tôi chắc chắn muốn đa hình", - đó có phải là cách khác không? Bạn có muốn đạt được mục tiêu thực tế của bạn hoặc "sử dụng đa hình"? IMO, polymophism rất phù hợp để xây dựng các API phức tạp và hành vi mô hình hóa. Nó ít phù hợp hơn cho việc mô hình hóa dữ liệu (đó là những gì bạn đang làm).
dùng1643723

1
@NathanMerrill "mỗi khung thời gian thực hiện một hành động hoặc chứa các khung thời gian khác và thực thi chúng hoặc yêu cầu nhắc nhở người dùng", - thông tin này rất có giá trị, tôi đề nghị bạn thêm nó vào câu hỏi.
dùng1643723

Câu trả lời:


15

Bạn có thể sử dụng mẫu khách truy cập:

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

interface Question {
    void visit(QuestionVisitor);
}

class MultipleChoice implements Question {

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

Một lựa chọn khác là một liên minh phân biệt đối xử. Điều này sẽ phụ thuộc rất nhiều vào ngôn ngữ của bạn. Điều này sẽ tốt hơn nhiều nếu ngôn ngữ của bạn hỗ trợ nó, nhưng nhiều ngôn ngữ phổ biến thì không.


2
Hmm .... đây không phải là một lựa chọn tồi, tuy nhiên giao diện AskVisitor sẽ cần thêm một phương thức mỗi khi có một loại câu hỏi khác nhau, không thể mở rộng được.
Nathan Merrill

3
@NathanMerrill, tôi không nghĩ nó thực sự thay đổi khả năng mở rộng của bạn nhiều. Có, bạn phải triển khai phương thức mới trong mọi trường hợp của Câu hỏi thường gặp. Nhưng đó là mã bạn sẽ phải viết trong mọi trường hợp để xử lý GUI cho loại câu hỏi mới. Tôi không nghĩ rằng nó thực sự bổ sung nhiều mã mà bạn không cần phải đúng, nhưng nó biến mã bị thiếu thành một lỗi biên dịch.
Winston Ewert

4
Thật. Tuy nhiên, nếu tôi từng muốn cho phép ai đó tạo loại Câu hỏi + Trình kết xuất của riêng họ (mà tôi không), tôi không nghĩ điều đó là có thể.
Nathan Merrill

2
@NathanMerrill, đó là sự thật. Cách tiếp cận này giả định rằng chỉ có một cơ sở mã là xác định các loại câu hỏi.
Winston Ewert

4
@WinstonEwert đây là cách sử dụng tốt mẫu khách truy cập. Nhưng việc thực hiện của bạn không hoàn toàn theo mô hình. Thông thường các phương thức trong khách truy cập không được đặt tên theo các loại, chúng thường có cùng tên và chỉ khác nhau về các loại tham số (nạp chồng tham số); tên phổ biến là visit(khách truy cập). Ngoài ra phương thức trong các đối tượng được truy cập thường được gọi accept(Visitor)(đối tượng chấp nhận một khách truy cập). Xem oodesign.com/visitor-potype.html
Viktor Seifert

2

Trong C # / WPF (và, tôi tưởng tượng, trong các ngôn ngữ thiết kế tập trung vào UI khác), chúng tôi có DataTemsheet . Bằng cách xác định các mẫu dữ liệu, bạn tạo liên kết giữa một loại "đối tượng dữ liệu" và "mẫu UI" chuyên dụng được tạo riêng để hiển thị đối tượng đó.

Khi bạn cung cấp hướng dẫn cho UI để tải một loại đối tượng cụ thể, nó sẽ xem liệu có bất kỳ mẫu dữ liệu nào được xác định cho đối tượng không.


Điều này dường như đang chuyển vấn đề sang XML khi bạn mất tất cả các kiểu gõ nghiêm ngặt ngay từ đầu.
Nathan Merrill

Tôi không chắc bạn đang nói đó là điều tốt hay điều xấu. Một mặt, chúng tôi đang chuyển vấn đề. Mặt khác, nó giống như một trận đấu được thực hiện trên thiên đường.
BTownTKD

2

Nếu mọi câu trả lời có thể được mã hóa thành một chuỗi, bạn có thể làm điều này:

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

Trường hợp chuỗi trống biểu thị một câu hỏi chưa có câu trả lời nào. Điều này cho phép tách biệt các câu hỏi, câu trả lời và GUI cho phép đa hình.

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

Hộp văn bản, khớp, v.v có thể có thiết kế tương tự, tất cả đều thực hiện giao diện câu hỏi. Việc xây dựng chuỗi câu trả lời xảy ra trong khung nhìn. Chuỗi câu trả lời đại diện cho trạng thái của bài kiểm tra. Chúng nên được lưu trữ khi học sinh tiến bộ. Áp dụng chúng cho các câu hỏi cho phép hiển thị bài kiểm tra và trạng thái theo cả cách phân loại và không được phân loại.

Bằng cách tách đầu ra thành display()displayGraded()khung nhìn không cần phải hoán đổi và không cần thực hiện phân nhánh trên các tham số. Tuy nhiên, mỗi chế độ xem được tự do sử dụng lại càng nhiều logic hiển thị càng tốt khi hiển thị. Bất cứ kế hoạch nào được đưa ra để làm điều đó không cần phải rò rỉ vào mã này.

Tuy nhiên, nếu bạn muốn có quyền kiểm soát năng động hơn về cách hiển thị một câu hỏi, bạn có thể làm điều này:

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

và điều này

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

Điều này có nhược điểm là nó yêu cầu các chế độ xem không có ý định hiển thị score()hoặc answerKeyphụ thuộc vào chúng khi chúng không cần chúng. Nhưng điều đó có nghĩa là bạn không phải xây dựng lại các câu hỏi kiểm tra cho từng loại chế độ xem bạn muốn sử dụng.


Vì vậy, điều này đặt mã GUI trong Câu hỏi. "Hiển thị" và "displayGraded" của bạn đang được tiết lộ: Đối với mỗi loại "màn hình", tôi sẽ phải có một chức năng khác.
Nathan Merrill

Không hoàn toàn, điều này đặt một tham chiếu đến một quan điểm là đa hình. Nó MIGHT là GUI, trang web, PDF, bất cứ điều gì. Đây là một cổng đầu ra được gửi nội dung miễn phí bố trí.
candied_orange

@NathanMerrill vui lòng lưu ý chỉnh sửa
candied_orange

Giao diện mới không hoạt động: Bạn đang đặt "NhiềuChoiceView" bên trong giao diện "Câu hỏi". Bạn có thể đặt trình xem vào hàm tạo, nhưng hầu hết thời gian bạn không biết (hoặc quan tâm) trình xem sẽ là gì khi bạn tạo đối tượng. (Điều đó có thể được giải quyết bằng cách sử dụng chức năng / nhà máy lười biếng nhưng logic đằng sau việc bơm vào nhà máy đó có thể trở nên lộn xộn)
Nathan Merrill

@NathanMerrill Một cái gì đó, ở đâu đó phải biết nơi này có nghĩa là được hiển thị. Điều duy nhất mà nhà xây dựng làm là cho phép bạn quyết định điều này tại thời điểm xây dựng và sau đó quên nó đi. Nếu bạn không muốn quyết định điều này khi thi công thì bạn phải quyết định sau và bằng cách nào đó hãy nhớ quyết định đó cho đến khi bạn gọi hiển thị. Sử dụng các nhà máy trong các phương pháp này sẽ không thay đổi những sự thật này. Nó chỉ che giấu cách bạn đưa ra quyết định. Thường không phải là một cách tốt.
candied_orange

1

Theo tôi, nếu bạn cần một tính năng chung như vậy, tôi sẽ giảm sự ghép nối giữa các thứ trong mã. Tôi sẽ cố gắng xác định loại Câu hỏi càng chung chung càng tốt, và sau đó tôi sẽ tạo các lớp khác nhau cho các đối tượng kết xuất. Xin vui lòng, xem các ví dụ dưới đây:

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

Sau đó, đối với phần kết xuất, tôi đã loại bỏ kiểm tra Loại bằng cách thực hiện kiểm tra đơn giản về dữ liệu trong đối tượng câu hỏi. Đoạn mã dưới đây cố gắng thực hiện hai điều: (i) tránh kiểm tra kiểu và tránh vi phạm nguyên tắc "L" (thay thế Liskov trong RẮN) bằng cách xóa phân nhóm lớp Câu hỏi; và (ii) làm cho mã có thể mở rộng, bằng cách không bao giờ thay đổi mã kết xuất lõi bên dưới, chỉ cần thêm các triển khai Câu hỏi và các phiên bản của nó vào mảng (đây thực sự là nguyên tắc "O" trong RẮN - mở để mở rộng và đóng để sửa đổi).

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

Điều gì xảy ra khi ManyChoiceQuestionView cố gắng truy cập vào trường NhiềuChoice.choices? Nó đòi hỏi một diễn viên. Chắc chắn, nếu chúng ta giả sử câu hỏi đó. Loại là duy nhất và mã là lành mạnh, đây là một diễn viên khá an toàn, nhưng nó vẫn là một diễn viên: P
Nathan Merrill

Nếu bạn lưu ý trong ví dụ của tôi, không có loại MultiChoice nào như vậy. Chỉ có một loại Câu hỏi mà tôi đã cố gắng xác định một cách khái quát, với một danh sách thông tin (bạn có thể lưu trữ nhiều lựa chọn trong danh sách này, bạn có thể định nghĩa nó như bạn muốn). Do đó, không có diễn viên, bạn chỉ có Câu hỏi loại một và nhiều đối tượng kiểm tra xem họ có thể đưa ra câu hỏi này không, nếu đối tượng hỗ trợ nó, thì bạn có thể gọi phương thức kết xuất một cách an toàn.
Emerson Cardoso

Trong ví dụ của tôi, tôi đã chọn giảm khớp nối giữa GUI của bạn và các thuộc tính được gõ mạnh trong phân vùng Câu hỏi cụ thể; thay vào đó, tôi thay thế các thuộc tính đó bằng các thuộc tính chung, mà GUI sẽ cần truy cập bằng khóa chuỗi hoặc thứ gì khác (khớp nối lỏng lẻo). Đây là một sự đánh đổi, có lẽ khớp nối lỏng lẻo này không được mong muốn trong kịch bản của bạn.
Emerson Cardoso

1

Một nhà máy sẽ có thể làm điều này. Bản đồ thay thế câu lệnh chuyển đổi, chỉ cần để ghép Câu hỏi (không biết gì về chế độ xem) với Câu hỏi.

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

Với chế độ xem này, sử dụng loại Câu hỏi cụ thể mà nó có thể hiển thị và mô hình vẫn bị ngắt kết nối khỏi chế độ xem.

Nhà máy có thể được tạo ra thông qua sự phản chiếu hoặc thủ công khi bắt đầu ứng dụng.


Nếu bạn đang ở trong một hệ thống mà bộ nhớ đệm quan sát là quan trọng (như trò chơi), thì nhà máy có thể bao gồm Nhóm các Câu hỏi.
Xtros

Điều này có vẻ khá giống với câu trả lời của Caleth: Bạn vẫn đang đi đến cần phải cast Questionvào một MultipleChoiceQuestionkhi bạn tạoMultipleChoiceView
Nathan Merrill

Trong C # ít nhất, tôi đã xoay sở để làm điều này mà không cần diễn viên. Trong phương thức getView, khi nó tạo ra thể hiện khung nhìn (bằng cách gọi Activator.CreateInstance (questionViewType, question)), tham số thứ hai của CreateInstance là tham số được gửi đến hàm tạo. Trình xây dựng ManyChoiceView của tôi chỉ chấp nhận một MultiChoiceQuestion. Có lẽ nó chỉ di chuyển các diễn viên vào bên trong chức năng CreatInstance.
Xtros

0

Tôi không chắc chắn điều này được tính là "tránh kiểm tra loại", tùy thuộc vào cách bạn cảm nhận về sự phản chiếu .

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

Về cơ bản, đây là kiểm tra loại, nhưng chuyển từ ifkiểm tra loại sang dictionarykiểm tra loại. Giống như cách Python sử dụng từ điển thay vì câu lệnh switch. Điều đó nói rằng, tôi thích cách này hơn là một danh sách các câu lệnh if.
Nathan Merrill

1
@NathanMerrill Vâng. Java không có cách tốt để giữ song song hai hệ thống phân cấp lớp. Trong c ++, tôi muốn giới thiệu một template <typename Q> struct question_traits;chuyên ngành phù hợp
Caleth

@Caleth, bạn có thể truy cập thông tin đó một cách linh hoạt không? Tôi nghĩ rằng bạn phải xây dựng đúng loại được đưa ra.
Winston Ewert

Ngoài ra, nhà máy có thể cần ví dụ câu hỏi được chuyển cho nó. Điều đó làm cho mô hình này không may lộn xộn, vì nó thường đòi hỏi một dàn diễn viên xấu xí.
Winston Ewert
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.