Modellbeziehungen mit DDD (oder mit Sinn)?

9

Hier ist eine vereinfachte Anforderung:

Benutzer erstellt ein Questionmit mehreren Answers. Questionmuss mindestens eine haben Answer.

Klarstellung: Denken Sie Questionund Answerwie in einem Test : Es gibt eine Frage, aber mehrere Antworten, bei denen nur wenige richtig sein können. Der Benutzer ist der Schauspieler, der diesen Test vorbereitet, daher erstellt er Fragen und Antworten.

Ich versuche, dieses einfache Beispiel so zu modellieren, dass 1) es mit dem realen Modell übereinstimmt 2) mit dem Code ausdrucksstark ist, um potenziellen Missbrauch und Fehler zu minimieren und Entwicklern Hinweise zur Verwendung des Modells zu geben.

Frage ist eine Entität , während Antwort Wertobjekt ist . Frage enthält Antworten. Bisher habe ich diese möglichen Lösungen.

[A] Fabrik im InnerenQuestion

Anstatt Answermanuell zu erstellen , können wir Folgendes aufrufen:

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

Dadurch wird eine Antwort erstellt und der Frage hinzugefügt. Dann können wir die Antwort manipulieren, indem wir ihre Eigenschaften festlegen. Auf diese Weise können nur Fragen eine Antwort liefern. Wir verhindern auch, dass wir eine Antwort ohne Frage haben. Wir haben jedoch keine Kontrolle über das Erstellen von Antworten, da dies in der Question.

Es gibt auch ein Problem mit der 'Sprache' des obigen Codes. Der Benutzer ist derjenige, der Antworten erstellt, nicht die Frage. Persönlich mag ich es nicht, wenn wir ein Wertobjekt erstellen und es je nach Entwickler mit Werten füllen - wie kann er sicher sein, was hinzugefügt werden muss?

[B] Fabrik in Frage, nimm # 2

Einige sagen, wir sollten diese Art von Methode haben in Question:

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

Ähnlich wie bei der obigen Lösung verwendet diese Methode obligatorische Daten für die Antwort und erstellt eine, die ebenfalls zur Frage hinzugefügt wird.

Das Problem hierbei ist, dass wir den Konstruktor von ohne guten Grund duplizierenAnswer . Schafft die Frage wirklich eine Antwort?

[C] Konstruktorabhängigkeiten

Lassen Sie uns beide Objekte selbst erstellen. Lassen Sie uns auch das Abhängigkeitsrecht im Konstruktor ausdrücken:

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

Dies gibt dem Entwickler Hinweise, da die Antwort nicht ohne Frage erstellt werden kann. Wir sehen jedoch nicht die "Sprache", die besagt, dass die Antwort der Frage "hinzugefügt" wird. Müssen wir es auf der anderen Seite wirklich sehen?

[D] Konstruktorabhängigkeit, nimm # 2

Wir können das Gegenteil tun:

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

Dies ist die entgegengesetzte Situation von oben. Hier können Antworten ohne eine Frage existieren (was keinen Sinn ergibt), aber eine Frage kann nicht ohne Antwort existieren (was keinen Sinn ergibt). Auch die ‚Sprache‘ ist hier deutlicher zu dieser Frage wird hat die Antworten.

[E] Gemeinsamer Weg

Dies ist, was ich den üblichen Weg nenne, das erste, was Menschen normalerweise tun:

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

Dies ist eine "lose" Version der beiden oben genannten Antworten, da sowohl Antwort als auch Frage ohne einander existieren können. Es gibt keinen besonderen Hinweis , dass Sie haben sie zusammen zu binden.

[F] Kombiniert

Oder sollte ich C, D, E kombinieren, um alle Möglichkeiten der Beziehungsherstellung abzudecken und Entwicklern zu helfen, das zu verwenden, was für sie am besten ist.

Frage

Ich weiß, dass die Leute eine der oben genannten Antworten basierend auf der Vermutung auswählen können. Aber ich frage mich, ob eine der oben genannten Varianten besser ist als die andere, mit einem guten Grund dafür. Denken Sie auch nicht an die obige Frage. Ich möchte hier einige Best Practices zusammenfassen, die in den meisten Fällen angewendet werden können. Wenn Sie zustimmen, sind die meisten Anwendungsfälle bei der Erstellung einiger Entitäten ähnlich. Lassen Sie uns hier auch technologieunabhängig sein, z. Ich möchte nicht darüber nachdenken, ob ORM verwendet wird oder nicht. Ich will nur einen guten, ausdrucksstarken Modus.

Irgendeine Weisheit dazu?

BEARBEITEN

