Modéliser les relations avec DDD (ou avec sens)?

9

Voici une exigence simplifiée:

L'utilisateur crée un Questionavec plusieurs Answers. Questiondoit en avoir au moins un Answer.

Clarification: pensez Questionet Answercomme dans un test : il y a une question, mais plusieurs réponses, où peu peuvent être correctes. L'utilisateur est l'acteur qui prépare ce test, il crée donc des questions et des réponses.

J'essaie de modéliser cet exemple simple pour 1) correspondre au modèle de la vie réelle 2) pour être expressif avec le code, afin de minimiser les abus et les erreurs potentiels, et de donner des conseils aux développeurs sur la façon d'utiliser le modèle.

La question est une entité , tandis que la réponse est un objet de valeur . La question contient des réponses. Jusqu'à présent, j'ai ces solutions possibles.

[A] Usine à l'intérieurQuestion

Au lieu de créer Answermanuellement, nous pouvons appeler:

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

Cela va créer une réponse et l' ajouter à la question. Ensuite, nous pouvons manipuler la réponse en définissant ses propriétés. De cette façon, seules les questions peuvent créer une réponse. De plus, nous empêchons d'avoir une réponse sans question. Pourtant, nous n'avons aucun contrôle sur la création de réponses, car cela est codé en dur dans le Question.

Il y a aussi un problème avec la «langue» du code ci-dessus. L'utilisateur est celui qui crée les réponses, pas la question. Personnellement, je n'aime pas que nous créons un objet de valeur et en fonction du développeur pour le remplir de valeurs - comment peut-il être sûr de ce qu'il faut ajouter?

[B] Usine à l'intérieur de la question, prenez # 2

Certains disent que nous devrions avoir ce genre de méthode dans Question:

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

Semblable à la solution ci-dessus, cette méthode prend des données obligatoires pour la réponse et en crée une qui sera également ajoutée à la question.

Le problème ici est que nous dupliquons le constructeur de la Answersans raison valable. De plus, la question crée-t-elle vraiment une réponse?

[C] Dépendances du constructeur

Soyons libres de créer les deux objets par nous-mêmes. Exprimons également le droit de dépendance dans le constructeur:

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

Cela donne des conseils au développeur, car la réponse ne peut pas être créée sans une question. Cependant, nous ne voyons pas la «langue» qui dit que la réponse est «ajoutée» à la question. D'un autre côté, avons-nous vraiment besoin de le voir?

[D] Dépendance constructeur, prendre # 2

On peut faire le contraire:

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

C'est la situation inverse de ci-dessus. Ici, les réponses peuvent exister sans question (ce qui n'a pas de sens), mais la question ne peut exister sans réponse (qui a du sens). En outre, la « langue » est ici plus clair sur cette question sera avoir les réponses.

[E] Voie commune

C'est ce que j'appelle la voie commune, la première chose que ppl fait habituellement:

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

qui est une version «lâche» des deux réponses ci-dessus, car la réponse et la question peuvent exister l'une sans l'autre. Il n'y a aucune indication particulière que vous devez les lier ensemble.

[F] Combiné

Ou devrais-je combiner C, D, E - pour couvrir toutes les façons dont la relation peut être établie, afin d'aider les développeurs à utiliser ce qui leur convient le mieux.

Question

Je sais que les gens peuvent choisir l'une des réponses ci-dessus en fonction du «pressentiment». Mais je me demande si l'une des variantes ci-dessus est meilleure que l'autre avec une bonne raison à cela. De plus, s'il vous plaît ne pensez pas à l'intérieur de la question ci-dessus, je voudrais présenter ici certaines des meilleures pratiques qui pourraient être appliquées à la plupart des cas - et si vous êtes d'accord, la plupart des cas d'utilisation de la création de certaines entités sont similaires. En outre, laisse la technologie agnostique ici, par exemple. Je ne veux pas penser si l'ORM va être utilisé ou non. Je veux juste un bon mode expressif.

Une sagesse à ce sujet?

ÉDITER

