Модельні стосунки з DDD (або з сенсом)?


9

Ось спрощена вимога:

Користувач створює Questionдекілька Answers. Questionповинен мати хоча б одну Answer.

Уточнення: подумайте Questionі Answerяк у тесті : є одне запитання, але кілька відповідей, де мало хто може бути правильним. Користувач - це актор, який готує цей тест, тому він створює питання та відповіді.

Я намагаюся моделювати цей простий приклад таким чином, щоб 1) відповідати реальній життєвій моделі 2), щоб бути виразним з кодом, щоб мінімізувати можливе неправильне використання та помилки, а також дати підказки розробникам, як користуватися моделлю.

Питання - це сутність , а відповідь - об'єкт значення . Питання містить відповіді. Поки що я маю ці можливі рішення.

[A] Фабрика всерединіQuestion

Замість створення Answerвручну ми можемо зателефонувати:

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

Це створить відповідь і додасть її до питання. Тоді ми можемо маніпулювати відповіддю, встановивши її властивості. Таким чином, лише питання можуть створити відповідь. Також ми заважаємо відповідати без запитання. Тим не менш, ми не маємо контролю над створенням відповідей, як це жорстко закодовано в Question.

Існує також одна проблема з "мовою" вищевказаного коду. Користувач - це той, хто створює відповіді, а не питання. Особисто мені не подобається, що ми створюємо об'єкт цінності і залежно від розробника заповнюємо його значеннями - як він може бути впевнений, що потрібно додати?

[B] Завод всередині питання, візьміть №2

Деякі кажуть, що ми повинні мати такий спосіб у Question:

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

Подібно до вищезгаданого рішення, цей метод бере обов'язкові дані для відповіді та створює такі, які також будуть додані до питання.

Проблема тут полягає в тому, що ми дублюємо конструктор Answerбез жодних причин. Також питання справді створює відповідь?

[C] Залежності конструктора

Будьмо вільні створити обидва об’єкти нашими власними силами. Висловимо також право залежності в конструкторі:

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

Це дає підказки для розробника, оскільки відповідь неможливо створити без запитання. Однак ми не бачимо "мови", яка говорить про те, що відповідь "додається" до питання. З іншого боку, чи насправді нам це потрібно бачити?

[D] Залежність конструктора, візьміть №2

Ми можемо зробити навпаки:

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

Це протилежна ситуація вище. Тут відповіді можуть існувати без запитання (що не має сенсу), але питання не може існувати без відповіді (які мають сенс). Крім того , «мова» тут більш ясно з цього питання буде мати відповіді.

[E] Загальний шлях

Це те, що я називаю загальним способом, перше, що зазвичай робить ppl:

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

що є "вільною" версією вищезгаданих двох відповідей, оскільки і відповідь, і питання можуть існувати один без одного. Немає особливого натяку на те, що вам потрібно зв'язати їх разом.

[F] Комбінована

Або я повинен поєднувати C, D, E - щоб висвітлити всі способи створення відносин, щоб допомогти розробникам використовувати все, що найкраще для них.

Питання

Я знаю, що люди можуть обрати одну з вищезазначених відповідей на основі "химки". Але мені цікаво, чи кращий будь-який із перерахованих вище варіантів, ніж інший, для цього є вагома причина. Крім того, будь ласка, не замислюйтесь у вищезазначеному питанні, я хотів би видавити тут кілька найкращих практик, які можна застосувати у більшості випадків - і якщо ви згодні, більшість випадків використання деяких суб'єктів подібні. Також тут давайте можливість агностики, наприклад. Я не хочу думати, чи буде ORM використовуватись чи ні. Просто хочеться хорошого, виразного режиму.

Якась мудрість щодо цього?

EDIT

Будь ласка, ігноруйте інші властивості Questionта Answer, вони не стосуються питання. Я редагував вище текст і змінив більшість конструкторів (де це потрібно): тепер вони приймають будь-які необхідні значення властивостей. Це може бути лише рядок запитань або карта рядків різними мовами, статусами тощо - які б властивості не передавались, вони не є для цього фокусом;) Отже, припускайте, що ми вище проходження необхідних параметрів, якщо не зазначено інше. Дякую!

Відповіді:


6

Оновлено. Роз’яснення, що враховуються.

Схоже, це домен з декількома варіантами, який, як правило, має такі вимоги

  1. запитання повинно мати принаймні два варіанти, щоб ви могли вибрати один із них
  2. повинен бути принаймні один правильний вибір
  3. не повинно бути вибору без запитання

Виходячи із сказаного

[A] не може забезпечити інваріант з пункту 1, ви можете закінчити питання без вибору

[B] має той самий недолік, що і [A]

[C] має той самий недолік, що і [A] та [B]

[D] є правильним підходом, але краще передати вибір як список, а не передавати їх окремо

[E] має той самий недолік, що і [A] , [B] та [C]

