Mô hình mối quan hệ với DDD (hoặc có ý nghĩa)?


9

Đây là một yêu cầu đơn giản:

Người dùng tạo một Questionvới nhiều Answers. Questionphải có ít nhất một Answer.

Làm rõ: suy nghĩ QuestionAnswernhư trong một bài kiểm tra : có một câu hỏi, nhưng một vài câu trả lời, trong đó ít có thể đúng. Người dùng là diễn viên đang chuẩn bị bài kiểm tra này, do đó anh ta tạo ra câu hỏi và câu trả lời.

Tôi đang cố gắng mô hình hóa ví dụ đơn giản này để 1) khớp với mô hình thực tế 2) để biểu cảm với mã, để giảm thiểu lạm dụng và lỗi tiềm ẩn và đưa ra gợi ý cho các nhà phát triển cách sử dụng mô hình.

Câu hỏi là một thực thể , trong khi câu trả lời là đối tượng giá trị . Câu hỏi giữ câu trả lời. Cho đến nay, tôi có những giải pháp có thể.

[A] Nhà máy bên trongQuestion

Thay vì tạo Answerthủ công, chúng ta có thể gọi:

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

Điều đó sẽ tạo ra một câu trả lời thêm nó vào câu hỏi. Sau đó, chúng ta có thể thao tác trả lời bằng cách đặt thuộc tính của nó. Bằng cách này, chỉ có câu hỏi có thể tạo ra một câu trả lời. Ngoài ra, chúng tôi ngăn chặn để có một câu trả lời mà không có câu hỏi. Tuy nhiên, chúng tôi không có quyền kiểm soát việc tạo câu trả lời, vì đó là mã hóa cứng trong Question.

Cũng có một vấn đề với 'ngôn ngữ' của đoạn mã trên. Người dùng là người tạo ra câu trả lời, không phải câu hỏi. Cá nhân, tôi không thích chúng tôi tạo đối tượng giá trị và tùy thuộc vào nhà phát triển để điền vào giá trị đó - làm thế nào anh ta có thể chắc chắn những gì được yêu cầu để thêm?

[B] Nhà máy bên trong Câu hỏi, lấy số 2

Một số người nói rằng chúng ta nên có loại phương pháp này trong Question:

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

Tương tự như giải pháp trên, phương pháp này lấy dữ liệu bắt buộc cho câu trả lời và tạo một dữ liệu cũng sẽ được thêm vào câu hỏi.

Vấn đề ở đây là chúng tôi nhân đôi các nhà xây dựng của Answerkhông có lý do chính đáng. Ngoài ra, câu hỏi có thực sự tạo ra một câu trả lời?

[C] Phụ thuộc nhà xây dựng

Chúng ta hãy tự do tạo cả hai đối tượng bằng chính mình. Chúng ta cũng thể hiện sự phụ thuộc ngay trong hàm tạo:

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

Điều này đưa ra gợi ý cho nhà phát triển, vì câu trả lời không thể được tạo mà không có câu hỏi. Tuy nhiên, chúng tôi không thấy "ngôn ngữ" nói rằng câu trả lời là "được thêm" vào câu hỏi. Mặt khác, chúng ta có thực sự cần phải nhìn thấy nó?

[D] Phụ thuộc nhà xây dựng, lấy số 2

Chúng ta có thể làm ngược lại:

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

Đây là tình huống ngược lại ở trên. Ở đây câu trả lời có thể tồn tại mà không có câu hỏi (không có ý nghĩa), nhưng câu hỏi không thể tồn tại mà không có câu trả lời (có ý nghĩa). Ngoài ra, "ngôn ngữ" ở đây rõ ràng hơn về câu hỏi đó sẽ câu trả lời.

[E] Cách thông thường

Đây là những gì tôi gọi là cách phổ biến, điều đầu tiên mà ppl thường làm:

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

đó là phiên bản 'lỏng lẻo' của hai câu trả lời trên, vì cả câu trả lời và câu hỏi có thể tồn tại mà không có nhau. Không có gợi ý đặc biệt rằng bạn phải liên kết chúng lại với nhau.

[F] Kết hợp