Veuillez ignorer les autres propriétés de Questionet Answer, elles ne sont pas pertinentes pour la question. J'ai édité le texte ci-dessus et changé la plupart des constructeurs (si nécessaire): maintenant ils acceptent toutes les valeurs de propriété nécessaires. Cela peut être juste une chaîne de questions, ou une carte de chaînes dans différentes langues, statuts, etc. - quelles que soient les propriétés transmises, elles ne sont pas un objectif pour cela;) Supposons donc simplement que nous dépassons les paramètres nécessaires, sauf indication contraire. Merci!

Lawpert
la source

Réponses:

6

Mise à jour. Précisions prises en compte.

On dirait que c'est un domaine à choix multiples, qui a généralement les exigences suivantes

  1. une question doit avoir au moins deux choix pour que vous puissiez choisir parmi
  2. il doit y avoir au moins un bon choix
  3. il ne devrait pas y avoir de choix sans question

Basé sur ce qui précède

[A] ne peut pas garantir l'invariant du point 1, vous pouvez vous retrouver avec une question sans choix

[B] présente le même inconvénient que [A]

[C] présente le même inconvénient que [A] et [B]

[D] est une approche valide, mais il vaut mieux passer les choix sous forme de liste plutôt que de les passer individuellement

[E] présente le même inconvénient que [A] , [B] et [C]

Par conséquent, je choisirais [D] car cela permet de garantir que les règles de domaine des points 1, 2 et 3 sont suivies. Même si vous dites qu'il est très peu probable qu'une question reste sans choix pendant une longue période, c'est toujours une bonne idée de transmettre les exigences du domaine via le code.

Je voudrais également renommer le Answerto Choicecar cela a plus de sens pour moi dans ce domaine.

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

Une note. Si vous faites de l' Questionentité une racine agrégée et que l' Choiceobjet valeur fait partie du même agrégat, il n'y a aucune chance que l'on puisse stocker un Choicesans qu'il soit affecté à un Question(même si vous ne passez pas une référence directe à l' Questionargument comme argument à l'argument Choiceconstructeur), car les référentiels fonctionnent uniquement avec des racines et une fois que vous avez créé votre, Questionvous avez tous vos choix qui lui sont assignés dans le constructeur.

J'espère que cela t'aides.

MISE À JOUR

Si cela vous dérange vraiment comment les choix sont créés avant leur question, il y a quelques astuces que vous pourriez trouver utiles

1) Réorganisez le code de sorte qu'il semble qu'il soit créé après la question ou au moins en même temps

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

2) Masquer les constructeurs et utiliser une méthode d'usine statique

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) Utilisez le modèle de générateur

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

Cependant, tout dépend de votre domaine. La plupart du temps, l'ordre de création des objets n'est pas important du point de vue du problème. Ce qui est plus important, c'est que dès que vous obtenez une instance de votre classe, elle est logiquement complète et prête à l'emploi.


Dépassé. Tout ce qui suit n'est pas pertinent pour la question après des clarifications.

Tout d'abord, selon le modèle de domaine DDD devrait avoir un sens dans le monde réel. Par conséquent, peu de points

  1. une question peut ne pas avoir de réponse
  2. il ne devrait pas y avoir de réponse sans question
  3. une réponse doit correspondre à exactement une question
  4. une réponse "vide" ne répond pas à une question

Basé sur ce qui précède

[A] peut contredire le point 4 car il est facile de mal utiliser et d'oublier de définir le texte.

[B] est une approche valide mais nécessite des paramètres facultatifs

[C] peut contredire le point 4 car il permet une réponse sans texte

[D] contredit le point 1 et peut contredire les points 2 et 3

[E] peut contredire les points 2, 3 et 4

Deuxièmement, nous pouvons utiliser les fonctionnalités de POO pour appliquer la logique du domaine. A savoir, nous pouvons utiliser des constructeurs pour les paramètres requis et des setters pour ceux optionnels.

Troisièmement, j'utiliserais le langage omniprésent qui est censé être plus naturel pour le domaine.

