Rapporti modello con DDD (o con senso)?


9

Ecco un requisito semplificato:

L'utente crea un Questioncon più Answers. Questiondeve averne almeno uno Answer.

Chiarimento: pensare Questione Answercome in un test : c'è una domanda, ma diverse risposte, dove poche possono essere corrette. L'utente è l'attore che sta preparando questo test, quindi crea domande e risposte.

Sto cercando di modellare questo semplice esempio in modo che 1) corrisponda al modello di vita reale 2) per essere espressivo con il codice, in modo da ridurre al minimo potenziali abusi ed errori e dare suggerimenti agli sviluppatori su come utilizzare il modello.

La domanda è un'entità , mentre la risposta è un oggetto valore . La domanda contiene risposte. Finora ho queste possibili soluzioni.

[A] Fabbrica dentroQuestion

Invece di creare Answermanualmente, possiamo chiamare:

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

Ciò creerà una risposta e la aggiungerà alla domanda. Quindi possiamo manipolare la risposta impostandone le proprietà. In questo modo, solo le domande possono creare una risposta. Inoltre, impediamo di avere una risposta senza una domanda. Tuttavia, non abbiamo il controllo sulla creazione di risposte, in quanto è codificato nel file Question.

C'è anche un problema con la 'lingua' del codice sopra. L'utente è colui che crea risposte, non la domanda. Personalmente, non mi piace che creiamo un oggetto valore e, a seconda dello sviluppatore, lo riempia di valori: come può essere sicuro di ciò che è necessario aggiungere?

[B] Factory Inside Question, prendi # 2

Alcuni dicono che dovremmo avere questo tipo di metodo in Question:

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

Simile alla soluzione precedente, questo metodo prende i dati obbligatori per la risposta e ne crea uno che verrà aggiunto anche alla domanda.

Il problema qui è che dupliciamo il costruttore del Answersenza una buona ragione. Inoltre, la domanda crea davvero una risposta?

[C] Dipendenze del costruttore

Siamo liberi di creare entrambi gli oggetti da soli. Esprimiamo anche la dipendenza proprio nel costruttore:

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

Ciò fornisce suggerimenti allo sviluppatore, poiché la risposta non può essere creata senza una domanda. Tuttavia, non vediamo la "lingua" che dice che la risposta è "aggiunta" alla domanda. D'altra parte, dobbiamo davvero vederlo?

[D] Dipendenza del costruttore, prendere # 2

Possiamo fare il contrario:

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

Questa è la situazione opposta di cui sopra. Qui le risposte possono esistere senza una domanda (che non ha senso), ma la domanda non può esistere senza una risposta (che ha un senso). Inoltre, il 'linguaggio' qui è più chiaro su tale questione si hanno le risposte.

[E] Modo comune

Questo è ciò che chiamo il modo comune, la prima cosa che di solito ppl:

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

che è la versione "libera" delle due risposte precedenti, poiché sia ​​la risposta che la domanda possono esistere l'una senza l'altra. Non v'è alcun accenno speciale che ha per legare insieme.

[F] Combinato

O dovrei combinare C, D, E - per coprire tutti i modi in cui è possibile stabilire una relazione, in modo da aiutare gli sviluppatori a utilizzare ciò che è meglio per loro.

Domanda

So che le persone possono scegliere una delle risposte sopra basate sul "sospetto". Ma mi chiedo se una qualsiasi delle varianti sopra è migliore dell'altra con una buona ragione per questo. Inoltre, per favore non pensare all'interno della domanda sopra, vorrei spremere qui alcune migliori pratiche che potrebbero essere applicate nella maggior parte dei casi - e se sei d'accordo, la maggior parte dei casi d'uso della creazione alcune entità sono simili. Inoltre, lascia che la tecnologia sia agnostica qui, ad es. Non voglio pensare se ORM verrà utilizzato o meno. Voglio solo una buona modalità espressiva.

Qualche saggezza su questo?

MODIFICARE