Hoặc tôi nên kết hợp C, D, E - để bao quát tất cả các cách tạo mối quan hệ, để giúp các nhà phát triển sử dụng bất cứ điều gì tốt nhất cho họ.

Câu hỏi

Tôi biết mọi người có thể chọn một trong những câu trả lời trên dựa trên 'linh cảm'. Nhưng tôi tự hỏi nếu bất kỳ biến thể ở trên là tốt hơn thì biến thể khác với một lý do tốt cho điều đó. Ngoài ra, xin đừng nghĩ bên trong câu hỏi trên, tôi muốn áp dụng ở đây một số thực tiễn tốt nhất có thể áp dụng cho hầu hết các trường hợp - và nếu bạn đồng ý, hầu hết các trường hợp sử dụng tạo một số thực thể đều tương tự nhau. Ngoài ra, hãy để bất khả tri công nghệ ở đây, ví dụ. Tôi không muốn nghĩ liệu ORM sẽ được sử dụng hay không. Chỉ muốn tốt, chế độ biểu cảm.

Bất kỳ sự khôn ngoan về điều này?

BIÊN TẬP

Vui lòng bỏ qua các thuộc tính khác của QuestionAnswer, chúng không liên quan đến câu hỏi. Tôi đã chỉnh sửa văn bản trên và thay đổi hầu hết các hàm tạo (khi cần): bây giờ họ chấp nhận bất kỳ giá trị thuộc tính cần thiết nào cần thiết. Đó có thể chỉ là một chuỗi câu hỏi hoặc bản đồ của các chuỗi trong các ngôn ngữ, trạng thái khác nhau, v.v. - bất kỳ thuộc tính nào được thông qua, chúng không phải là trọng tâm cho điều này;) Vì vậy, giả sử chúng ta ở trên vượt qua các tham số cần thiết, trừ khi nói khác nhau. Thanx!

Câu trả lời:


6

Cập nhật. Làm rõ có tính đến.

Có vẻ như đây là một miền nhiều lựa chọn, thường có các yêu cầu sau

  1. một câu hỏi phải có ít nhất hai lựa chọn để bạn có thể chọn trong số
  2. phải có ít nhất một lựa chọn đúng
  3. không nên có một sự lựa chọn mà không có câu hỏi

Dựa vào những điều trên

[A] không thể đảm bảo bất biến từ điểm 1, bạn có thể kết thúc bằng một câu hỏi mà không có sự lựa chọn nào

[B] có cùng nhược điểm với [A]

[C] có cùng nhược điểm là [A][B]

[D] là một cách tiếp cận hợp lệ, nhưng tốt hơn là vượt qua các lựa chọn dưới dạng danh sách thay vì chuyển chúng riêng lẻ

[E] có cùng nhược điểm là [A] , [B][C]

Do đó, tôi sẽ chọn [D] vì nó cho phép đảm bảo các quy tắc miền từ các điểm 1, 2 và 3 được tuân theo. Ngay cả khi bạn nói rằng rất khó để một câu hỏi tồn tại mà không có sự lựa chọn nào trong một thời gian dài, thì luôn luôn là một ý tưởng tốt để truyền đạt các yêu cầu tên miền thông qua mã.

Tôi cũng sẽ đổi tên Answerthành Choicevì nó có ý nghĩa hơn đối với tôi trong lĩnh vực này.

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

Một lưu ý. Nếu bạn biến Questionthực thể thành một gốc tổng hợp và Choiceđối tượng giá trị là một phần của cùng một tổng hợp, sẽ không có cơ hội nào có thể lưu trữ Choicemà không được gán cho một Question(ngay cả khi bạn không chuyển tham chiếu trực tiếp đến Questionđối số cho Choiceconstructor), bởi vì các kho lưu trữ chỉ hoạt động với các gốc và một khi bạn xây dựng, Questionbạn có tất cả các lựa chọn của mình được gán cho nó trong hàm tạo.

Hi vọng điêu nay co ich.

CẬP NHẬT

Nếu nó thực sự làm phiền bạn về cách các lựa chọn được tạo ra trước câu hỏi của họ, có một vài thủ thuật bạn có thể thấy hữu ích

