Model hubungan dengan DDD (atau dengan akal)?

9

Berikut ini persyaratan yang disederhanakan:

Pengguna membuat Questiondengan beberapa Answers. Questionharus memiliki setidaknya satu Answer.

Klarifikasi: pikirkan Questiondan Answerseperti dalam ujian : ada satu pertanyaan, tetapi beberapa jawaban, di mana hanya sedikit yang benar. Pengguna adalah aktor yang sedang mempersiapkan tes ini, maka ia menciptakan pertanyaan dan jawaban.

Saya mencoba memodelkan contoh sederhana ini agar 1) mencocokkan model kehidupan nyata 2) agar ekspresif dengan kode, sehingga untuk meminimalkan potensi penyalahgunaan dan kesalahan, dan untuk memberikan petunjuk kepada pengembang cara menggunakan model.

Pertanyaan adalah entitas , sedangkan jawaban adalah objek nilai . Pertanyaan memiliki jawaban. Sejauh ini, saya punya solusi yang memungkinkan.

[A] Pabrik di dalamQuestion

Alih-alih membuat Answersecara manual, kita dapat memanggil:

Answer answer = question.createAnswer()
answer.setText("");
...

Itu akan membuat jawaban dan menambahkannya ke pertanyaan. Kemudian kita dapat memanipulasi jawaban dengan mengatur propertinya. Dengan cara ini, hanya pertanyaan yang dapat memberikan jawaban. Selain itu, kami mencegah mendapat jawaban tanpa pertanyaan. Namun, kami tidak memiliki kendali atas menciptakan jawaban, karena itu di-hardcode di Question.

Ada juga satu masalah dengan 'bahasa' kode di atas. Pengguna adalah orang yang menciptakan jawaban, bukan pertanyaan. Secara pribadi, saya tidak suka kita membuat objek nilai dan bergantung pada pengembang untuk mengisinya dengan nilai - bagaimana dia bisa yakin apa yang harus ditambahkan?

[B] Pabrik di dalam Pertanyaan, ambil nomor 2

Beberapa mengatakan kita harus memiliki metode semacam ini di Question:

question.addAnswer(String answer, boolean correct, int level....);

Mirip dengan solusi di atas, metode ini mengambil data wajib untuk jawabannya dan membuat yang juga akan ditambahkan ke pertanyaan.

Masalahnya di sini adalah bahwa kita menduplikasi konstruktor Answertanpa alasan yang bagus. Juga, apakah pertanyaan benar-benar menciptakan jawaban?

[C] dependensi konstruktor

Mari kita bebas membuat kedua objek dengan diri kita sendiri. Mari kita juga mengekspresikan dependensi yang tepat dalam konstruktor:

Question q = new Question(...);
Answer a = new Answer(q, ...);   // answer can't exist without a question

Ini memberikan petunjuk kepada pengembang, karena jawaban tidak dapat dibuat tanpa pertanyaan. Namun, kami tidak melihat 'bahasa' yang mengatakan bahwa jawaban itu 'ditambahkan' ke pertanyaan. Di sisi lain, apakah kita benar-benar perlu melihatnya?

[D] Constructor ketergantungan, mengambil # 2

Kita bisa melakukan yang sebaliknya:

Answer a1 = new Answer("",...);
Answer a2 = new Answer("",...);
Question q = new Question("", a1, a2);

Ini adalah situasi yang berlawanan di atas. Di sini jawaban bisa ada tanpa pertanyaan (yang tidak masuk akal), tetapi pertanyaan tidak bisa ada tanpa jawaban (yang masuk akal). Juga, 'bahasa' di sini lebih jelas tentang pertanyaan itu yang akan memiliki jawabannya.

[E] Cara umum

Inilah yang saya sebut cara umum, hal pertama yang biasanya dilakukan ppl:

Question q = new Question("",...);
Answer a = new Answer("",...);
q.addAnswer(a);

yang merupakan versi 'longgar' dari dua jawaban di atas, karena baik jawaban maupun pertanyaan mungkin ada tanpa satu sama lain. Tidak ada petunjuk khusus bahwa Anda harus mengikat mereka bersama.

[F] Gabungan

Atau saya harus menggabungkan C, D, E - untuk mencakup semua cara bagaimana hubungan dapat dibuat, sehingga untuk membantu pengembang untuk menggunakan apa pun yang terbaik untuk mereka.