Et enfin, nous pouvons tout concevoir en utilisant des modèles DDD comme des racines d'agrégats, des entités et des objets de valeur. Nous pouvons faire de la question une racine de son agrégat et la réponse en faire partie. Il s'agit d'une décision logique car une réponse n'a pas de sens en dehors du contexte d'une question.

Donc, tout ce qui précède se résume à la conception suivante

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 En réponse à votre question, j'ai fait quelques hypothèses sur votre domaine qui pourraient ne pas être correctes, alors n'hésitez pas à ajuster ce qui précède avec vos spécificités.

zafarkhaja
la source
1
Pour résumer: il s'agit d'un mélange de B et C. Veuillez voir ma clarification des exigences. Votre point 1. peut exister uniquement pour une «courte» période de temps, tout en construisant une question; mais pas dans la base de données. En ce sens, 4. ne devrait jamais arriver. J'espère que maintenant les exigences sont claires;)
lawpert
Btw, avec la clarification, il me semble que ce serait addAnswerou assignAnswerserait un meilleur langage que juste answer, j'espère que vous êtes d'accord là-dessus. Quoi qu'il en soit, ma question est - iriez-vous toujours pour le B et par exemple avoir la copie de la plupart des arguments dans la méthode de réponse? Ne s'agirait-il pas d'une duplication?
lawpert
Désolé pour les exigences peu claires, seriez-vous si aimable de mettre à jour la réponse?
lawpert
1
Il s'avère que mes hypothèses étaient incorrectes. J'ai traité votre domaine QA comme un exemple de sites Web stackexchange, mais cela ressemble plus à un test à choix multiples. Bien sûr, je mettrai à jour ma réponse.
zafarkhaja
1
@lawpert Answerest un objet de valeur, il va être stocké avec une racine agrégée de son agrégat. Vous ne stockez pas directement les objets de valeur, et vous n'enregistrez pas d'entités si elles ne sont pas des racines de leurs agrégats.
zafarkhaja
1

Dans le cas où les exigences sont si simples, qu'il existe plusieurs solutions possibles, le principe KISS doit être suivi. Dans votre cas, ce serait l'option E.

Il y a aussi le cas de créer du code qui exprime quelque chose, qu'il ne devrait pas. Par exemple, lier la création de réponses à la question (A et B) ou donner une référence de réponse à la question (C et D) ajoute un comportement qui n'est pas nécessaire au domaine et peut être déroutant. De plus, dans votre cas, la question serait très probablement agrégée avec la réponse et la réponse serait un type de valeur.