1) Sắp xếp lại mã sao cho giống như chúng được tạo sau câu hỏi hoặc ít nhất là cùng một lúc

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

2) Ẩn các hàm tạo và sử dụng phương thức tĩnh của nhà máy

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) Sử dụng mẫu xây dựng

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

Tuy nhiên, mọi thứ phụ thuộc vào tên miền của bạn. Hầu hết các lần thứ tự tạo đối tượng không quan trọng từ quan điểm miền vấn đề. Điều quan trọng hơn là ngay khi bạn nhận được một thể hiện của lớp, nó đã hoàn tất về mặt logic và sẵn sàng để sử dụng.


Đã lỗi thời. Tất cả mọi thứ dưới đây không liên quan đến câu hỏi sau khi làm rõ.

Trước hết, theo mô hình miền DDD nên có ý nghĩa trong thế giới thực. Do đó, một vài điểm

  1. một câu hỏi có thể không có câu trả lời
  2. không nên có câu trả lời mà không có câu hỏi
  3. một câu trả lời phải tương ứng với chính xác một câu hỏi
  4. một câu trả lời "trống rỗng" không trả lời một câu hỏi

Dựa vào những điều trên

[A] có thể mâu thuẫn với điểm 4 vì dễ sử dụng sai và quên đặt văn bản.

[B] là một cách tiếp cận hợp lệ nhưng yêu cầu các tham số là tùy chọn

[C] có thể mâu thuẫn với điểm 4 vì nó cho phép câu trả lời không có văn bản

[D] mâu thuẫn với điểm 1 và có thể mâu thuẫn với điểm 2 và 3

[E] có thể mâu thuẫn với các điểm 2, 3 và 4

Thứ hai, chúng ta có thể sử dụng các tính năng OOP để thực thi logic miền. Cụ thể, chúng ta có thể sử dụng các hàm tạo cho các tham số và setters cần thiết cho các tùy chọn.

Thứ ba, tôi sẽ sử dụng ngôn ngữ phổ biến được cho là tự nhiên hơn cho miền.

Và cuối cùng, chúng ta có thể thiết kế tất cả bằng cách sử dụng các mẫu DDD như gốc tổng hợp, các thực thể và các đối tượng giá trị. Chúng ta có thể làm cho Câu hỏi trở thành gốc của tổng hợp và Câu trả lời là một phần của câu hỏi. Đây là một quyết định hợp lý vì một câu trả lời không có ý nghĩa gì ngoài ngữ cảnh của câu hỏi.

Vì vậy, tất cả các bên trên sôi xuống với thiết kế sau đây

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

Trả lời câu hỏi của bạn Tôi đã đưa ra một vài giả định về tên miền của bạn có thể không chính xác, vì vậy hãy thoải mái điều chỉnh những điều trên với chi tiết cụ thể của bạn.


1
Để tóm tắt: đây là sự pha trộn của B và C. Xin vui lòng xem phần làm rõ của tôi về các yêu cầu. Điểm 1. của bạn chỉ có thể tồn tại trong khoảng thời gian 'ngắn', trong khi xây dựng câu hỏi; nhưng không có trong cơ sở dữ liệu. Theo nghĩa đó, 4. không bao giờ nên xảy ra. Tôi hy vọng bây giờ các yêu cầu đã rõ ràng;)
lawpert

Btw, với sự làm rõ, với tôi có vẻ như addAnswerhoặc assignAnswersẽ là ngôn ngữ tốt hơn so với chỉ answer, tôi hy vọng bạn đồng ý về điều này. Dù sao, câu hỏi của tôi là - bạn vẫn sẽ đi đến B và ví dụ: có bản sao của hầu hết các đối số trong phương thức trả lời không? Đó sẽ không phải là trùng lặp?
luật

Xin lỗi vì những yêu cầu không rõ ràng, bạn sẽ thật tử tế khi cập nhật câu trả lời chứ?
luật

1
Hóa ra giả định của tôi là không chính xác. Tôi đã coi miền QA của bạn là một ví dụ về các trang web stackexchange nhưng nó trông giống như một bài kiểm tra trắc nghiệm. Chắc chắn, tôi sẽ cập nhật câu trả lời của tôi.
zafarkhaja