Pertanyaan

Saya tahu orang dapat memilih salah satu jawaban di atas berdasarkan 'firasat'. Tapi saya ingin tahu apakah ada varian di atas yang lebih baik dari yang lain dengan alasan yang bagus Juga, tolong jangan berpikir di dalam pertanyaan di atas, saya ingin memeras di sini beberapa praktik terbaik yang dapat diterapkan pada kebanyakan kasus - dan jika Anda setuju, sebagian besar kasus penggunaan penciptaan beberapa entitas serupa. Juga, mari kita menjadi agnostik teknologi di sini, misalnya. Saya tidak ingin berpikir apakah ORM akan digunakan atau tidak. Hanya ingin mode ekspresif yang bagus.

Adakah kebijaksanaan tentang hal ini?

EDIT

Harap abaikan properti lain dari Questiondan Answer, mereka tidak relevan untuk pertanyaan. Saya mengedit teks di atas dan mengubah sebagian besar konstruktor (bila perlu): sekarang mereka menerima nilai properti yang diperlukan. Itu mungkin hanya string pertanyaan, atau peta string dalam bahasa yang berbeda, status dll - properti apa pun yang dilewati, mereka bukan fokus untuk ini;) Jadi anggap saja kita di atas melewati parameter yang diperlukan, kecuali dikatakan berbeda. Terima kasih!

lawpert
sumber

Jawaban:

6

Diperbarui. Klarifikasi diperhitungkan.

Sepertinya ini adalah domain pilihan ganda, yang biasanya memiliki persyaratan berikut

  1. sebuah pertanyaan harus memiliki setidaknya dua pilihan sehingga Anda dapat memilih di antara
  2. harus ada setidaknya satu pilihan yang benar
  3. seharusnya tidak ada pilihan tanpa pertanyaan

berdasarkan hal di atas

[A] tidak dapat memastikan invarian dari poin 1, Anda mungkin berakhir dengan pertanyaan tanpa pilihan

[B] memiliki kerugian yang sama dengan [A]

[C] memiliki kerugian yang sama dengan [A] dan [B]

[D] adalah pendekatan yang valid, tetapi lebih baik untuk melewati pilihan sebagai daftar daripada melewati secara individual

[E] memiliki kelemahan yang sama dengan [A] , [B] dan [C]

Oleh karena itu, saya akan memilih [D] karena memungkinkan untuk memastikan aturan domain dari poin 1, 2 dan 3 diikuti. Bahkan jika Anda mengatakan bahwa kemungkinan sebuah pertanyaan tidak akan bertahan tanpa pilihan untuk jangka waktu yang lama, itu selalu merupakan ide yang baik untuk menyampaikan persyaratan domain melalui kode.

Saya juga akan mengganti nama Answermenjadi Choicekarena lebih masuk akal bagi saya dalam domain ini.

public class Choice implements ValueObject {

    private Question q;
    private final String txt;
    private final boolean isCorrect;
    private boolean isSelected = false;

    public Choice(String txt, boolean isCorrect) {
        // validate and assign
    }

    public void assignToQuestion(Question q) {
        this.q = q;
    }

    public void select() {
        isSelected = true;
    }

    public void unselect() {
        isSelected = false;
    }

    public boolean isSelected() {
        return isSelected;
    }
}

public class Question implements Entity {

    private final String txt;
    private final List<Choice> choices;

    public Question(String txt, List<Choice> choices) {
        // ensure requirements are met
        // 1. make sure there are more than 2 choices
        // 2. make sure at least 1 of the choices is correct
        // 3. assign each choice to this question
    }
}

Choice ch1 = new Choice("The sky", false);
Choice ch2 = new Choice("Ceiling", true);
List<Choice> choices = Arrays.asList(ch1, ch2);
Question q = new Question("What's up?", choices);

Sebuah catatan. Jika Anda menjadikan Questionentitas agregat root dan Choiceobjek nilai bagian dari agregat yang sama, tidak ada kemungkinan seseorang dapat menyimpan Choicetanpa itu ditugaskan ke Question(meskipun Anda tidak memberikan referensi langsung ke Questionsebagai argumen ke Choicekonstruktor), karena repositori hanya bekerja dengan root dan sekali Anda membangun, QuestionAnda memiliki semua pilihan yang ditugaskan di konstruktor.

