¿Relaciones modelo con DDD (o con sentido)?

9

Aquí hay un requisito simplificado:

El usuario crea un Questioncon múltiples Answers. Questiondebe tener al menos uno Answer.

Aclaración: piense Questiony Answercomo en una prueba : hay una pregunta, pero varias respuestas, donde pocas pueden ser correctas. El usuario es el actor que está preparando esta prueba, por lo tanto, crea preguntas y respuestas.

Estoy tratando de modelar este ejemplo simple para que 1) coincida con el modelo de la vida real 2) para ser expresivo con el código, para minimizar el posible mal uso y errores, y para dar pistas a los desarrolladores sobre cómo usar el modelo.

La pregunta es una entidad , mientras que la respuesta es un objeto de valor . La pregunta tiene respuestas. Hasta ahora, tengo estas posibles soluciones.

[A] Fábrica adentroQuestion

En lugar de crear Answermanualmente, podemos llamar a:

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

Eso creará una respuesta y la agregará a la pregunta. Entonces podemos manipular la respuesta configurando sus propiedades. De esta manera, solo las preguntas pueden crear una respuesta. Además, evitamos tener una respuesta sin una pregunta. Sin embargo, no tenemos control sobre la creación de respuestas, ya que eso está codificado en el Question.

También hay un problema con el 'idioma' del código anterior. El usuario es quien crea las respuestas, no la pregunta. Personalmente, no me gusta que creamos un objeto de valor y, dependiendo del desarrollador para llenarlo con valores, ¿cómo puede estar seguro de lo que se requiere agregar?

[B] Fábrica dentro de la pregunta, tome el # 2

Algunos dicen que deberíamos tener este tipo de método en Question:

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

Similar a la solución anterior, este método toma datos obligatorios para la respuesta y crea uno que también se agregará a la pregunta.

El problema aquí es que duplicamos el constructor de la Answersin razón alguna. Además, ¿la pregunta realmente crea una respuesta?

[C] Dependencias del constructor

Seamos libres de crear ambos objetos por nosotros mismos. Expresemos también el derecho de dependencia en el constructor:

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

Esto le da pistas al desarrollador, ya que la respuesta no se puede crear sin una pregunta. Sin embargo, no vemos el 'lenguaje' que dice que la respuesta se 'agrega' a la pregunta. Por otro lado, ¿realmente necesitamos verlo?

[D] Dependencia del constructor, toma # 2

Podemos hacer lo contrario:

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

Esta es la situación opuesta de arriba. Aquí las respuestas pueden existir sin una pregunta (que no tiene sentido), pero la pregunta no puede existir sin una respuesta (que tiene sentido). Además, el 'lenguaje' aquí es más claro en esa pregunta que tendrá las respuestas.

[E] Manera común

Esto es lo que llamo la forma común, lo primero que suelen hacer las personas:

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

que es la versión 'suelta' de las dos respuestas anteriores, ya que tanto la respuesta como la pregunta pueden existir sin la otra. No hay ninguna pista especial de que tengas que unirlos.

[F] Combinado

¿O debería combinar C, D, E? Para cubrir todas las formas en que se puede establecer una relación, para ayudar a los desarrolladores a usar lo que sea mejor para ellos.

Pregunta

Sé que las personas pueden elegir una de las respuestas anteriores en función de la 'corazonada'. Pero me pregunto si alguna de las variantes anteriores es mejor que la otra con una buena razón para eso. Además, no piense dentro de la pregunta anterior, me gustaría exponer aquí algunas de las mejores prácticas que podrían aplicarse en la mayoría de los casos, y si está de acuerdo, la mayoría de los casos de creación son algunas entidades similares. Además, seamos independientes de la tecnología aquí, por ejemplo. No quiero pensar si ORM se va a usar o no. Solo quiero un buen modo expresivo.

¿Alguna sabiduría sobre esto?

EDITAR

Ignore otras propiedades de Questiony Answer, no son relevantes para la pregunta. Edité el texto anterior y cambié la mayoría de los constructores (donde fue necesario): ahora aceptan cualquiera de los valores de propiedad necesarios. Eso puede ser solo una cadena de preguntas o un mapa de cadenas en diferentes idiomas, estados, etc., independientemente de las propiedades que se pasen, no son un foco para esto;) Así que supongamos que estamos por encima de pasar los parámetros necesarios, a menos que se diga diferente. Gracias!

Lawpert
fuente

Respuestas:

6

Actualizado. Aclaraciones tenidas en cuenta.

Parece que este es un dominio de opción múltiple, que generalmente tiene los siguientes requisitos

  1. una pregunta debe tener al menos dos opciones para poder elegir entre
  2. debe haber al menos una opción correcta
  3. no debería haber una elección sin una pregunta

Basado en lo anterior

[A] no puede garantizar la invariante desde el punto 1, puede terminar con una pregunta sin ninguna opción

[B] tiene la misma desventaja que [A]

[C] tiene la misma desventaja que [A] y [B]

[D] es un enfoque válido, pero es mejor pasar las opciones como una lista en lugar de pasarlas individualmente

[E] tiene la misma desventaja que [A] , [B] y [C]