Bitte ignorieren Sie andere Eigenschaften von Questionund Answer, sie sind für die Frage nicht relevant. Ich habe den obigen Text bearbeitet und die meisten Konstruktoren geändert (wo erforderlich): Jetzt akzeptieren sie alle erforderlichen Eigenschaftswerte. Dies kann nur eine Fragezeichenfolge oder eine Zuordnung von Zeichenfolgen in verschiedenen Sprachen, Status usw. sein. Unabhängig davon, welche Eigenschaften übergeben werden, stehen sie nicht im Mittelpunkt.) Nehmen wir also an, wir übersteigen die erforderlichen Parameter, sofern nicht anders angegeben. Danke!

Rechtsanwalt
quelle

Antworten:

6

Aktualisiert. Klarstellungen berücksichtigt.

Es sieht so aus, als wäre dies eine Multiple-Choice-Domain, für die normalerweise die folgenden Anforderungen gelten

  1. Eine Frage muss mindestens zwei Auswahlmöglichkeiten haben, damit Sie eine auswählen können
  2. Es muss mindestens eine richtige Wahl geben
  3. Es sollte keine Wahl ohne Frage geben

basierend auf dem oben genannten

[A] kann die Invariante ab Punkt 1 nicht sicherstellen. Möglicherweise erhalten Sie eine Frage ohne Auswahl

[B] hat den gleichen Nachteil wie [A]

[C] hat den gleichen Nachteil wie [A] und [B]

[D] ist ein gültiger Ansatz, aber es ist besser, die Auswahl als Liste zu übergeben, als sie einzeln zu übergeben

[E] hat den gleichen Nachteil wie [A] , [B] und [C]

Daher würde ich mich für [D] entscheiden, da dadurch sichergestellt werden kann, dass die Domänenregeln aus den Punkten 1, 2 und 3 eingehalten werden. Selbst wenn Sie sagen, dass es sehr unwahrscheinlich ist, dass eine Frage über einen längeren Zeitraum ohne Auswahl bleibt, ist es immer eine gute Idee, die Domänenanforderungen über den Code zu vermitteln.

Ich würde das auch umbenennen Answer, Choiceda es für mich in diesem Bereich sinnvoller ist.

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

Eine Notiz. Wenn Sie die QuestionEntität zu einem Aggregatstamm und das ChoiceWertobjekt zu einem Teil desselben Aggregats machen, besteht keine Möglichkeit, dass Sie ein Objekt speichern können, Choiceohne es einem zuzuweisen Question(obwohl Sie keinen direkten Verweis auf das Questionals Argument an das übergeben ChoiceKonstruktor), da die Repositorys nur mit Roots arbeiten und Sie nach dem Erstellen Ihres Repositorys Questionalle Ihre Auswahlmöglichkeiten im Konstruktor zugewiesen haben.

Hoffe das hilft.

AKTUALISIEREN

Wenn es Sie wirklich stört, wie die Auswahlmöglichkeiten vor ihrer Frage erstellt werden, gibt es einige Tricks, die Sie möglicherweise nützlich finden

1) Ordnen Sie den Code so an, dass er nach der Frage oder zumindest gleichzeitig erstellt wird

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

2) Verstecken Sie Konstruktoren und verwenden Sie eine statische Factory-Methode

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) Verwenden Sie das Builder-Muster

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

Alles hängt jedoch von Ihrer Domain ab. In den meisten Fällen ist die Reihenfolge der Objekterstellung aus Sicht der Problemdomäne nicht wichtig. Was wichtiger ist, ist, dass sobald Sie eine Instanz Ihrer Klasse erhalten, diese logisch vollständig und einsatzbereit ist.


Veraltet. Alles unten ist für die Frage nach Klarstellungen irrelevant.

Zunächst sollte laut DDD-Domänenmodell in der realen Welt Sinn machen. Daher nur wenige Punkte

  1. Eine Frage hat möglicherweise keine Antworten
  2. Es sollte keine Antwort ohne Frage geben
  3. Eine Antwort sollte genau einer Frage entsprechen
  4. Eine "leere" Antwort beantwortet keine Frage

basierend auf dem oben genannten

[A] kann Punkt 4 widersprechen, da es leicht ist, den Text zu missbrauchen und zu vergessen, ihn festzulegen.

[B] ist ein gültiger Ansatz, erfordert jedoch optionale Parameter

[C] kann Punkt 4 widersprechen, da es eine Antwort ohne Text zulässt

[D] widerspricht Punkt 1 und kann den Punkten 2 und 3 widersprechen

[E] kann den Punkten 2, 3 und 4 widersprechen

Zweitens können wir OOP-Funktionen verwenden, um die Domänenlogik durchzusetzen. Wir können nämlich Konstruktoren für die erforderlichen Parameter und Setter für die optionalen verwenden.

Drittens würde ich die allgegenwärtige Sprache verwenden, die für die Domäne natürlicher sein soll.