1
@lawpert Answerlà một đối tượng giá trị, nó sẽ được lưu trữ với một gốc tổng hợp của tổng hợp của nó. Bạn không lưu trữ các đối tượng giá trị trực tiếp, bạn cũng không lưu các thực thể nếu chúng không phải là gốc của tập hợp của chúng.
zafarkhaja

1

Trong trường hợp các yêu cầu rất đơn giản, tồn tại nhiều giải pháp khả thi, thì cần tuân thủ nguyên tắc KISS. Trong trường hợp của bạn, đó sẽ là tùy chọn E.

Cũng có trường hợp tạo mã thể hiện một cái gì đó, nhưng nó không nên. Ví dụ: buộc tạo câu trả lời cho câu hỏi (A và B) hoặc đưa ra câu trả lời tham chiếu cho câu hỏi (C và D) thêm một số hành vi không cần thiết cho miền và có thể gây nhầm lẫn. Ngoài ra, trong trường hợp của bạn, Câu hỏi rất có thể sẽ được tổng hợp với Trả lời và Trả lời sẽ là một loại giá trị.


1
Tại sao [C] là một hành vi không cần thiết ? Như tôi thấy, [C] truyền đạt rằng Câu trả lời không thể sống mà không có Câu hỏi và đó chính xác là nó. Hơn nữa, hãy tưởng tượng nếu Trả lời yêu cầu thêm một số cờ (ví dụ: loại câu trả lời, danh mục, v.v.) là bắt buộc. Đi KISS chúng tôi mất kiến ​​thức về những gì là bắt buộc và nhà phát triển phải biết trước những gì anh ta cần thêm / đặt vào Câu trả lời để làm cho đúng. Tôi tin ở đây câu hỏi không phải là mô hình hóa ví dụ rất đơn giản này, mà là tìm ra cách thực hành tốt hơn để viết ngôn ngữ phổ biến bằng OO.
igor

@igor E đã truyền đạt rằng Trả lời là một phần của Câu hỏi bằng cách bắt buộc phải gán Trả lời cho câu hỏi để lưu nó là kho lưu trữ. Nếu có một cách để lưu chỉ Trả lời mà không tải câu hỏi đó, thì C sẽ tốt hơn. Nhưng đó không phải là một điều hiển nhiên từ những gì bạn viết.
Euphoric

@igor Ngoài ra, nếu bạn muốn liên kết việc tạo Câu trả lời với Câu hỏi, thì A sẽ tốt hơn, vì nếu bạn đi với C, thì nó sẽ ẩn khi câu trả lời được gán cho câu hỏi. Ngoài ra, đọc văn bản của bạn trong A, bạn nên phân biệt "hành vi kiểu mẫu" và người bắt đầu hành vi này. Câu hỏi có thể chịu trách nhiệm tạo câu trả lời, khi cần khởi tạo câu trả lời theo một cách nào đó. Nó không có gì để làm với "người dùng tạo câu trả lời".
Euphoric

Chỉ để ghi lại, tôi bị rách giữa C & E :) Bây giờ, điều này: "... bằng cách bắt buộc phải gán Trả lời cho câu hỏi để lưu nó là kho lưu trữ." Điều này có nghĩa là phần 'bắt buộc' chỉ đến khi chúng ta đến kho lưu trữ. Vì vậy, kết nối bắt buộc không 'hiển thị' cho nhà phát triển tại thời điểm biên dịch và các quy tắc kinh doanh bị rò rỉ trong kho lưu trữ. Đó là lý do tại sao tôi đang thử nghiệm [C] ở đây. Có lẽ cuộc nói chuyện này có thể cung cấp thêm thông tin về những gì tôi nghĩ tùy chọn C là về.
igor

Điều này: "... muốn gắn kết việc tạo Câu trả lời với Câu hỏi ...". Tôi không muốn buộc _creation chính nó. Chỉ muốn thể hiện mối quan hệ bắt buộc . (Cá nhân tôi thích có thể tự tạo các đối tượng mô hình, khi có thể). Vì vậy, theo quan điểm của tôi, đây không phải là về việc tạo ra, đó là lý do tại sao tôi sớm bỏ rơi A và B. Tôi không thấy rằng Câu hỏi có trách nhiệm tạo ra câu trả lời.
igor

