Модель отношений с DDD (или со смыслом)?

9

Вот упрощенное требование:

Пользователь создает Questionс несколькими Answerс. 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 или нет. Просто хочется хорошего, выразительного режима.

Есть ли в этом какая-то мудрость?

РЕДАКТИРОВАТЬ

Пожалуйста, игнорируйте другие свойства Questionи Answer, они не имеют отношения к вопросу. Я отредактировал приведенный выше текст и изменил большинство конструкторов (при необходимости): теперь они принимают любые необходимые значения свойств. Это может быть просто строка вопроса или карта строк на разных языках, статусы и т. Д. - независимо от того, какие свойства переданы, они не являются фокусом для этого;) Поэтому просто предположим, что мы выше передачи необходимых параметров, если не указано иное. Thanx!

lawpert
источник

Ответы:

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объект значения - частью одной и той же совокупности, нет никаких шансов, что можно сохранить объект 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

[E] может противоречить пунктам 2, 3 и 4

Во-вторых, мы можем использовать функции ООП для обеспечения логики домена. А именно мы можем использовать конструкторы для обязательных параметров и установщики для необязательных.

В-третьих, я бы использовал вездесущий язык, который должен быть более естественным для домена.

И, наконец, мы можем спроектировать все это, используя шаблоны 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 Отвечая на ваш вопрос, я сделал несколько предположений о вашем домене, которые могут быть неверными, поэтому не стесняйтесь подгонять вышесказанное с учетом ваших особенностей.

zafarkhaja
источник
1
Подводя итог: это сочетание B и C. Пожалуйста, смотрите мое разъяснение требований. Ваша точка 1. может существовать только в течение «короткого» периода времени при построении вопроса; но не в базе данных. В этом смысле 4. никогда не должно происходить. Надеюсь теперь требования понятны;)
lawpert
Кстати, с разъяснениями, мне кажется, что это будет addAnswerили assignAnswerбудет лучше, чем просто answer, я надеюсь, вы согласны с этим. В любом случае, мой вопрос - вы все равно выбрали бы B и, к примеру, получили копию большинства аргументов в методе ответа? Разве это не было бы дублированием?
Lawpert
Извините за неясные требования, не могли бы вы обновить ответ?
Lawpert
1
Оказывается, мои предположения были неверны. Я рассматривал ваш домен QA как пример веб-сайтов для обмена стеками, но это больше похоже на тест с множественным выбором. Конечно, я обновлю свой ответ.
Зафархайя
1
@lawpert Answer- это объект значения, он будет храниться вместе с корнем агрегата своего агрегата. Вы не сохраняете объекты-значения напрямую и не сохраняете сущности, если они не являются корнями их агрегатов.
Зафархая
1

В случае, когда требования настолько просты, что существует множество возможных решений, следует следовать принципу KISS. В вашем случае это будет вариант Е.

Существует также случай создания кода, который выражает что-то, чего не должно быть. Например, связывание создания Ответов на вопрос (A и B) или предоставление ссылки на ответ на вопрос (C и D) добавляет некоторое поведение, которое не является необходимым для домена и может привести к путанице. Кроме того, в вашем случае Вопрос, скорее всего, будет агрегирован с Ответом, а Ответ будет типом значения.