Si prega di ignorare altre proprietà di Questione Answer, non sono rilevanti per la domanda. Ho modificato il testo sopra e modificato la maggior parte dei costruttori (dove necessario): ora accettano tutti i valori di proprietà necessari. Potrebbe trattarsi solo di una stringa di domanda o di una mappa di stringhe in diverse lingue, stati, ecc. - qualunque sia la proprietà passata, non è un punto focale per questo;) Quindi supponiamo che stiamo superando i parametri necessari, a meno che non sia detto diversamente. Grazie!

Risposte:


6

Aggiornato. Chiarimenti presi in considerazione.

Sembra che questo sia un dominio a scelta multipla, che di solito ha i seguenti requisiti

  1. una domanda deve avere almeno due scelte in modo da poter scegliere tra
  2. ci deve essere almeno una scelta corretta
  3. non ci dovrebbe essere una scelta senza una domanda

Sulla base di quanto sopra

[A] non può garantire l'invariante dal punto 1, potresti finire con una domanda senza alcuna scelta

[B] ha lo stesso svantaggio di [A]

[C] ha lo stesso svantaggio di [A] e [B]

[D] è un approccio valido, ma è meglio passare le scelte come un elenco piuttosto che passarle singolarmente

[E] ha lo stesso svantaggio di [A] , [B] e [C]

Quindi, sceglierei [D] perché consente di garantire il rispetto delle regole di dominio dai punti 1, 2 e 3. Anche se dici che è molto improbabile che una domanda rimanga senza scelta per un lungo periodo di tempo, è sempre una buona idea trasmettere i requisiti di dominio attraverso il codice.

Vorrei anche rinominare il Answerper Choicecome ha più senso per me in questo settore.

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

Una nota. Se rendi l' Questionentità una radice aggregata e l' Choiceoggetto valore una parte dello stesso aggregato, non c'è alcuna possibilità che si possa archiviare una Choicesenza che sia assegnata a una Question(anche se non si passa un riferimento diretto a Questioncome argomento alla Choice"costruttore"), perché i repository funzionano solo con le radici e una volta creato il tuo Questionhai tutte le tue scelte assegnate nel costruttore.

Spero che sia di aiuto.

AGGIORNARE

Se ti dà davvero fastidio il modo in cui le scelte vengono create prima della loro domanda, ci sono alcuni trucchi che potresti trovare utili

1) Riorganizzare il codice in modo che sembri che siano stati creati dopo la domanda o almeno allo stesso tempo

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

2) Nascondere i costruttori e utilizzare un metodo statico di fabbrica

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) Utilizzare il modello del generatore

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

Tuttavia, tutto dipende dal tuo dominio. Il più delle volte l'ordine di creazione degli oggetti non è importante dal punto di vista del dominio problematico. Ciò che è più importante è che non appena ottieni un'istanza della tua classe, questa è logicamente completa e pronta per l'uso.


Obsoleto. Tutto sotto è irrilevante per la domanda dopo chiarimenti.

Prima di tutto, secondo il modello di dominio DDD dovrebbe avere senso nel mondo reale. Quindi, pochi punti

  1. una domanda potrebbe non avere risposte
  2. non ci dovrebbe essere una risposta senza una domanda
  3. una risposta dovrebbe corrispondere esattamente a una domanda
  4. una risposta "vuota" non risponde a una domanda

Sulla base di quanto sopra

[A] può contraddire il punto 4 perché è facile abusare e dimenticare di impostare il testo.

[B] è un approccio valido ma richiede parametri facoltativi

[C] può contraddire il punto 4 perché consente una risposta senza testo

[D] contraddice il punto 1 e può contraddire i punti 2 e 3

[E] può contraddire i punti 2, 3 e 4

In secondo luogo, possiamo utilizzare le funzionalità OOP per applicare la logica del dominio. Vale a dire che possiamo usare i costruttori per i parametri richiesti e i setter per quelli opzionali.

In terzo luogo, userei il linguaggio onnipresente che dovrebbe essere più naturale per il dominio.

