Modelar relacionamentos com DDD (ou com senso)?

9

Aqui está um requisito simplificado:

O usuário cria um Questioncom vários Answers. Questiondeve ter pelo menos um Answer.

Esclarecimento: pense Questione Answercomo em um teste : há uma pergunta, mas várias respostas, onde poucas podem estar corretas. Usuário é o ator que está preparando este teste, portanto, ele cria perguntas e respostas.

Estou tentando modelar este exemplo simples para 1) corresponder ao modelo da vida real 2) para ser expressivo com o código, para minimizar possíveis usos indevidos e erros e dar dicas aos desenvolvedores sobre como usar o modelo.

Pergunta é uma entidade , enquanto resposta é objeto de valor . A pergunta contém respostas. Até agora, eu tenho essas soluções possíveis.

[A] fábrica dentroQuestion

Em vez de criar Answermanualmente, podemos chamar:

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

Isso criará uma resposta e a adicionará à pergunta. Em seguida, podemos manipular a resposta definindo suas propriedades. Dessa forma, apenas perguntas podem criar uma resposta. Além disso, evitamos ter uma resposta sem uma pergunta. No entanto, não temos controle sobre a criação de respostas, pois isso é codificado no Question.

Há também um problema com o 'idioma' do código acima. Usuário é quem cria respostas, não a pergunta. Pessoalmente, não gosto de criar objetos de valor e, dependendo do desenvolvedor, preenchê-los com valores - como ele pode ter certeza do que é necessário adicionar?

[B] Fábrica dentro da pergunta, pegue a # 2

Alguns dizem que devemos ter esse tipo de método em Question:

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

Semelhante à solução acima, esse método utiliza dados obrigatórios para a resposta e cria um que também será adicionado à pergunta.

O problema aqui é que duplicamos o construtor do Answersem nenhuma boa razão. Além disso, a pergunta realmente cria uma resposta?

Dependências do construtor [C]

Sejamos livres para criar os dois objetos por nós mesmos. Vamos também expressar a dependência correta no construtor:

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

Isso fornece dicas para o desenvolvedor, pois a resposta não pode ser criada sem uma pergunta. No entanto, não vemos o 'idioma' que diz que a resposta é 'adicionada' à pergunta. Por outro lado, precisamos mesmo vê-lo?

[D] Dependência de construtor, pegue # 2

Podemos fazer o oposto:

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

Esta é a situação oposta de cima. Aqui as respostas podem existir sem uma pergunta (que não faz sentido), mas a pergunta não pode existir sem resposta (que faz sentido). Além disso, a 'linguagem' aqui é mais clara sobre essa questão vai ter as respostas.

[E] maneira comum

Isto é o que chamo de maneira comum, a primeira coisa que as pessoas costumam fazer:

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

que é a versão 'solta' das duas respostas acima, já que a resposta e a pergunta podem existir uma sem a outra. Não há nenhum indício especial que você tem para uni-los.

[F] Combinado

Ou devo combinar C, D, E - para cobrir todas as maneiras pelas quais o relacionamento pode ser feito, para ajudar os desenvolvedores a usar o que é melhor para eles.

Questão

Eu sei que as pessoas podem escolher uma das respostas acima com base no 'palpite'. Mas eu me pergunto se alguma das variantes acima é melhor que a outra com uma boa razão para isso. Além disso, não pense na pergunta acima. Gostaria de incluir aqui algumas práticas recomendadas que podem ser aplicadas na maioria dos casos - e, se você concordar, na maioria dos casos de uso de criação, algumas entidades são semelhantes. Além disso, vamos tornar a tecnologia independente aqui, por exemplo. Não quero pensar se o ORM será usado ou não. Só quero um modo expressivo e bom.

Alguma sabedoria nisso?

EDITAR

Ignore outras propriedades de Questione Answer, elas não são relevantes para a pergunta. Editei o texto acima e alterei a maioria dos construtores (onde necessário): agora eles aceitam qualquer um dos valores de propriedade necessários. Isso pode ser apenas uma sequência de perguntas ou um mapa de sequências em diferentes idiomas, status etc. - quaisquer que sejam as propriedades passadas, elas não são o foco disso;) Portanto, assuma que estamos acima de passar os parâmetros necessários, a menos que sejam diferentes. Thanx!

Lawpert
fonte

Respostas:

6

Atualizada. Esclarecimentos levados em conta.

Parece que este é um domínio de múltipla escolha, que geralmente possui os seguintes requisitos

  1. uma pergunta deve ter pelo menos duas opções para que você possa escolher entre
  2. deve haver pelo menos uma escolha correta
  3. não deve haver uma escolha sem uma pergunta

Com base no acima

[A] não pode garantir a invariante do ponto 1, você pode terminar com uma pergunta sem escolha

[B] tem a mesma desvantagem que [A]

[C] tem a mesma desvantagem que [A] e [B]

[D] é uma abordagem válida, mas é melhor passar as opções como uma lista do que passá-las individualmente

[E] tem a mesma desvantagem que [A] , [B] e [C]

