Bagaimana Anda membuat GUI untuk kelas polimorfik?

17

Katakanlah saya memiliki pembuat ujian, sehingga guru dapat membuat banyak pertanyaan untuk ujian.

Namun, tidak semua pertanyaan sama: Anda memiliki banyak pilihan, kotak teks, pencocokan, dan sebagainya. Setiap jenis pertanyaan ini perlu menyimpan berbagai jenis data, dan memerlukan GUI yang berbeda untuk pencipta dan untuk peserta tes.

Saya ingin menghindari dua hal:

  1. Ketik cek atau ketik casting
  2. Apa pun yang terkait dengan GUI dalam kode data saya.

Dalam upaya awal saya, saya berakhir dengan kelas-kelas berikut:

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

Namun, ketika saya pergi untuk menampilkan tes, saya pasti akan berakhir dengan kode seperti:

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

Ini terasa seperti masalah yang sangat umum. Apakah ada pola desain yang memungkinkan saya memiliki pertanyaan polimorfik sambil menghindari item yang tercantum di atas? Atau apakah polimorfisme ide yang salah sejak awal?

Nathan Merrill
sumber
6
Bukan ide yang buruk untuk bertanya tentang hal-hal yang bermasalah dengan Anda, tetapi bagi saya pertanyaan ini cenderung terlalu luas / tidak jelas dan akhirnya Anda mempertanyakan pertanyaan ...
kayess
1
Secara umum, saya mencoba untuk menghindari pengecekan tipe / pengecoran tipe karena umumnya mengarah ke pengecekan waktu kompilasi yang lebih sedikit dan pada dasarnya "mengatasi" polimorfisme daripada menggunakannya. Saya pada dasarnya tidak menentang mereka, tetapi mencoba mencari solusi tanpa mereka.
Nathan Merrill
1
Apa yang Anda cari pada dasarnya adalah DSL untuk menggambarkan template sederhana, bukan model objek hierarkis.
user1643723
2
@NathanMerrill "Saya pasti ingin polimofisme", - bukankah seharusnya sebaliknya? Apakah Anda lebih suka mencapai tujuan Anda yang sebenarnya atau "menggunakan polimofisme"? IMO, polimofisme sangat cocok untuk membangun API yang kompleks dan perilaku pemodelan. Ini kurang cocok untuk memodelkan data (yang saat ini Anda lakukan).
user1643723
1
@NathanMerrill "setiap kunci waktu menjalankan suatu tindakan, atau berisi kunci waktu lain dan mengeksekusi mereka, atau meminta prompt pengguna", - informasi ini sangat berharga, saya sarankan, bahwa Anda menambahkannya ke pertanyaan.
user1643723

Jawaban:

15

Anda dapat menggunakan pola pengunjung:

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

interface Question {
    void visit(QuestionVisitor);
}