Отже, я б пішов на [D], оскільки це дозволяє забезпечити дотримання правил домену з пунктів 1, 2 та 3. Навіть якщо ви скажете, що навряд чи питання залишатиметься без вибору протягом тривалого періоду часу, завжди добре передати вимоги домену через код.

Я також хотів би перейменувати , Answerщоб , Choiceяк це має сенс для мене в цій області.

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

Нотатка. Якщо ви зробите Questionсутність сукупним коренем і Choiceоб'єктом значення частиною одного агрегату, немає жодних шансів, що можна зберегти a, Choiceне призначивши його Question(навіть якщо ви не передаєте пряме посилання на Questionаргумент на Choiceконструктор), тому що репозиторії працюють лише з коренями, і коли ви будуєте свій Question, у вашому конструкторі призначені всі варіанти вибору.

Сподіваюсь, це допомагає.

ОНОВЛЕННЯ

Якщо це насправді турбує вас, як вибір створюється напередодні їхнього питання, є кілька хитрощів, які можуть вам бути корисними

1) Перестановіть код так, щоб він виглядав так, як вони створені після запитання або принаймні одночасно

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

2) Сховати конструктори і використовувати статичний заводський метод

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) Використовуйте схему конструктора

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

Однак все залежить від вашого домену. У більшості випадків порядок створення об’єктів не важливий з точки зору проблемної області. Що важливіше, що як тільки ви отримаєте екземпляр свого класу, він логічно завершений і готовий до використання.


Застаріла. Все, що нижче, не стосується питання після уточнень.

Перш за все, згідно з доменною моделлю DDD має мати сенс у реальному світі. Отже, мало балів

  1. запитання може не мати відповідей
  2. відповіді без запитання не повинно бути
  3. відповідь повинна відповідати точно одному питанню
  4. "порожня" відповідь не відповідає на запитання

Виходячи із сказаного

[A] може суперечити пункту 4, оскільки легко неправильно використовувати та забути встановити текст.

[B] є коректним підходом, але вимагає необов'язкових параметрів

[C] може суперечити пункту 4, оскільки він дозволяє відповісти без тексту

[D] суперечить пункту 1 і може суперечити пунктам 2 і 3

[Е] може суперечити пунктам 2, 3 та 4

По-друге, ми можемо використовувати функції OOP для забезпечення логіки домену. А саме ми можемо використовувати конструктори для необхідних параметрів та задачі для необов'язкових.

По-третє, я б використовував всюдисущу мову, яка повинна бути більш природною для домену.

І нарешті, ми можемо спроектувати все це за допомогою шаблонів DDD, таких як сукупні корені, сутності та об'єкти цінності. Ми можемо зробити Питання коренем його сукупності, а Відповідь - частиною. Це логічне рішення, оскільки відповідь не має значення поза контекстом питання.

Отже, все вищезазначене зводиться до наступної конструкції

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 Відповідаючи на ваше запитання, я зробив декілька припущень щодо вашого домену, які можуть бути невірними, тому не соромтесь коригувати вищевикладене зі своїми особливостями.


1
Підводячи підсумок: це суміш B і C. Будь ласка, дивіться мої роз’яснення вимог. Ваша точка 1. може існувати лише "короткий" проміжок часу, будуючи питання; але не в базі даних. У цьому сенсі 4. ніколи не повинно відбуватися. Сподіваюся, зараз вимоги зрозумілі;)
юрист

До речі, із з'ясуванням, мені здається , що addAnswerабо assignAnswerбуло б краще , ніж мова просто answer, я сподіваюся , що ви згодні з цим. У будь-якому разі, моє запитання - ти все-таки підеш за B і, наприклад, мав би копію більшості аргументів у методі відповіді? Не було б це дублюванням?
юрист

Вибачте за незрозумілі вимоги, чи не хочете ви так оновити відповідь?
юрист

1
Виявляється, мої припущення були невірними. Я розглянув ваш домен QA як приклад веб-сайтів stackexchange, але він більше схожий на тест з декількома варіантами. Звичайно, я оновлю свою відповідь.
zafarkhaja

1
@lawpert Answer- це об'єкт значення, який буде зберігатися з сукупним коренем його сукупності. Ви не зберігаєте об'єкти цінності безпосередньо, а також не зберігаєте сутності, якщо вони не є коренями їх сукупностей.
zafarkhaja

1

Якщо вимоги настільки прості, що існує декілька можливих рішень, слід керуватися принципом KISS. У вашому випадку це був би варіант E.

Існує також випадок створення коду, який виражає щось, чого не слід. Наприклад, пов’язуючи створення відповідей на запитання (A і B) або даючи посилання відповіді на питання (C і D), додайте певну поведінку, яка не потрібна домену і може заплутати. Крім того, у вашому випадку питання, швидше за все, буде об'єднано з відповіддю, а відповідь буде типом значення.