E infine, possiamo progettare tutto usando modelli DDD come radici aggregate, entità e oggetti valore. Possiamo fare della Domanda una radice del suo aggregato e la Risposta una parte di essa. Questa è una decisione logica perché una risposta non ha significato al di fuori del contesto di una domanda.

Quindi, tutto quanto sopra si riduce al seguente disegno

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 Per rispondere alla tua domanda ho formulato alcune ipotesi sul tuo dominio che potrebbero non essere corrette, quindi sentiti libero di modificare quanto sopra con le tue specifiche.


1
Riassumendo: questo è un mix di B e C. Si prega di consultare il mio chiarimento dei requisiti. Il tuo punto 1. può esistere solo per un periodo di tempo "breve", mentre costruisci una domanda; ma non nel database. In tal senso, 4. non dovrebbe mai accadere. Spero ora che i requisiti siano chiari;)
avvocato

A proposito, con il chiarimento, a me sembra che addAnswero assignAnswersarebbe un linguaggio migliore di solo answer, spero che tu sia d'accordo su questo. Ad ogni modo, la mia domanda è: sceglieresti ancora la B e, ad esempio, avresti la copia della maggior parte degli argomenti nel metodo di risposta? Non sarebbe duplicazione?
avvocato

Ci scusiamo per i requisiti poco chiari, saresti così gentile da aggiornare la risposta?
avvocato il

1
Risulta che i miei presupposti erano errati. Ho trattato il tuo dominio di QA come un esempio di siti Web stackexchange ma sembra più un test a scelta multipla. Certo, aggiornerò la mia risposta.
zafarkhaja,

1
@lawpert Answerè un oggetto valore, verrà archiviato con una radice aggregata del suo aggregato. Non si memorizzano direttamente oggetti valore, né si salvano entità se non sono radici dei loro aggregati.
zafarkhaja,

1

Nel caso in cui i requisiti siano così semplici, che esistano più soluzioni possibili, è necessario seguire il principio KISS. Nel tuo caso, sarebbe l'opzione E.

C'è anche il caso di creare codice che esprima qualcosa, che non dovrebbe. Ad esempio, legando la creazione di Risposte alla domanda (A e B) o facendo riferimento alla risposta alla Domanda (C e D) si aggiungono alcuni comportamenti che non sono necessari al dominio e potrebbero creare confusione. Inoltre, nel tuo caso, la domanda sarebbe molto probabilmente aggregata con la risposta e la risposta sarebbe un tipo di valore.


1
Perché [C] è un comportamento non necessario ? A mio avviso, [C] comunica che la risposta non può vivere senza una domanda, ed è esattamente quello che è. Inoltre, immagina se la risposta richiede qualche altro flag (ad es. Tipo di risposta, categoria, ecc.) Che sono obbligatori. Andando KISS stiamo perdendo quella conoscenza di ciò che è obbligatorio, e lo sviluppatore deve sapere in primo piano ciò che deve aggiungere / impostare alla Risposta per renderlo giusto. Credo che qui la domanda non fosse quella di modellare questo esempio molto semplice, ma di trovare la migliore pratica per scrivere un linguaggio onnipresente usando OO.
vigore

@igor E comunica già che la risposta è parte della domanda rendendo obbligatorio assegnare la risposta alla domanda affinché sia ​​salvata nel repository. Se ci fosse un modo per salvare solo Rispondi senza caricare la domanda, allora C sarebbe meglio. Ma questo non è ovvio da quello che hai scritto.
Euforico,

@igor Inoltre, se vuoi legare la creazione di Answer with Question, allora A sarebbe meglio, perché se vai con C, allora si nasconde quando la risposta è assegnata alla domanda. Inoltre, leggendo il testo in A, dovresti differenziare il "comportamento modello" e chi inizia questo comportamento. La domanda potrebbe essere responsabile della creazione di risposte, quando deve in qualche modo inizializzare la risposta. Non ha nulla a che fare con "la creazione di risposte da parte dell'utente".
Euforico,