class MultipleChoice implements Question {

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

Pilihan lain adalah serikat terdiskriminasi. Ini akan sangat tergantung pada bahasa Anda. Ini jauh lebih baik jika bahasa Anda mendukungnya, tetapi banyak bahasa populer tidak.

Winston Ewert
sumber
2
Hmm .... ini bukan pilihan yang mengerikan, namun antarmuka QuestionVisitor perlu menambahkan metode setiap kali ada jenis pertanyaan yang berbeda, yang tidak super skalabel.
Nathan Merrill
3
@NathanMerrill, saya tidak berpikir itu benar-benar mengubah skalabilitas Anda. Ya, Anda harus menerapkan metode baru di setiap instance dari QuestionVisitor. Tetapi kode itulah yang harus Anda tulis untuk menangani GUI untuk jenis pertanyaan baru. Saya tidak berpikir itu benar-benar menambahkan banyak kode yang seharusnya tidak Anda harus benar, tetapi mengubah kode yang hilang menjadi kesalahan kompilasi.
Winston Ewert
4
Benar. Namun, jika saya ingin mengijinkan seseorang membuat Tipe pertanyaan sendiri + Renderer (yang tidak saya miliki), saya tidak berpikir itu mungkin.
Nathan Merrill
2
@NathanMerrill, itu benar. Pendekatan ini mengasumsikan bahwa hanya satu basis kode yang mendefinisikan jenis pertanyaan.
Winston Ewert
4
@ WinstonEwert ini adalah penggunaan yang baik dari pola pengunjung. Tetapi implementasi Anda tidak cukup sesuai dengan pola. Biasanya metode pada pengunjung tidak diberi nama setelah jenis, mereka biasanya memiliki nama yang sama dan hanya berbeda dalam jenis parameter (parameter overloading); nama umum adalah visit(kunjungan pengunjung). Juga metode dalam objek yang dikunjungi biasanya disebut accept(Visitor)(objek menerima pengunjung). Lihat oodesign.com/visitor-pattern.html
Viktor Seifert
2

Dalam C # / WPF (dan, saya bayangkan, dalam bahasa desain yang berfokus pada UI lainnya), kami memiliki DataTemplates . Dengan mendefinisikan templat data, Anda membuat hubungan antara satu jenis "objek data" dan "templat UI" khusus yang dibuat khusus untuk menampilkan objek itu.

Setelah Anda memberikan instruksi kepada UI untuk memuat objek jenis tertentu, ia akan melihat apakah ada templat data yang ditentukan untuk objek tersebut.

BTownTKD
sumber
Ini tampaknya memindahkan masalah ke XML di mana Anda kehilangan semua pengetikan ketat di tempat pertama.
Nathan Merrill
Saya tidak yakin apakah Anda mengatakan itu hal yang baik atau buruk. Di satu sisi, kami sedang memindahkan masalah. Di sisi lain, itu terdengar seperti korek api yang dibuat di surga.
BTownTKD
2

Jika setiap jawaban dapat dikodekan sebagai string, Anda dapat melakukan ini:

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

Di mana string kosong menandakan sebuah pertanyaan tanpa jawaban untuk itu. Ini memungkinkan pertanyaan, jawaban, dan GUI untuk dipisahkan namun memungkinkan untuk polimorfisme.

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

Kotak teks, pencocokan, dan sebagainya dapat memiliki desain yang serupa, semua mengimplementasikan antarmuka pertanyaan. Konstruksi string jawaban terjadi dalam tampilan. String jawaban mewakili kondisi pengujian. Mereka harus disimpan ketika siswa berkembang. Menerapkannya pada pertanyaan memungkinkan menampilkan tes dan statusnya dalam cara yang bertingkat dan tidak bertingkat.

Dengan memisahkan output menjadi display()dan displayGraded()tampilan tidak perlu ditukar dan tidak perlu dilakukan percabangan pada parameter. Namun, setiap tampilan bebas untuk menggunakan kembali logika tampilan sebanyak mungkin saat menampilkan. Skema apa pun yang dirancang untuk melakukan itu tidak perlu bocor ke dalam kode ini.

Namun, jika Anda ingin memiliki kontrol yang lebih dinamis tentang bagaimana sebuah pertanyaan ditampilkan, Anda dapat melakukan ini:

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

dan ini

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

Ini memang memiliki kekurangan yang membutuhkan tampilan yang tidak bermaksud untuk menampilkan score()atau answerKeybergantung pada mereka ketika mereka tidak membutuhkannya. Tetapi itu berarti Anda tidak perlu membangun kembali pertanyaan tes untuk setiap jenis tampilan yang ingin Anda gunakan.

candied_orange
sumber
Jadi ini menempatkan kode GUI dalam Pertanyaan. "Tampilan" dan "displayGraded" Anda terbuka: Untuk setiap jenis "tampilan", saya harus memiliki fungsi lain.
Nathan Merrill
Tidak cukup, ini menempatkan referensi ke tampilan yang polimorfik. MUNGKIN menjadi GUI, halaman web, PDF, apa pun. Ini adalah port keluaran yang dikirimi konten bebas tata letak.
candied_orange
@NathanMerrill harap diperhatikan edit
candied_orange
Antarmuka baru tidak berfungsi: Anda meletakkan "MultipleChoiceView" di dalam antarmuka "Pertanyaan". Anda dapat menempatkan pemirsa ke dalam konstruktor, tetapi sebagian besar waktu Anda tidak tahu (atau peduli) yang akan dilihat saat Anda membuat objek. (Itu bisa diatasi dengan menggunakan fungsi malas / pabrik tetapi logika di balik menyuntikkan ke pabrik itu bisa berantakan)
Nathan Merrill
@NathanMerrill Sesuatu, di suatu tempat harus tahu di mana ini dimaksudkan untuk ditampilkan. Satu-satunya hal yang dilakukan oleh konstruktor adalah membiarkan Anda memutuskan ini pada waktu konstruksi dan kemudian melupakannya. Jika Anda tidak ingin memutuskan ini pada konstruksi maka Anda harus memutuskan nanti dan entah bagaimana mengingat keputusan itu sampai Anda memanggil tampilan. Menggunakan pabrik dalam metode ini tidak akan mengubah fakta ini. Itu hanya menyembunyikan bagaimana Anda membuat keputusan. Biasanya tidak dengan cara yang baik.
candied_orange
1

Menurut pendapat saya, jika Anda membutuhkan fitur generik seperti itu, saya akan mengurangi kopling antara hal-hal dalam kode. Saya akan mencoba mendefinisikan jenis Pertanyaan yang lebih umum yang saya bisa, dan setelah itu saya akan membuat kelas yang berbeda untuk objek penyaji. Silakan lihat contoh di bawah ini:

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

Kemudian, untuk bagian rendering, saya menghapus pemeriksaan Tipe dengan menerapkan pemeriksaan sederhana pada data dalam objek pertanyaan. Kode di bawah ini mencoba menyelesaikan dua hal: (i) hindari pengecekan tipe dan hindari pelanggaran prinsip "L" (substitusi Liskov dalam SOLID) dengan menghapus subtipe kelas Pertanyaan; dan (ii) membuat kode dapat diperluas, dengan tidak pernah mengubah kode rendering inti di bawah ini, hanya menambahkan lebih banyak implementasi QuestionView dan instansnya ke array (ini sebenarnya prinsip "O" dalam SOLID - buka untuk ekstensi dan ditutup untuk modifikasi).

///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));
    }
  }
}
Emerson Cardoso
sumber
Apa yang terjadi ketika MultipleChoiceQuestionView mencoba mengakses bidang MultipleChoice.choices? Itu membutuhkan gips. Tentu, jika kita mengasumsikan pertanyaan itu. Jenisnya unik dan kodenya waras, ini merupakan pemeran yang cukup aman, tetapi itu masih merupakan pemeran: P
Nathan Merrill
Jika Anda perhatikan dalam contoh saya, tidak ada tipe MultipleChoice seperti itu. Hanya ada satu jenis Pertanyaan, yang saya coba definisikan secara umum, dengan daftar informasi (Anda dapat menyimpan banyak pilihan dalam daftar ini, Anda dapat mendefinisikannya seperti yang Anda inginkan). Oleh karena itu, tidak ada pemeran, Anda hanya memiliki Satu jenis Pertanyaan, dan beberapa objek yang memeriksa apakah mereka dapat memberikan pertanyaan ini, jika objek mendukungnya, maka Anda dapat memanggil metode rendering dengan aman.
Emerson Cardoso
Dalam contoh saya, saya memilih untuk mengurangi kopling antara GUI Anda dan properti yang diketik kuat di kelas Pertanyaan tertentu; alih-alih saya mengganti properti tersebut dengan properti generik, yang GUI perlu akses dengan kunci string atau sesuatu yang lain (kopling longgar). Ini adalah tradeoff, mungkin kopling longgar ini tidak diinginkan dalam skenario Anda.
Emerson Cardoso
1