Semoga ini membantu.

MEMPERBARUI

Jika itu benar-benar mengganggu Anda bagaimana pilihan dibuat sebelum pertanyaan mereka, ada beberapa trik yang mungkin berguna

1) Atur ulang kode sehingga terlihat seperti dibuat setelah pertanyaan atau setidaknya pada saat yang sama

Question q = new Question(
    "What's up?",
    Arrays.asList(
        new Choice("The sky", false),
        new Choice("Ceiling", true)
    )
);

2) Sembunyikan konstruktor dan gunakan metode pabrik statis

public class Question implements Entity {
    ...

    private Question(String txt) { ... }

    public static Question newInstance(String txt, List<Choice> choices) {
        Question q = new Question(txt);
        for (Choice ch : choices) {
            q.assignChoice(ch);
        }
    }

    public void assignChoice(Choice ch) { ... }
    ...
}

3) Gunakan pola pembangun

Question q = new Question.Builder("What's up?")
    .assignChoice(new Choice("The sky", false))
    .assignChoice(new Choice("Ceiling", true))
    .build();

Namun, semuanya tergantung pada domain Anda. Sebagian besar urutan penciptaan objek tidak penting dari perspektif domain masalah. Yang lebih penting adalah bahwa segera setelah Anda mendapatkan instance dari kelas Anda, itu secara logis lengkap dan siap digunakan.


Ketinggalan jaman. Semuanya di bawah ini tidak relevan dengan pertanyaan setelah klarifikasi.

Pertama-tama, menurut model domain DDD harus masuk akal di dunia nyata. Karenanya, beberapa poin

  1. sebuah pertanyaan mungkin tidak memiliki jawaban
  2. seharusnya tidak ada jawaban tanpa pertanyaan
  3. jawaban harus sesuai dengan satu pertanyaan
  4. jawaban "kosong" tidak menjawab pertanyaan

berdasarkan hal di atas

[A] dapat bertentangan dengan poin 4 karena mudah disalahgunakan dan lupa mengatur teks.

[B] adalah pendekatan yang valid tetapi membutuhkan parameter yang opsional

[C] dapat bertentangan dengan poin 4 karena memungkinkan jawaban tanpa teks

[D] bertentangan dengan poin 1 dan dapat bertentangan dengan poin 2 dan 3

[E] dapat bertentangan dengan poin 2, 3 dan 4

Kedua, kita dapat menggunakan fitur OOP untuk menegakkan logika domain. Yaitu kita dapat menggunakan konstruktor untuk parameter yang diperlukan dan setter untuk yang opsional.

Ketiga, saya akan menggunakan bahasa di mana-mana yang seharusnya lebih alami untuk domain.

Dan akhirnya, kita bisa mendesain semuanya menggunakan pola DDD seperti akar agregat, entitas dan objek nilai. Kita dapat menjadikan Pertanyaan sebagai akar dari agregatnya dan Jawaban menjadi bagian darinya. Ini adalah keputusan yang logis karena jawaban tidak memiliki makna di luar konteks pertanyaan.

Jadi, semua hal di atas bermuara pada desain berikut

class Answer implements ValueObject {

    private final Question q;
    private String txt;
    private boolean isCorrect = false;

    Answer(Question q, String txt) {
        // validate and assign
    }

    public void markAsCorrect() {
        isCorrect = true;
    }

    public boolean isCorrect() {
        return isCorrect;
    }
}

public class Question implements Entity {

    private String txt;
    private final List<Answer> answers = new ArrayList<>();

    public Question(String txt) {
        // validate and assign
    }

    // Ubiquitous Language: answer() instead of addAnswer()
    public void answer(String txt) {
        answers.add(new Answer(this, txt));
    }
}

Question q = new Question("What's up?");
q.answer("The sky");

PS Menjawab pertanyaan Anda Saya membuat beberapa asumsi tentang domain Anda yang mungkin tidak benar, jadi silakan menyesuaikan di atas dengan spesifik Anda.