Solo per la cronaca, sono diviso tra C&E :) Ora, questo: "... rendendo obbligatorio assegnare la risposta alla domanda per salvarlo è un repository". Questo significa che la parte 'obbligatoria' arriva solo quando arriviamo al repository. Pertanto la connessione obbligatoria non è "visibile" allo sviluppatore al momento della compilazione e le regole aziendali perdono nel repository. Ecco perché sto testando la [C] qui. Forse questo discorso può dare maggiori informazioni su ciò che penso sia l'opzione C.
vigore

Questo: "... vuoi legare la creazione di Answer with Question ...". Non voglio legare la stessa creazione . Voglio solo esprimere la relazione obbligatoria (personalmente mi piace essere in grado di creare oggetti modello da solo, quando possibile). Quindi, dal mio punto di vista, non si tratta di creare, ecco perché presto abbandonerò A e B. Non vedo che la domanda sia responsabile della creazione della risposta.
vigore

1

Vorrei andare o [C] o [E].

Innanzitutto, perché non A e B? Non voglio che la mia domanda sia responsabile della creazione di alcun valore correlato. Immagina se la domanda ha molti altri oggetti valore - metteresti il createmetodo per ognuno? O se ci sono alcuni aggregati complessi, lo stesso caso.

Perché non [D]? Perché è l'opposto di ciò che abbiamo in natura. Per prima cosa creiamo una domanda. Puoi immaginare una pagina web in cui crei tutto questo: l'utente dovrebbe prima creare una domanda, giusto? Pertanto, non D.

[E] è KISS, come ha detto @Euphoric. Ma inizio anche a farmi piacere [C] di recente. Questo non è così confuso come sembra. Inoltre, immagina se la domanda dipende da più cose - quindi lo sviluppatore deve sapere cosa deve inserire nella domanda per averla correttamente inizializzata. Sebbene tu abbia ragione, non esiste un linguaggio "visivo" che spieghi che la risposta è effettivamente aggiunta alla domanda.

Lettura aggiuntiva

Domande come questa mi chiedono se i nostri linguaggi informatici siano troppo generici per la modellazione. (Capisco che devono essere generici per rispondere a tutti i requisiti di programmazione). Recentemente sto cercando di trovare un modo migliore per esprimere il linguaggio aziendale usando interfacce fluide. Qualcosa del genere (in lingua sudo):

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

vale a dire cercare di allontanarsi da qualsiasi grande * servizio e * classe di deposito in blocchi più piccoli di logica aziendale. Solo un'idea


Stai parlando nel componente aggiuntivo delle lingue specifiche del dominio?
avvocato il

Ora, quando hai menzionato, sembra così :) Acquista Non ho alcuna esperienza significativa con esso.
vigore

2
Penso che ci sia un consenso ormai sul fatto che l'IO è una resposibilità ortogonale e quindi non dovrebbe essere gestita da entità (storeToRepo)
Esben Skov Pedersen,

Sono d'accordo su @Esben Skov Pedersen che l'entità stessa non dovrebbe chiamare repo all'interno (questo è quello che hai detto, giusto?); ma come AFAIU qui abbiamo una sorta di modello di builder dietro che invoca comandi; quindi IO non viene eseguito nell'entità qui. Almeno così ho capito;)
avvocato il

@lawpert è corretto. Non riesco a vedere come dovrebbe funzionare ma sarebbe interessante.
Esben Skov Pedersen,

1

Credo che tu abbia perso un punto qui, la tua radice aggregata dovrebbe essere la tua entità di test.

E se è davvero il caso, credo che un TestFactory sarebbe più adatto a rispondere al tuo problema.

Delegheresti la costruzione di domande e risposte alla Fabbrica e quindi potresti sostanzialmente utilizzare qualsiasi soluzione che hai pensato senza corrompere il tuo modello perché ti nascondi al cliente nel modo in cui crei un'istanza delle tue entità secondarie.

Questo è, purché TestFactory sia l'unica interfaccia che usi per creare un'istanza del tuo Test.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.