Euphorique
la source
1
Pourquoi [C] est un comportement inutile ? Comme je le vois, [C] communique que la réponse ne peut pas vivre sans une question, et c'est exactement ce que c'est. De plus, imaginez si Answer nécessite des indicateurs supplémentaires (par exemple, type de réponse, catégorie, etc.) qui sont obligatoires. En allant KISS, nous perdons cette connaissance de ce qui est obligatoire, et le développeur doit savoir à l'avance ce qu'il doit ajouter / définir à la réponse afin de bien faire les choses. Je crois que la question n'était pas ici de modéliser cet exemple très simple, mais de trouver la meilleure pratique pour écrire un langage omniprésent en utilisant OO.
igor
@igor E communique déjà que la réponse fait partie de la question en rendant obligatoire l'attribution de la réponse à la question pour qu'elle soit enregistrée dans le référentiel. S'il y avait un moyen de sauvegarder simplement Answer sans charger sa question, alors C serait mieux. Mais ce n'est pas évident d'après ce que vous avez écrit.
Euphoric
@igor Aussi, si vous voulez lier la création de la réponse à la question, alors A serait mieux, car si vous choisissez C, il se cache lorsque la réponse est affectée à la question. De plus, en lisant votre texte en A, vous devez différencier le «comportement du modèle» et qui initie ce comportement. La question peut être responsable de la création de réponses, lorsqu'elle doit initialiser la réponse d'une manière ou d'une autre. Cela n'a rien à voir avec "la création de réponses par l'utilisateur".
Euphoric
Juste pour mémoire, je suis partagé entre les C&E :) Maintenant, ceci: "... en rendant obligatoire l'attribution de la réponse à la question pour qu'elle soit enregistrée est un référentiel." Cela signifie que la partie «obligatoire» ne vient que lorsque nous arrivons au référentiel. La connexion obligatoire n'est donc pas «visible» pour le développeur au moment de la compilation et les règles métier fuient dans le référentiel. C'est pourquoi je teste le [C] ici. Peut - être que cet exposé peut donner plus d'informations sur ce que je pense que l'option C concerne.
igor
Ceci: "... vous voulez lier la création de la réponse à la question ...". Je ne veux pas lier la _création elle-même. Je veux juste exprimer la relation obligatoire (j'aime personnellement pouvoir créer des objets modèles par moi-même, si possible). Donc, à mon avis, il ne s'agit pas de créer, c'est pourquoi je laisse tomber A et B bientôt. Je ne vois pas que la question est responsable de la création de la réponse.
igor
1

J'irais soit [C] ou [E].

D'abord, pourquoi pas A et B? Je ne veux pas que ma question soit responsable de la création d'une valeur associée. Imaginez si Question a beaucoup d'autres objets de valeur - mettriez-vous la createméthode pour chacun? Ou s'il existe des agrégats complexes, le même cas.

Pourquoi pas [D]? Parce que c'est contraire à ce que nous avons dans la nature. Nous créons d'abord une question. Vous pouvez imaginer une page Web où vous créez tout cela - l'utilisateur créerait d'abord une question, non? Par conséquent, pas D.

[E] est KISS, comme l'a dit @Euphoric. Mais je commence aussi à aimer [C] récemment. Ce n'est pas aussi déroutant qu'il n'y paraît. De plus, imaginez si la question dépend de plus de choses - alors le développeur doit savoir ce qu'il doit mettre à l'intérieur de la question pour l'initialiser correctement. Bien que vous ayez raison - il n'y a pas de langage «visuel» expliquant que la réponse est réellement ajoutée à la question.

Lecture complémentaire

Des questions comme celle-ci me font me demander si nos langages informatiques sont trop génériques pour la modélisation. (Je comprends qu'ils doivent être génériques pour répondre à toutes les exigences de programmation). Récemment, j'essaie de trouver une meilleure façon d'exprimer le langage des affaires en utilisant des interfaces fluides. Quelque chose comme ça (en langue sudo):

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

c'est-à-dire essayer de s'éloigner de n'importe quel gros * Services et * classes de référentiel vers de plus petits morceaux de logique métier. Juste une idée.

igor
la source
Vous parlez dans l'addon des langages spécifiques au domaine?
lawpert
Maintenant, quand vous l'avez mentionné, il semble si :) Acheter Je n'ai pas d'expérience significative avec cela.
igor
2
Je pense qu'il y a un consensus maintenant que les E / S sont une responsabilité orthogonale et ne devraient donc pas être gérées par des entités (storeToRepo)
Esben Skov Pedersen
J'accepte @Esben Skov Pedersen que l'entité elle-même ne devrait pas appeler repo inside (c'est ce que vous avez dit, non?); mais comme AFAIU ici, nous avons une sorte de modèle de générateur derrière qui appelle des commandes; donc IO ne se fait pas dans l'entité ici. C'est du moins ce que j'ai compris;)
Lawpert
@lawpert qui est correct. Je ne vois pas comment cela est censé fonctionner, mais ce serait intéressant.
Esben Skov Pedersen
1

Je crois que vous avez raté un point ici, votre racine agrégée devrait être votre entité de test.

Et si c'est vraiment le cas, je pense qu'une TestFactory serait la mieux adaptée pour répondre à votre problème.

Vous délégueriez le bâtiment des questions et réponses à la fabrique et, par conséquent, vous pourriez essentiellement utiliser n'importe quelle solution à laquelle vous pensiez sans corrompre votre modèle, car vous cachez au client la façon dont vous instanciez vos sous-entités.

C'est, tant que la TestFactory est la seule interface que vous utilisez pour instancier votre test.

Alexandre BODIN
la source