Euphoric
источник
1
Почему [C] ненужное поведение? Как я понимаю, [C] сообщает, что Ответ не может жить без Вопроса, и это именно то, что есть. Кроме того, представьте, если для ответа требуются дополнительные флажки (например, тип ответа, категория и т. Д.), Которые являются обязательными. Что касается KISS, мы теряем знание о том, что является обязательным, и разработчик должен знать заранее, что ему нужно добавить / установить в ответ, чтобы сделать его правильным. Я считаю, что здесь вопрос состоял не в том, чтобы смоделировать этот очень простой пример, а в том, чтобы найти лучшую практику написания вездесущего языка с использованием ОО.
Игорь
@igor E уже сообщает, что Ответ является частью Вопроса, делая обязательным назначение Ответа на вопрос для сохранения в репозитории. Если бы был способ сохранить только Ответ, не загружая его вопрос, то C был бы лучше. Но это не очевидно из того, что вы написали.
Эйфорическая
@igor Кроме того, если вы хотите связать создание ответа с вопросом, то A будет лучше, потому что если вы перейдете с C, то он будет скрыт, когда ответ назначен на вопрос. Также, читая ваш текст на А, вы должны различать «модельное поведение» и того, кто инициирует это поведение. Вопрос может отвечать за создание ответов, когда нужно каким-то образом инициализировать ответ. Это не имеет ничего общего с "пользователь создает ответы".
Эйфорический
Просто для записи, я разрываюсь между C & E :) Теперь, это: «... сделав обязательным присваивать ответ на вопрос, чтобы сохранить его, это хранилище». Это означает, что «обязательная» часть появляется только тогда, когда мы заходим в хранилище. Таким образом, обязательное соединение не «видно» разработчику во время компиляции, и бизнес-правила просачиваются в репозиторий. Вот почему я тестирую [C] здесь. Может быть, этот доклад может дать больше информации о том, что я думаю о C вариант.
Игорь
Это: «... хочу связать создание ответа с вопросом ...». Я не хочу связывать само создание . Просто хочу выразить обязательные отношения . (Лично хотел бы иметь возможность создавать модели объектов самостоятельно, когда это возможно). Так что, на мой взгляд, речь идет не о создании, поэтому я скоро бросаю A и B. Я не вижу, чтобы Вопрос отвечал за создание ответа.
Игорь
1

Я бы пошел либо [C], либо [E].

Во-первых, почему не А и Б? Я не хочу, чтобы мой Вопрос отвечал за создание какой-либо связанной ценности. Представьте себе, если у Вопроса есть много других ценностных объектов - вы бы поставили createметод для каждого? Или, если есть какие-то сложные агрегаты, тот же случай.

Почему бы не [D]? Потому что это противоположно тому, что мы имеем в природе. Сначала мы создаем вопрос. Вы можете представить себе веб-страницу, где вы все это создадите - пользователь сначала создаст вопрос, верно? Следовательно, не D.

[E] это KISS, как сказал @Euphoric. Но я также начинаю любить [C] в последнее время. Это не так запутанно, как кажется. Кроме того, представьте, если Вопрос зависит от большего количества вещей - тогда разработчик должен знать, что ему нужно поместить в Вопрос, чтобы правильно его инициализировать. Хотя вы правы - не существует «визуального» языка, объясняющего, что ответ на самом деле добавляется к вопросу.

Дополнительное чтение

Подобные вопросы заставляют меня задуматься, не являются ли наши компьютерные языки слишком общими для моделирования. (Я понимаю, что они должны быть общими, чтобы ответить на все требования программирования). В последнее время я пытаюсь найти лучший способ выразить бизнес-язык, используя свободные интерфейсы. Примерно так (на языке sudo):

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

то есть, пытаясь отойти от любых больших * Services и * Repository классов на более мелкие куски бизнес-логики. Просто идея.

игорь
источник
Вы говорите в аддоне о предметно-ориентированных языках?
Lawpert
Теперь, когда вы упомянули, это выглядит так :) Купить у меня нет никакого значительного опыта с ним.
Игорь
2
Я думаю, что к настоящему моменту существует консенсус, что IO - это ортогональная повторяемость и, следовательно, не должны обрабатываться сущностями (storeToRepo)
Эсбен Сков Педерсен
Я согласен с @Esben Skov Pedersen, что сама сущность не должна вызывать репо внутри (это то, что вы сказали, верно?); но, как AFAIU здесь, у нас есть некая модель построения, которая вызывает команды; поэтому IO здесь не делается. По крайней мере, так я это понял;)
Lawpert
@lawpert это правильно. Я не понимаю, как это должно работать, но было бы интересно.
Эсбен Сков Педерсен
1

Я полагаю, что вы упустили момент, ваш Агрегированный корень должен быть вашим Тестовым объектом.

И если это действительно так, я считаю, что TestFactory лучше всего подходит для решения вашей проблемы.

Вы бы делегировали здание вопросов и ответов на фабрику, и, следовательно, вы могли бы использовать любое решение, о котором думали, не повреждая вашу модель, потому что вы скрываете от клиента то, как вы реализуете свои дочерние объекты.

Это так, если TestFactory - единственный интерфейс, который вы используете для создания экземпляра Test.

Александр БОДИН
источник