Por lo tanto, elegiría [D] porque permite garantizar que se sigan las reglas de dominio de los puntos 1, 2 y 3. Incluso si dice que es muy poco probable que una pregunta permanezca sin ninguna opción durante un largo período de tiempo, siempre es una buena idea transmitir los requisitos de dominio a través del código.

También cambiaría el nombre de Answera Choiceya que tiene más sentido para mí en este 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. Si hace que la Questionentidad sea una raíz agregada y el Choiceobjeto de valor sea parte del mismo agregado, no hay posibilidades de que uno pueda almacenar una Choicesin que se asigne a una Question(aunque no pase una referencia directa a la Questioncomo argumento para la Choice's constructor), porque los repositorios funcionan solo con raíces y una vez que construyes tu Questiontienes todas tus opciones asignadas en el constructor.

Espero que esto ayude.

ACTUALIZAR

Si realmente le molesta cómo se crean las opciones antes de su pregunta, hay algunos trucos que pueden resultarle útiles.

1) Reorganice el código para que parezca que se crearon después de la pregunta o al menos al mismo tiempo

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

2) Ocultar constructores y usar un método de fábrica estático

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 el patrón de construcción

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

Sin embargo, todo depende de tu dominio. La mayoría de las veces el orden de creación de objetos no es importante desde la perspectiva del dominio del problema. Lo más importante es que tan pronto como obtenga una instancia de su clase, está lógicamente completa y lista para usar.


Anticuado. Todo lo que sigue es irrelevante para la pregunta después de las aclaraciones.

En primer lugar, según el modelo de dominio DDD debería tener sentido en el mundo real. Por lo tanto, pocos puntos

  1. una pregunta puede no tener respuestas
  2. no debería haber una respuesta sin una pregunta
  3. una respuesta debe corresponder exactamente a una pregunta
  4. una respuesta "vacía" no responde una pregunta

Basado en lo anterior

[A] puede contradecir el punto 4 porque es fácil de usar mal y olvidarse de configurar el texto.

[B] es un enfoque válido pero requiere parámetros que son opcionales

[C] puede contradecir el punto 4 porque permite una respuesta sin texto

[D] contradice el punto 1 y puede contradecir los puntos 2 y 3

[E] puede contradecir los puntos 2, 3 y 4

En segundo lugar, podemos hacer uso de las características de OOP para aplicar la lógica de dominio. Es decir, podemos usar constructores para los parámetros requeridos y establecedores para los opcionales.

En tercer lugar, usaría el lenguaje omnipresente que se supone que es más natural para el dominio.

Y finalmente, podemos diseñarlo todo usando patrones DDD como raíces agregadas, entidades y objetos de valor. Podemos hacer que la Pregunta sea la raíz de su agregado y la Respuesta una parte de ella. Esta es una decisión lógica porque una respuesta no tiene sentido fuera del contexto de una pregunta.

Entonces, todo lo anterior se reduce al siguiente diseño

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

PD Respondiendo a su pregunta, hice algunas suposiciones sobre su dominio que podrían no ser correctas, así que siéntase libre de ajustar lo anterior con sus detalles.

zafarkhaja
fuente
1
Para resumir: esta es una mezcla de B y C. Consulte mi aclaración de los requisitos. Su punto 1. puede existir solo por un período de tiempo 'corto', mientras se construye una pregunta; pero no en la base de datos. En ese sentido, 4. nunca debería suceder. Espero que ahora los requisitos estén claros;)
lawpert
Por cierto, con la aclaración, me parece que sería addAnswero assignAnswersería un lenguaje mejor que solo answer, espero que esté de acuerdo con esto. De todos modos, mi pregunta es: ¿seguiría con la B y, por ejemplo, tendría la copia de la mayoría de los argumentos en el método de respuesta? ¿No sería eso una duplicación?
lawpert
Perdón por los requisitos poco claros, ¿sería tan amable de actualizar la respuesta?
lawpert
1
Resulta que mis suposiciones eran incorrectas. Traté su dominio de QA como un ejemplo de sitios web de stackexchange, pero se parece más a una prueba de opción múltiple. Claro, actualizaré mi respuesta.
zafarkhaja
1
@lawpert Answeres un objeto de valor, se almacenará con una raíz agregada de su agregado. No almacena objetos de valor directamente, ni guarda entidades si no son raíces de sus agregados.
zafarkhaja
1

En caso de que los requisitos sean tan simples que existan múltiples soluciones posibles, entonces se debe seguir el principio KISS. En su caso, esa sería la opción E.

También existe el caso de crear código que exprese algo, que no debería. Por ejemplo, vincular la creación de respuestas a la pregunta (A y B) o dar una referencia de respuesta a la pregunta (C y D) agrega un comportamiento que no es necesario para el dominio y puede ser confuso. Además, en su caso, la pregunta probablemente se agregaría con la respuesta y la respuesta sería un tipo de valor.

