Relazioni modello con DDD (o con senso)?

9

Ecco un requisito semplificato:

User creates a Question with multiple Answers. Question must have at least one Answer.

Clarification: think Question and Answer as in a test: there is one question, but several answers, where few may be correct. User is the actor who is preparing this test, hence he creates question and answers.

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

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

[A] Fabbrica all'interno di Question

Invece di creare Answer manualmente, possiamo chiamare:

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

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

C'è anche un problema con la 'lingua' del codice precedente. L'utente è colui che crea risposte, non la domanda. Personalmente, non mi piace creare oggetti valore e, a seconda dello sviluppatore, riempirlo di valori: come può essere sicuro di cosa è necessario aggiungere?

[B] Factory inside Question, take # 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à anche aggiunto alla domanda.

Il problema è che noi duplichiamo il costruttore di Answer senza una buona ragione. Inoltre, la domanda crea davvero una risposta?

[C] dipendenze del costruttore

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

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

Questo dà 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, abbiamo davvero bisogno di vederlo?

[D] Dipendenza del costruttore, prendi # 2

Possiamo fare il contrario:

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

Questa è la situazione opposta di sopra. Qui le risposte possono esistere senza una domanda (che non ha senso), ma la domanda non può esistere senza risposta (che ha senso). Inoltre, la "lingua" qui è più chiara su quella domanda avere le risposte.

[E] modo comune

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

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

che è la versione "libera" di sopra di due risposte, poiché sia la risposta che la domanda possono esistere l'una senza l'altra. Non c'è nessun suggerimento speciale per avere per unirli insieme.

[F] Combinato

O dovrei combinare C, D, E - per coprire tutti i modi in cui è possibile creare 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 in base alla "sensazione". Ma mi chiedo se una delle varianti precedenti sia migliore dell'altro con una buona ragione per questo. Inoltre, per favore non pensare nella domanda sopra, vorrei spremere qui alcune best practice che potrebbero essere applicate nella maggior parte dei casi - e se sei d'accordo, la maggior parte dei casi di creazione di uso di alcune entità sono simili. Inoltre, lascia qui la tecnologia agnostica, ad es. Non voglio pensare se ORM sarà usato o meno. Voglio solo una buona modalità espressiva.

Qualche saggezza su questo?

Modifica

Ignora le altre proprietà di Question e Answer , non sono rilevanti per la domanda. Ho modificato il testo sopra e ho modificato la maggior parte dei costruttori (laddove necessario): ora accettano tutti i valori di proprietà necessari necessari. Potrebbe trattarsi solo di una stringa di domande, o di una mappa di stringhe in lingue diverse, di stati ecc. - qualsiasi proprietà venga passata, non sono un punto focale per questo; Quindi, supponiamo solo che stiamo superando i parametri necessari, a meno che non siano diversi. Grazie!

    
posta lawpert 21.10.2014 - 20:35
fonte

4 risposte

6

Aggiornamento. 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 che tu possa scegliere tra
  2. deve esserci almeno una scelta corretta
  3. non dovrebbe esserci una scelta senza una domanda

In base a quanto sopra

[A] non può garantire l'invariante dal punto 1, potresti finire con una domanda senza 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 elenco piuttosto che passarle singolarmente

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

Quindi, opterei per [D] perché consente di garantire che le regole di dominio dei punti 1, 2 e 3 siano seguite. Anche se dici che è molto improbabile che una domanda rimanga senza scelta per un lungo periodo di tempo, è sempre una buona idea comunicare i requisiti del dominio attraverso il codice.

Vorrei anche rinominare Answer in Choice come ha più senso per me in questo dominio.

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'entità Question una radice aggregata e l'oggetto valore Choice una parte dello stesso aggregato, non ci sono possibilità che uno possa memorizzare Choice senza che sia assegnato a Question (anche se non lo fai t passare un riferimento diretto a Question come argomento al costruttore di Choice ), perché i repository funzionano solo con le radici e una volta creato il Question hai tutte le scelte assegnate ad esso nel costruttore.