Und schließlich können wir alles mithilfe von DDD-Mustern wie aggregierten Wurzeln, Entitäten und Wertobjekten entwerfen. Wir können die Frage zu einer Wurzel ihres Aggregats und die Antwort zu einem Teil davon machen. Dies ist eine logische Entscheidung, da eine Antwort außerhalb des Kontextes einer Frage keine Bedeutung hat.

Alles oben Genannte läuft also auf das folgende Design hinaus

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 Bei der Beantwortung Ihrer Frage habe ich nur wenige Annahmen zu Ihrer Domain getroffen, die möglicherweise nicht korrekt sind. Sie können die oben genannten Angaben also an Ihre Besonderheiten anpassen.

zafarkhaja
quelle
1
Zusammenfassend: Dies ist eine Mischung aus B und C. Bitte beachten Sie meine Klarstellung der Anforderungen. Ihr Punkt 1. kann nur für einen 'kurzen' Zeitraum existieren, während Sie eine Frage erstellen. aber nicht in der Datenbank. In diesem Sinne sollte 4. niemals passieren. Ich hoffe jetzt sind die Anforderungen klar;)
lawpert
Übrigens, mit der Klarstellung, scheint es mir, dass addAnsweroder assignAnswerwäre eine bessere Sprache als nur answer, ich hoffe, Sie stimmen dem zu. Wie auch immer, meine Frage ist - würden Sie sich immer noch für das B entscheiden und z. B. die Kopie der meisten Argumente in der Antwortmethode haben? Wäre das nicht eine Vervielfältigung?
Lawpert
Entschuldigen Sie die unklaren Anforderungen. Würden Sie die Antwort so gerne aktualisieren?
Lawpert
1
Es stellte sich heraus, dass meine Annahmen falsch waren. Ich habe Ihre QS-Domain als Beispiel für Stackexchange-Websites behandelt, aber es sieht eher nach einem Multiple-Choice-Test aus. Klar, ich werde meine Antwort aktualisieren.
Zafarkhaja
1
@lawpert Answerist ein Wertobjekt, das mit einer aggregierten Wurzel seines Aggregats gespeichert wird. Sie speichern Wertobjekte weder direkt noch speichern Sie Entitäten, wenn sie keine Wurzeln ihrer Aggregate sind.
Zafarkhaja
1

Wenn die Anforderungen so einfach sind, dass mehrere mögliche Lösungen existieren, sollte das KISS-Prinzip befolgt werden. In Ihrem Fall wäre das Option E.

Es gibt auch Fälle, in denen Code erstellt wird, der etwas ausdrückt, was nicht der Fall sein sollte. Wenn Sie beispielsweise die Erstellung von Antworten auf Frage (A und B) verknüpfen oder einen Antwortverweis auf Frage (C und D) geben, fügen Sie ein Verhalten hinzu, das für die Domäne nicht erforderlich und möglicherweise verwirrend ist. In Ihrem Fall würde die Frage höchstwahrscheinlich mit Antwort aggregiert, und Antwort wäre ein Werttyp.

Euphorisch
quelle
1
Warum ist [C] ein unnötiges Verhalten? Aus meiner Sicht teilt [C] mit, dass die Antwort ohne Frage nicht leben kann, und genau das ist es. Stellen Sie sich außerdem vor, dass für die Antwort weitere Flags erforderlich sind (z. B. Antworttyp, Kategorie usw.), die obligatorisch sind. Wenn wir KISS gehen, verlieren wir das Wissen darüber, was obligatorisch ist, und der Entwickler muss im Voraus wissen, was er zur Antwort hinzufügen / einstellen muss, um es richtig zu machen. Ich glaube, hier ging es nicht darum, dieses sehr einfache Beispiel zu modellieren, sondern die bessere Praxis zu finden, um mit OO eine allgegenwärtige Sprache zu schreiben.
igor
@igor E teilt bereits mit, dass die Antwort Teil der Frage ist, indem es obligatorisch ist, der Frage eine Antwort zuzuweisen, damit sie im Repository gespeichert wird. Wenn es eine Möglichkeit gäbe, nur die Antwort zu speichern, ohne die Frage zu laden, wäre C besser. Aber das ist aus dem, was Sie geschrieben haben, nicht ersichtlich.
Euphoric
@igor Wenn Sie die Erstellung der Antwort mit der Frage verknüpfen möchten, ist A besser. Wenn Sie sich für C entscheiden, wird es ausgeblendet, wenn die Antwort der Frage zugewiesen wird. Wenn Sie Ihren Text in A lesen, sollten Sie auch das "Modellverhalten" unterscheiden und wer dieses Verhalten initiiert. Die Frage kann für die Erstellung von Antworten verantwortlich sein, wenn die Antwort auf irgendeine Weise initialisiert werden muss. Es hat nichts mit "Benutzer, der Antworten erstellt" zu tun.
Euphoric
Nur zur Veranschaulichung, ich bin zwischen C & E hin und her gerissen :) Nun, dies: "... indem es obligatorisch ist, der Frage eine Antwort zuzuweisen, damit sie gespeichert wird, ist das Repository." Dies bedeutet, dass der 'obligatorische' Teil nur kommt , wenn wir zum Repository kommen. Daher ist die obligatorische Verbindung für Entwickler zum Zeitpunkt der Kompilierung nicht sichtbar, und die Geschäftsregeln sind im Repository nicht bekannt. Deshalb teste ich hier das [C]. Vielleicht kann dieser Vortrag mehr Informationen darüber geben, worum es meiner Meinung nach bei der C-Option geht.
igor
Dies: "... möchte die Erstellung der Antwort mit der Frage verknüpfen ...". Ich möchte die Kreation selbst nicht binden . Ich möchte nur die obligatorische Beziehung ausdrücken (persönlich möchte ich, wenn möglich, Modellobjekte selbst erstellen können). Meiner Ansicht nach geht es also nicht darum, etwas zu erschaffen, deshalb habe ich A und B bald fallen lassen. Ich sehe nicht, dass die Frage für die Erstellung der Antwort verantwortlich ist.
igor
1