zafarkhaja
sumber
1
Untuk meringkas: ini adalah campuran B dan C. Silakan lihat klarifikasi saya tentang persyaratan. Poin Anda 1. mungkin ada hanya untuk periode 'singkat', sambil membangun pertanyaan; tetapi tidak dalam database. Dalam hal itu, 4. seharusnya tidak pernah terjadi. Saya harap sekarang persyaratannya jelas;)
lawpert
Btw, dengan klarifikasi, bagi saya tampaknya addAnsweratau assignAnswerakan menjadi bahasa yang lebih baik daripada hanya answer, saya harap Anda setuju dengan ini. Lagi pula, pertanyaan saya adalah - apakah Anda masih memilih B dan misalnya memiliki salinan sebagian besar argumen dalam metode jawaban? Bukankah itu duplikasi?
lawpert
Maaf untuk persyaratan yang tidak jelas, apakah Anda akan berbaik hati memperbarui jawabannya?
lawpert
1
Ternyata asumsi saya salah. Saya memperlakukan domain QA Anda sebagai contoh situs web stackexchange tetapi lebih mirip tes pilihan ganda. Tentu, saya akan memperbarui jawaban saya.
zafarkhaja
1
@lawpert Answeradalah objek nilai, itu akan disimpan dengan akar agregat dari agregatnya. Anda tidak menyimpan objek nilai secara langsung, Anda juga tidak menyimpan entitas jika mereka bukan akar dari agregatnya.
zafarkhaja
1

Jika persyaratannya sangat sederhana, ada beberapa kemungkinan solusi, maka prinsip KISS harus diikuti. Dalam kasus Anda, itu akan menjadi opsi E.

Ada juga kasus membuat kode yang mengekspresikan sesuatu, yang seharusnya tidak. Misalnya mengikat pembuatan Jawaban atas Pertanyaan (A dan B) atau memberikan referensi Jawaban ke Pertanyaan (C dan D) menambahkan beberapa perilaku yang tidak perlu ke domain dan mungkin membingungkan. Juga, dalam kasus Anda, Pertanyaan kemungkinan besar adalah agregat dengan Jawaban dan Jawaban akan menjadi tipe nilai.

Euforia
sumber
1
Mengapa [C] adalah perilaku yang tidak perlu ? Seperti yang saya lihat, [C] mengomunikasikan bahwa Jawaban tidak dapat hidup tanpa Pertanyaan, dan itulah yang sebenarnya. Terlebih lagi, bayangkan jika Answer membutuhkan beberapa flag lagi (mis. Tipe jawaban, kategori, dll) yang wajib. Pergi KISS kami kehilangan pengetahuan itu tentang apa yang wajib, dan pengembang harus tahu di depan apa yang dia perlu tambahkan / set ke Jawaban untuk membuatnya benar. Saya percaya di sini pertanyaannya bukan untuk memodelkan contoh yang sangat sederhana ini, tetapi untuk menemukan praktik yang lebih baik untuk menulis bahasa di mana-mana menggunakan OO.
igor
@igor E sudah mengomunikasikan bahwa Jawaban adalah bagian dari Pertanyaan dengan mewajibkannya untuk memberikan Jawaban atas pertanyaan agar dapat disimpan adalah repositori. Jika ada cara untuk menyimpan Jawaban saja tanpa memuat pertanyaannya, maka C akan lebih baik. Tapi itu tidak jelas dari apa yang Anda tulis.
Euforia
@ Goror Juga, jika Anda ingin mengikat penciptaan Jawaban dengan Pertanyaan, maka A akan lebih baik, karena jika Anda menggunakan C, maka ia bersembunyi ketika jawaban ditugaskan untuk pertanyaan. Juga, membaca teks Anda dalam A, Anda harus membedakan "perilaku model" dan siapa yang memulai perilaku ini. Pertanyaan dapat bertanggung jawab untuk menciptakan jawaban, ketika perlu menginisialisasi jawaban dengan beberapa cara. Ini tidak ada hubungannya dengan "pengguna menciptakan jawaban".
Euforia
Sekadar catatan, saya terpecah antara C&E :) Sekarang, ini: "... dengan mewajibkannya untuk memberikan Jawab atas pertanyaan agar dapat disimpan adalah repositori." Ini berarti bahwa bagian 'wajib' datang hanya ketika kita datang ke repositori. Jadi koneksi wajib tidak 'terlihat' oleh pengembang pada waktu kompilasi, dan aturan bisnis bocor dalam repositori. Itulah mengapa saya menguji [C] di sini. Mungkin pembicaraan ini dapat memberikan lebih banyak info tentang apa yang menurut saya pilihan C.
igor
Ini: "... ingin mengikat kreasi Jawaban dengan Pertanyaan ...". Saya tidak ingin mengikat _creation itu sendiri. Hanya ingin mengekspresikan hubungan wajib . (Secara pribadi ingin dapat membuat objek model sendiri, jika mungkin). Jadi, dalam pandangan saya, ini bukan tentang menciptakan, itu sebabnya saya membuang A dan B segera. Saya tidak melihat bahwa Pertanyaan bertanggung jawab untuk membuat jawaban.
igor
1