1
Чому [С] - непотрібна поведінка? Як я бачу, [С] повідомляє, що Відповідь не може жити без Питання, і саме так воно і є. Крім того, уявіть, якщо для відповіді потрібні ще якісь прапори (наприклад, тип відповіді, категорія тощо), які є обов'язковими. Переходячи до KISS, ми втрачаємо знання про те, що є обов'язковим, і розробник повинен знати на передньому місці, що йому потрібно додати / встановити у відповідь, щоб зробити його правильним. Я вважаю, що тут питання полягало не в моделюванні цього дуже простого прикладу, а в пошуку кращої практики написання всюдисущої мови за допомогою OO.
igor

@igor E вже повідомляє, що відповідь є частиною питання, примушуючи призначати відповідь на запитання, щоб зберегти її, сховище. Якби був спосіб зберегти просто відповідь, не завантажуючи це питання, тоді C було б краще. Але це не очевидно з того, що ви написали.
Ейфорія

@igor Крім того, якщо ви хочете пов'язати створення відповіді з питанням, тоді A було б краще, тому що якщо ви переходите з C, то він ховає, коли відповідь призначена на питання. Також, читаючи текст у А, слід розрізнити "поведінку моделі" та хто ініціює цю поведінку. Питання може бути відповідальним за створення відповідей, коли йому потрібно ініціалізувати відповідь якимось чином. Це не має нічого спільного з "створенням відповідей користувачем".
Ейфорія

Тільки для запису, Я розірваний між C&E :) Тепер це: "... примушуючи призначати відповідь на питання для його збереження, це сховище." Це означає, що "обов'язкова" частина приходить лише тоді, коли ми зайшли до сховища. Таким чином, обов'язкове підключення не є "видимим" для розробника під час компіляції, і бізнес-правила просочуються у сховище. Ось чому я тестую [C] тут. Можливо, ця розмова може дати більше інформації про те, про що я думаю, що мова йде про C.
igor

Це: "... хочу пов'язати створення відповіді з питанням ...". Я не хочу пов'язувати сам _створення . Просто хочу висловити обов'язкові відносини (особисто мені подобається вміти створювати модельні об’єкти, коли це можливо). Отже, на мій погляд, справа не в тому, щоб створити, ось чому я скоро кину А і Б. Я не бачу, що Питання відповідає за створення відповіді.
ігор

1

Я б пішов або [C], або [E].

По-перше, чому б не A і B? Я не хочу, щоб моє запитання відповідало за створення будь-якого пов'язаного значення. Уявіть, якщо у "Питання" багато інших значущих об'єктів - ви б застосували createметод для кожного? Або якщо є якісь складні агрегати, той самий випадок.

Чому б не [D]? Тому що це протилежне тому, що ми маємо в природі. Спочатку ми створюємо питання. Ви можете уявити веб-сторінку, де ви створюєте все це - користувач спочатку створить питання, правда? Тому не Д.

[E] є KISS, як сказав @Euphoric. Але мені теж недавно починають подобатися [C]. Це не так заплутано, як здається. Більше того, уявіть, якщо питання залежить від кількох речей - тоді розробник повинен знати, що йому потрібно поставити всередину питання, щоб його правильно ініціалізувати. Хоча ви маєте рацію - не існує «візуальної» мови, яка б пояснила, що відповідь насправді додається до питання.

Додаткове читання

Такі питання змушують мене замислитись, чи наші комп'ютерні мови занадто загальні для моделювання. (Я розумію, що вони повинні бути загальними, щоб відповідати на всі вимоги до програмування). Останнім часом я намагаюся знайти кращий спосіб виразити ділову мову за допомогою вільних інтерфейсів. Щось подібне (мовою судо):

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

тобто намагаються відійти від будь-яких великих * класів служб і * репозиторіїв до менших шматочків бізнес-логіки. Просто ідея.


Ви говорите в додатку про мови, що належать до домену?
юрист

Тепер, коли ви згадали, це виглядає так :) Купуйте, я не маю жодного значного досвіду з цим.
ігор

2
Я думаю, до цього часу існує консенсус щодо того, що IO є ортогональною відповідальністю, і тому вони не повинні оброблятись сутностями (storeToRepo)
Есбен Сков Педерсен

Я згоден @Esben Skov Pedersen, що суб'єкт господарювання сам не повинен викликати РЕПО всередині (ось що ви сказали, правда?); але, як AFAIU, тут у нас є якась модель побудови, яка викликає команди; тому IO тут не робиться в сутності. Принаймні, так я зрозумів це;)
юрист

@lawpert це правильно. Я не бачу, як це має працювати, але було б цікаво.
Есбен Сков Педерсен

1

Я вважаю, що ви пропустили крапку тут, ваш корінний агрегат повинен бути вашим тестовим об'єктом.

І якщо це дійсно так, я вважаю, що TestFactory найкраще відповість на вашу проблему.

Ви б делегували будівництво "Питання та відповіді" на "Фабрику", і тому ви могли в основному використовувати будь-яке рішення, про яке ви думали, не пошкоджуючи свою модель, оскільки ви ховаєте клієнта, як інстанціювати свої суб'єкти.

Це, поки TestFactory є єдиним інтерфейсом, який ви використовуєте для інстанціювання свого тесту.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.