1

Tôi sẽ đi [C] hoặc [E].

Đầu tiên, tại sao không phải là A và B? Tôi không muốn Câu hỏi của mình chịu trách nhiệm tạo ra bất kỳ giá trị liên quan nào. Hãy tưởng tượng nếu Câu hỏi có nhiều đối tượng giá trị khác - bạn sẽ đặt createphương thức cho mọi đối tượng chứ? Hoặc nếu có một số tập hợp phức tạp, trường hợp tương tự.

Tại sao không [D]? Bởi vì nó trái ngược với những gì chúng ta có trong tự nhiên. Trước tiên chúng ta tạo một Câu hỏi. Bạn có thể tưởng tượng một trang web nơi bạn tạo tất cả điều này - trước tiên người dùng sẽ tạo một câu hỏi, phải không? Do đó, không phải D.

[E] là KISS, như @Euphoric đã nói. Nhưng tôi cũng bắt đầu thích [C] gần đây. Điều này không quá khó hiểu như nó có vẻ. Hơn nữa, hãy tưởng tượng nếu Câu hỏi phụ thuộc vào nhiều thứ hơn - thì nhà phát triển phải biết những gì anh ta cần đưa vào Câu hỏi để được khởi tạo đúng cách. Mặc dù bạn đúng - không có ngôn ngữ "trực quan" nào giải thích rằng câu trả lời thực sự được thêm vào câu hỏi.

Đọc thêm

Những câu hỏi như thế này khiến tôi tự hỏi liệu ngôn ngữ máy tính của chúng ta quá chung chung để tạo mô hình. (Tôi hiểu họ phải chung chung để trả lời tất cả các yêu cầu lập trình). Gần đây tôi đang cố gắng tìm một cách tốt hơn để diễn đạt ngôn ngữ kinh doanh bằng các giao diện lưu loát. Một cái gì đó như thế này (bằng ngôn ngữ sudo):

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

tức là cố gắng di chuyển khỏi bất kỳ lớp * Dịch vụ và * Kho lưu trữ lớn nào đến các khối logic kinh doanh nhỏ hơn. Chỉ là một ý tưởng.


Bạn đang nói trong addon về Ngôn ngữ cụ thể miền?
luật

Bây giờ khi bạn đề cập, có vẻ như vậy :) Mua Tôi không có bất kỳ kinh nghiệm đáng kể nào với nó.
igor

2
Tôi nghĩ rằng bây giờ có một sự đồng thuận rằng IO là một trách nhiệm trực giao và do đó không nên được xử lý bởi các thực thể (storeToRepo)
Esben Skov Pedersen

Tôi đồng ý @Esben Skov Pedersen rằng chính thực thể đó không nên gọi repo bên trong (đó là những gì bạn đã nói, phải không?); nhưng như AFAIU ở đây, chúng ta có một số kiểu mẫu xây dựng đằng sau mà gọi các lệnh; vì vậy IO không được thực hiện trong thực thể ở đây. Ít nhất đây là cách Ive hiểu nó;)
lawpert

@lawpert đúng rồi. Tôi không thấy nó hoạt động như thế nào nhưng sẽ rất thú vị.
Esben Skov Pedersen

1

Tôi tin rằng bạn đã bỏ lỡ một điểm ở đây, gốc Tổng hợp của bạn phải là Thực thể Kiểm tra của bạn.

Và nếu đó thực sự là trường hợp tôi tin rằng TestFactory sẽ phù hợp nhất để trả lời vấn đề của bạn.

Bạn sẽ ủy thác tòa nhà Câu hỏi và Trả lời cho Nhà máy và do đó về cơ bản bạn có thể sử dụng bất kỳ giải pháp nào bạn nghĩ đến mà không làm hỏng mô hình của bạn vì bạn đang ẩn cho khách hàng theo cách bạn khởi tạo các thực thể phụ của mình.

Điều này là, miễn là TestFactory là giao diện duy nhất bạn sử dụng để khởi tạo Thử nghiệm của mình.

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.