Saya akan memilih [C] atau [E].

Pertama, mengapa tidak A dan B? Saya tidak ingin Pertanyaan saya bertanggung jawab untuk menciptakan nilai terkait. Bayangkan jika Pertanyaan memiliki banyak objek bernilai lainnya - apakah Anda akan memasukkan createmetode untuk setiap objek bernilai ? Atau jika ada beberapa agregat kompleks, hal yang sama.

Kenapa tidak [D]? Karena itu berlawanan dengan apa yang kita miliki di alam. Kami pertama kali membuat Pertanyaan. Anda dapat membayangkan halaman web tempat Anda membuat semua ini - pengguna pertama kali akan membuat pertanyaan, bukan? Karena itu, bukan D.

[E] KISS, seperti yang dikatakan @Euphoric. Tapi saya juga mulai menyukai [C] baru-baru ini. Ini tidak begitu membingungkan seperti yang terlihat. Selain itu, bayangkan jika Pertanyaan tergantung pada lebih banyak hal - maka pengembang harus tahu apa yang perlu dimasukkan ke dalam Pertanyaan agar diinisialisasi dengan benar. Meskipun Anda benar - tidak ada bahasa 'visual' yang menjelaskan bahwa jawaban sebenarnya ditambahkan ke pertanyaan.

Bacaan tambahan

Pertanyaan seperti ini membuat saya bertanya-tanya apakah bahasa komputer kita terlalu umum untuk pemodelan. (Saya mengerti mereka harus generik untuk menjawab semua persyaratan pemrograman). Baru-baru ini saya mencoba menemukan cara yang lebih baik untuk mengekspresikan bahasa bisnis menggunakan antarmuka yang lancar. Sesuatu seperti ini (dalam bahasa sudo):

use(question).addAnswer(answer).storeToRepo();

yaitu mencoba untuk beralih dari kelas * Layanan dan * Repositori besar ke bagian yang lebih kecil dari logika bisnis. Hanya sebuah ide.

igor
sumber
Anda berbicara di addon tentang Bahasa Khusus Domain?
lawpert
Sekarang ketika Anda sebutkan, kelihatannya begitu :) Beli Saya tidak punya pengalaman berarti dengannya.
igor
2
Saya pikir ada konsensus sekarang bahwa IO adalah tanggung jawab ortogonal dan karenanya tidak boleh ditangani oleh entitas (storeToRepo)
Esben Skov Pedersen
Saya setuju @Esben Skov Pedersen bahwa entitas itu sendiri tidak boleh memanggil repo di dalam (itu yang Anda katakan, kan?); tetapi sebagai AFAIU di sini kita memiliki semacam pola pembangun di belakang yang memanggil perintah; jadi IO tidak dilakukan dalam entitas di sini. Setidaknya begitulah cara saya memahaminya;)
lawpert
@lawpert itu benar. Saya gagal melihat bagaimana seharusnya bekerja tetapi akan menarik.
Esben Skov Pedersen
1

Saya yakin Anda melewatkan satu poin di sini, akar Agregat Anda harus menjadi Entitas Tes Anda.

Dan jika itu benar-benar terjadi, saya percaya TestFactory akan paling cocok untuk menjawab masalah Anda.

Anda akan mendelegasikan bangunan Pertanyaan dan Jawaban ke Pabrik dan karena itu Anda pada dasarnya dapat menggunakan solusi apa pun yang Anda pikirkan tanpa merusak model Anda karena Anda bersembunyi kepada klien seperti cara Anda instantiate sub entitas Anda.

Ini, selama TestFactory adalah satu-satunya antarmuka yang Anda gunakan untuk instantiate Tes Anda.

Alexandre BODIN
sumber