Sebuah pabrik harus dapat melakukan ini. Peta menggantikan pernyataan sakelar, yang diperlukan hanya untuk memasangkan Pertanyaan (yang tidak tahu apa-apa tentang tampilan) dengan 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)
}

Dengan ini tampilan menggunakan jenis Pertanyaan spesifik yang dapat ditampilkan, dan model tetap terputus dari tampilan.

Pabrik dapat diisi melalui refleksi atau secara manual saat aplikasi dimulai.

Xtros
sumber
Jika Anda berada dalam sistem di mana caching tampilan itu penting (seperti game), pabrik dapat menyertakan Pool of the QuestionViews.
Xtros
Ini tampaknya sangat mirip dengan jawaban Caleth: Anda masih perlu Questionmemasukkan ke dalam MultipleChoiceQuestionketika Anda membuatMultipleChoiceView
Nathan Merrill
Di C # setidaknya, saya berhasil melakukan ini tanpa gips. Dalam metode getView, ketika itu menciptakan tampilan contoh (dengan memanggil Activator.CreateInstance (questionViewType, pertanyaan)), parameter kedua CreateInstance adalah parameter yang dikirim ke konstruktor. Konstruktor MultipleChoiceView saya hanya menerima MultipleChoiceQuestion. Mungkin itu hanya memindahkan para pemain ke dalam fungsi CreateInstance sekalipun.
Xtros
0

Saya tidak yakin ini dianggap sebagai "menghindari pemeriksaan tipe", tergantung pada bagaimana perasaan Anda tentang refleksi .

// 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()]());
}
Caleth
sumber
Ini pada dasarnya adalah pemeriksaan tipe, tetapi beralih dari pemeriksaan iftipe ke pemeriksaan dictionarytipe. Seperti bagaimana Python menggunakan kamus alih-alih beralih pernyataan. Yang mengatakan, saya suka cara ini lebih dari daftar pernyataan if.
Nathan Merrill
1
@NathanMerrill Ya. Java tidak memiliki cara yang baik untuk menjaga dua hierarki kelas secara paralel. Dalam c ++ saya akan merekomendasikan template <typename Q> struct question_traits;dengan spesialisasi yang sesuai
Caleth
@ Caleth, dapatkah Anda mengakses informasi itu secara dinamis? Saya pikir Anda harus untuk membangun tipe yang tepat diberikan contoh.
Winston Ewert
Juga, pabrik mungkin membutuhkan contoh pertanyaan yang diteruskan ke sana. Sayangnya, pola ini berantakan, karena biasanya membutuhkan cetakan yang jelek.
Winston Ewert