Ich würde entweder [C] oder [E] gehen.

Erstens, warum nicht A und B? Ich möchte nicht, dass meine Frage für die Schaffung eines verwandten Werts verantwortlich ist. Stellen Sie sich vor, Frage hat viele andere Wertobjekte - würden Sie createfür jedes eine Methode setzen ? Oder wenn es einige komplexe Aggregate gibt, der gleiche Fall.

Warum nicht [D]? Weil es dem entgegengesetzt ist, was wir in der Natur haben. Wir erstellen zuerst eine Frage. Sie können sich eine Webseite vorstellen, auf der Sie all dies erstellen - der Benutzer würde zuerst eine Frage erstellen, oder? Daher nicht D.

[E] ist KISS, wie @Euphoric sagte. Aber ich fange in letzter Zeit auch an, [C] zu mögen. Das ist nicht so verwirrend, wie es scheint. Stellen Sie sich außerdem vor, wenn die Frage von mehr Dingen abhängt, muss der Entwickler wissen, was er in die Frage einfügen muss, damit sie ordnungsgemäß initialisiert wird. Obwohl Sie Recht haben, gibt es keine "visuelle" Sprache, die erklärt, dass die Antwort tatsächlich zur Frage hinzugefügt wird.

Zusätzliche Lektüre

Bei solchen Fragen frage ich mich, ob unsere Computersprachen für die Modellierung zu allgemein sind. (Ich verstehe, dass sie generisch sein müssen, um alle Programmieranforderungen zu erfüllen). Kürzlich habe ich versucht, einen besseren Weg zu finden, um die Geschäftssprache über fließende Schnittstellen auszudrücken. So etwas (in Sudo-Sprache):

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

dh versuchen, von großen * Services- und * Repository-Klassen zu kleineren Teilen der Geschäftslogik überzugehen. Nur eine Idee.

igor
quelle
Sie sprechen im Addon über die domänenspezifischen Sprachen?
Lawpert
Nun, als Sie erwähnt haben, sieht es so aus :) Kaufen Ich habe keine nennenswerte Erfahrung damit.
igor
2
Ich denke, es gibt inzwischen einen Konsens darüber, dass IO eine orthogonale Verantwortung ist und daher nicht von Entitäten (storeToRepo) gehandhabt werden sollte
Esben Skov Pedersen
Ich stimme @Esben Skov Pedersen zu, dass das Unternehmen selbst kein Repo im Inneren aufrufen sollte (das haben Sie gesagt, oder?). Aber als AFAIU haben wir hier eine Art Builder-Muster, das Befehle aufruft. Daher wird IO in der Entität hier nicht ausgeführt. Zumindest habe
ich das so
@lawpert das ist richtig. Ich sehe nicht, wie es funktionieren soll, wäre aber interessant.
Esben Skov Pedersen
1

Ich glaube, Sie haben hier einen Punkt verpasst. Ihre aggregierte Wurzel sollte Ihre Testeinheit sein.

Und wenn es wirklich so ist, ist eine TestFactory meiner Meinung nach am besten geeignet, um Ihr Problem zu beantworten.

Sie würden das Frage-und-Antwort-Gebäude an die Factory delegieren und könnten daher grundsätzlich jede Lösung verwenden, an die Sie gedacht haben, ohne Ihr Modell zu beschädigen, da Sie sich vor dem Client verstecken, wie Sie Ihre Unterentitäten instanziieren.

Dies gilt, solange die TestFactory die einzige Schnittstelle ist, über die Sie Ihren Test instanziieren.

Alexandre BODIN
quelle