Portanto, eu usaria [D] porque ele permite garantir que as regras de domínio dos pontos 1, 2 e 3 sejam seguidas. Mesmo se você disser que é muito improvável que uma pergunta permaneça sem escolha por um longo período de tempo, é sempre uma boa idéia transmitir os requisitos de domínio por meio do código.

Eu também renomearia o Answerpara Choice, pois faz mais sentido para mim nesse domínio.

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

Uma nota. Se você transformar a Questionentidade em uma raiz agregada e o Choiceobjeto de valor fizer parte da mesma agregação, não haverá chances de armazenar um Choicesem que ele seja atribuído a um Question(mesmo que você não passe uma referência direta ao Questionargumento como Choicedo construtor), porque os repositórios funcionam apenas com raízes e, quando você constrói o seu, Questionvocê tem todas as suas opções atribuídas a ele no construtor.

Espero que isto ajude.

ATUALIZAR

Se realmente lhe incomoda como as escolhas são criadas antes da pergunta, existem alguns truques que você pode achar úteis

1) Reorganize o código para que pareça que ele foi criado após a pergunta ou pelo menos ao mesmo tempo

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

2) Ocultar construtores e usar um método estático de fábrica

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) Use o padrão do construtor

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

No entanto, tudo depende do seu domínio. Na maioria das vezes, a ordem de criação dos objetos não é importante da perspectiva do domínio do problema. O mais importante é que, assim que você obtém uma instância de sua classe, ela está logicamente completa e pronta para uso.


Desatualizado. Tudo abaixo é irrelevante para a questão após esclarecimentos.

Primeiro de tudo, de acordo com o modelo de domínio DDD deve fazer sentido no mundo real. Portanto, alguns pontos

  1. uma pergunta pode não ter respostas
  2. não deve haver uma resposta sem uma pergunta
  3. uma resposta deve corresponder exatamente a uma pergunta
  4. uma resposta "vazia" não responde a uma pergunta

Com base no acima

[A] pode contradizer o ponto 4, porque é fácil usar mal e esquecer de definir o texto.

[B] é uma abordagem válida, mas requer parâmetros opcionais

[C] pode contradizer o ponto 4 porque permite uma resposta sem texto

[D] contradiz o ponto 1 e pode contradizer os pontos 2 e 3

[E] pode contradizer os pontos 2, 3 e 4

Em segundo lugar, podemos usar os recursos do OOP para aplicar a lógica do domínio. Ou seja, podemos usar construtores para os parâmetros necessários e setters para os opcionais.

Terceiro, eu usaria a linguagem onipresente que deveria ser mais natural para o domínio.

E, finalmente, podemos projetar tudo isso usando padrões DDD, como raízes agregadas, entidades e objetos de valor. Podemos fazer da pergunta uma raiz de seu agregado e a resposta como parte dela. Esta é uma decisão lógica, porque uma resposta não tem significado fora do contexto de uma pergunta.

Portanto, todos os itens acima se resumem ao seguinte design

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 Respondendo à sua pergunta, fiz algumas suposições sobre o seu domínio que podem não estar corretas. Portanto, sinta-se à vontade para ajustar o que precede com suas especificações.

zafarkhaja
fonte
11
Para resumir: esta é uma mistura de B e C. Por favor, veja meus esclarecimentos sobre os requisitos. Seu ponto 1. pode existir apenas por um período 'curto' de tempo, enquanto você cria uma pergunta; mas não no banco de dados. Nesse sentido, 4. nunca deve acontecer. Espero que agora os requisitos estejam claros;) #
lawpert
Btw, com o esclarecimento, para mim parece que addAnswerou assignAnswerseria uma linguagem melhor do que apenas answer, espero que você concorde com isso. De qualquer forma, minha pergunta é: você ainda escolheria o B e, por exemplo, teria a cópia da maioria dos argumentos no método de resposta? Isso não seria duplicação?
lawpert
Desculpe pelos requisitos pouco claros, você gostaria de atualizar a resposta?
lawpert
11
Acontece que minhas suposições estavam incorretas. Tratei seu domínio de controle de qualidade como um exemplo de sites de stackexchange, mas parece mais um teste de múltipla escolha. Claro, vou atualizar minha resposta.
Zafarkhaja ​​22/10
11
O @lawpert Answeré um objeto de valor, que será armazenado com uma raiz agregada de seu agregado. Você não armazena objetos de valor diretamente nem salva entidades se elas não forem raízes de seus agregados.
Zafarkhaja
1

Caso os requisitos sejam tão simples, que existam várias soluções possíveis, o princípio do KISS deve ser seguido. No seu caso, essa seria a opção E.

Há também o caso de criar código que expressa algo, que não deveria. Por exemplo, amarrando a criação das Respostas à Pergunta (A e B) ou fornecendo referência de Resposta à Pergunta (C e D), adicione um comportamento que não é necessário para o domínio e pode ser confuso. Além disso, no seu caso, a pergunta provavelmente seria agregada à resposta e a resposta seria um tipo de valor.