Spero che questo aiuti.

UPDATE

Se ti infastidisce davvero come vengono create le scelte prima della loro domanda, ci sono alcuni trucchi che potresti trovare utili

1) Riorganizza il codice in modo che sembri creato 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 factory statico

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) Usa lo schema costruttore

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. La maggior parte delle volte l'ordine di creazione di oggetti non è importante dal punto di vista del dominio del problema. Ciò che è più importante è che non appena ottieni un'istanza della tua classe è logicamente completa e pronta per l'uso.

obsoleto. Tutto ciò che segue è irrilevante per la domanda dopo i 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 dovrebbe esserci una risposta senza una domanda
  3. una risposta dovrebbe corrispondere esattamente a una domanda
  4. una risposta "vuota" non risponde a una domanda

In base a quanto sopra

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

[B] è un approccio valido ma richiede parametri facoltativi

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

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

[E] potrebbe contraddire i punti 2, 3 e 4

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

In terzo luogo, utilizzerei il linguaggio ubiquitario che dovrebbe essere più naturale per il dominio.

E infine, possiamo progettarlo 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 esso. 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");

P.S. Risposta alla tua domanda Ho fatto alcune supposizioni sul tuo dominio che potrebbero non essere corrette, quindi sentiti libero di adattare quanto sopra alle tue specifiche.

    
risposta data 22.10.2014 - 11:35
fonte
1

Nel caso in cui i requisiti siano così semplici, che esistano più soluzioni possibili, allora il principio KISS dovrebbe essere seguito. Nel tuo caso, sarebbe l'opzione E.

C'è anche il caso di creare codice che esprima qualcosa, che non dovrebbe. Ad esempio legare la creazione di Risposte alla domanda (A e B) o dare una risposta a Domanda (C e D) aggiunge un comportamento non necessario al dominio e potrebbe essere fonte di confusione. Inoltre, nel tuo caso, la domanda sarebbe molto probabilmente aggregata con la risposta e la risposta sarebbe un tipo di valore.

    
risposta data 22.10.2014 - 00:09
fonte
1

Vorrei andare su [C] o [E].

In primo luogo, perché non A e B? Non voglio che la mia domanda sia responsabile della creazione di alcun valore correlato. Immagina se Question abbia molti altri oggetti valore - potresti mettere il metodo create per ognuno? O se ci sono alcuni complessi aggregati, lo stesso caso.

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

[E] è KISS, come ha detto @Euphoric. Ma comincio anche a piacermi [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 mettere all'interno della domanda per essere inizializzato correttamente. Sebbene tu abbia ragione, non esiste una lingua "visiva" che spieghi che la risposta è effettivamente aggiunta alla domanda.

Letture aggiuntive

Domande come questa mi chiedono se i nostri linguaggi informatici siano troppo generici per la modellazione. (Capisco che hanno per essere generico per rispondere a tutti i requisiti di programmazione). Recentemente sto cercando di trovare un modo migliore per esprimere la lingua del business utilizzando interfacce fluenti. Qualcosa di simile (in linguaggio sudo):

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

vale a dire. cercando di allontanarsi da qualsiasi grande * Servizi e * Classi di repository in blocchi di business logic più piccoli. Solo un'idea.

    
risposta data 22.10.2014 - 09:16
fonte
0

Credo che tu abbia perso un punto qui, la tua radice Aggregato dovrebbe essere la tua Entità Test.

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

Dovresti delegare la creazione di domande e risposte alla fabbrica e quindi potresti sostanzialmente utilizzare qualsiasi soluzione che pensavi senza corrompere il tuo modello perché ti stai nascondendo al client nel modo in cui instanzia le tue sub entità.

Questo è, purché TestFactory sia l'unica interfaccia che usi per istanziare il tuo Test.

    
risposta data 21.07.2016 - 18:40
fonte

Leggi altre domande sui tag