Eufórico
fuente
1
¿Por qué [C] es un comportamiento innecesario ? A mi modo de ver, [C] comunica que la respuesta no puede vivir sin una pregunta, y eso es exactamente lo que es. Además, imagine si la respuesta requiere más indicadores (por ejemplo, tipo de respuesta, categoría, etc.) que son obligatorios. Al ir a KISS, estamos perdiendo ese conocimiento de lo que es obligatorio, y el desarrollador debe saber al frente lo que necesita agregar / configurar a la Respuesta para corregirlo. Creo que aquí la pregunta no era modelar este ejemplo tan simple, sino encontrar la mejor práctica para escribir lenguaje ubicuo usando OO.
igor
@igor E ya comunica que la Respuesta es parte de la Pregunta al hacer obligatorio asignar la Respuesta a la pregunta para que se guarde en su repositorio. Si hubiera una manera de guardar solo Answer sin cargar su pregunta, entonces C sería mejor. Pero eso no es obvio por lo que escribiste.
Eufórico
@igor Además, si desea vincular la creación de la Respuesta con la Pregunta, entonces A sería mejor, porque si va con C, entonces se oculta cuando la respuesta se asigna a la pregunta. Además, al leer su texto en A, debe diferenciar el "comportamiento modelo" y quién inicia este comportamiento. La pregunta podría ser responsable de crear respuestas, cuando necesita inicializar la respuesta de alguna manera. No tiene nada que ver con "usuario creando respuestas".
Eufórico el
Solo para que conste, estoy dividido entre C&E :) Ahora, esto: "... al hacer obligatorio asignar Respuesta a pregunta para que se guarde es el repositorio". Esto significa que la parte 'obligatoria' viene solo cuando llegamos al repositorio. Por lo tanto, la conexión obligatoria no es 'visible' para el desarrollador en el momento de la compilación, y las reglas de negocio se filtran en el repositorio. Por eso estoy probando la [C] aquí. Tal vez esta charla pueda dar más información sobre lo que creo que trata la opción C.
igor
Esto: "... desea vincular la creación de la respuesta con la pregunta ...". No quiero atar la creación misma. Solo quiero expresar la relación obligatoria (personalmente me gusta poder crear objetos modelo por mí mismo, cuando sea posible). Entonces, desde mi punto de vista, no se trata de crear, por eso abandono A y B pronto. No veo que la Pregunta sea responsable de crear la respuesta.
igor
1

Yo iría ya sea [C] o [E].

Primero, ¿por qué no A y B? No quiero que mi pregunta sea responsable de crear ningún valor relacionado. Imagínese si la Pregunta tiene muchos otros objetos de valor: ¿pondría el createmétodo para cada uno? O si hay algunos agregados complejos, el mismo caso.

¿Por qué no [D]? Porque es lo opuesto a lo que tenemos en la naturaleza. Primero creamos una pregunta. Puede imaginar una página web donde cree todo esto: el usuario primero crearía una pregunta, ¿verdad? Por lo tanto, no D.

[E] es KISS, como dijo @Euphoric. Pero también me empieza a gustar [C] recientemente. Esto no es tan confuso como parece. Además, imagine que si la Pregunta depende de más cosas, entonces el desarrollador debe saber lo que necesita poner dentro de la Pregunta para que se inicialice correctamente. Aunque tiene razón, no existe un lenguaje 'visual' que explique que la respuesta se agrega a la pregunta.

Lectura adicional

Preguntas como esta me hacen preguntarme si nuestros lenguajes de computadora son demasiado genéricos para modelar. (Entiendo que tienen que ser genéricos para responder a todos los requisitos de programación). Recientemente estoy tratando de encontrar una mejor manera de expresar el lenguaje de negocios utilizando interfaces fluidas. Algo como esto (en idioma sudo):

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

es decir, tratar de alejarse de las grandes clases de * Servicios y * Repositorios a fragmentos más pequeños de lógica empresarial. Solo una idea.

igor
fuente
¿Estás hablando en el complemento sobre los lenguajes específicos de dominio?
lawpert
Ahora, cuando lo mencionaste, se ve así :) Comprar No tengo ninguna experiencia significativa con él.
igor
2
Creo que ya existe un consenso de que IO es una responsabilidad ortogonal y, por lo tanto, no debería ser manejada por entidades (storeToRepo)
Esben Skov Pedersen
Estoy de acuerdo con @Esben Skov Pedersen que la entidad en sí no debería llamar al repositorio (eso es lo que dijiste, ¿verdad?); pero como AFAIU aquí tenemos algún tipo de patrón de generador detrás que invoca comandos; entonces IO no se hace en la entidad aquí. Al menos así es como lo he entendido;)
lawpert
@lawpert eso es correcto. No veo cómo se supone que funciona, pero sería interesante.
Esben Skov Pedersen
1

Creo que te perdiste un punto aquí, tu raíz agregada debería ser tu entidad de prueba.

Y si realmente es así, creo que un TestFactory sería el más adecuado para responder a su problema.

Delegaría el edificio de Preguntas y Respuestas a la Fábrica y, por lo tanto, básicamente podría usar cualquier solución que pensara sin corromper su modelo porque se está ocultando al cliente la forma en que instancia sus entidades secundarias.

Esto es, siempre que TestFactory sea la única interfaz que use para crear una instancia de su Prueba.

Alexandre BODIN
fuente