Eufórico
fonte
11
Por que [C] é um comportamento desnecessário ? A meu ver, [C] comunica que a resposta não pode viver sem uma pergunta, e é exatamente isso que é. Além disso, imagine se o Answer precisar de mais alguns sinalizadores (por exemplo, tipo de resposta, categoria etc.) obrigatórios. No KISS, estamos perdendo esse conhecimento do que é obrigatório, e o desenvolvedor deve saber de frente o que ele precisa adicionar / definir na resposta para corrigir isso. Creio que aqui a questão não era modelar esse exemplo muito simples, mas encontrar a melhor prática para escrever linguagem onipresente usando OO.
Igor #
O @igor E já comunica que a resposta faz parte da pergunta, tornando obrigatório atribuir a resposta à pergunta para que ela seja salva no repositório. Se houvesse uma maneira de salvar apenas a resposta sem carregar sua pergunta, C seria melhor. Mas isso não é óbvio pelo que você escreveu.
Euphoric
@igor Além disso, se você deseja vincular a criação da resposta à pergunta, A seria melhor, porque se você optar por C, ele oculta quando a resposta é atribuída à pergunta. Além disso, ao ler seu texto em A, você deve diferenciar o "comportamento do modelo" e quem inicia esse comportamento. A pergunta pode ser responsável pela criação de respostas, quando for necessário inicializá-la de alguma forma. Não tem nada a ver com "usuário criando respostas".
Euphoric
Só para constar, estou dividido entre C&E :) Agora, este: "... tornando obrigatório atribuir Resposta à pergunta para que ela seja salva é um repositório." Isso significa que a parte 'obrigatória' vem somente quando chegamos ao repositório. Portanto, a conexão obrigatória não é 'visível' para o desenvolvedor em tempo de compilação e as regras de negócios vazam no repositório. É por isso que estou testando o [C] aqui. Talvez essa palestra possa dar mais informações sobre o que eu acho que é a opção C.
igor 22/10
Isto: "... deseja vincular a criação da resposta à pergunta ...". Não quero amarrar a própria criação . Só quero expressar o relacionamento obrigatório . (Pessoalmente, gosto de poder criar objetos de modelo sozinho, quando possível). Então, na minha opinião, não se trata de criar, é por isso que abandono A e B em breve. Não vejo que a pergunta seja responsável por criar a resposta.
igor 22/10
1

Eu iria [C] ou [E].

Primeiro, por que não A e B? Não quero que minha pergunta seja responsável por criar qualquer valor relacionado. Imagine se a pergunta tiver muitos outros objetos de valor - você colocaria um createmétodo para cada um? Ou, se houver alguns agregados complexos, o mesmo caso.

Por que não [D]? Porque é oposto ao que temos na natureza. Primeiro, criamos uma pergunta. Você pode imaginar uma página da Web onde você cria tudo isso - o usuário primeiro criaria uma pergunta, certo? Portanto, não D.

[E] é o KISS, como @Euphoric disse. Mas também começo a gostar de [C] recentemente. Isso não é tão confuso quanto parece. Além disso, imagine se a pergunta depende de mais coisas - então o desenvolvedor deve saber o que ele precisa colocar dentro da pergunta para que ela seja inicializada corretamente. Embora você esteja certo - não há linguagem 'visual' explicando que a resposta é realmente adicionada à pergunta.

Leitura adicional

Perguntas como essa me fazem pensar se nossas linguagens de computador são muito genéricas para modelagem. (Entendo que eles precisam ser genéricos para responder a todos os requisitos de programação). Recentemente, estou tentando encontrar uma maneira melhor de expressar a linguagem comercial usando interfaces fluentes. Algo parecido com isto (na linguagem sudo):

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

ou seja, tentando se afastar de qualquer classe * Services e * Repository grande para pedaços menores da lógica de negócios. Apenas uma ideia.

igor
fonte
Você está falando sobre o addon sobre os idiomas específicos do domínio?
lawpert
Agora, quando você mencionou, parece tão :) Comprar Não tenho nenhuma experiência significativa com ele.
Igor #
2
Eu acho que há um consenso até agora que IO é uma resposibility ortogonal e, portanto, não devem ser manuseados por entidades (storeToRepo)
Esben Skov Pedersen
Concordo @Esben Skov Pedersen que a própria entidade não deve chamar repo por dentro (foi isso que você disse, certo?); mas como AFAIU aqui, temos algum tipo de padrão de construtor por trás que chama comandos; portanto, o IO não é feito na entidade aqui. Pelo menos é assim que Ive entendeu;)
lawpert
@lawpert está correto. Não vejo como deve funcionar, mas seria interessante.
Esben Skov Pedersen
1

Acredito que você perdeu um ponto aqui, sua raiz agregada deve ser sua entidade de teste.

E se realmente for o caso, acredito que um TestFactory seria o mais adequado para responder ao seu problema.

Você delegaria o edifício de perguntas e respostas à fábrica e, portanto, poderia usar basicamente qualquer solução que pensasse sem danificar seu modelo, porque está ocultando o cliente da maneira como instancia suas subentidades.

Isto é, desde que o TestFactory seja a única interface usada para instanciar seu teste.

Alexandre BODIN
fonte