Servicios de inyección DDD en llamadas a métodos de entidad

11

Formato corto de pregunta

¿Está dentro de las mejores prácticas de DDD y OOP inyectar servicios en llamadas de método de entidad?

Ejemplo de formato largo

Supongamos que tenemos el clásico caso Order-LineItems en DDD, donde tenemos una Entidad de dominio llamada Order, que también actúa como Aggregate Root, y esa Entidad está compuesta no solo por sus Value Value, sino también una colección de Line Item Entidades

Supongamos que queremos una sintaxis fluida en nuestra aplicación, de modo que podamos hacer algo como esto (notando la sintaxis en la línea 2, donde llamamos al getLineItemsmétodo):

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems($orderService) as $lineItem) {
  ...
}

No queremos inyectar ningún tipo de LineItemRepository en OrderEntity, ya que es una violación de varios principios en los que puedo pensar. Pero, la fluidez de la sintaxis es algo que realmente queremos, porque es fácil de leer y mantener, además de probar.

Considere el siguiente código, anotando el método getLineItemsen OrderEntity:

interface IOrderService {
    public function getOrderByID($orderID) : OrderEntity;
    public function getLineItems(OrderEntity $orderEntity) : LineItemCollection;
}

class OrderService implements IOrderService {
    private $orderRepository;
    private $lineItemRepository;

    public function __construct(IOrderRepository $orderRepository, ILineItemRepository $lineItemRepository) {
        $this->orderRepository = $orderRepository;
        $this->lineItemRepository = $lineItemRepository;
    }

    public function getOrderByID($orderID) : OrderEntity {
        return $this->orderRepository->getByID($orderID);
    }

    public function getLineItems(OrderEntity $orderEntity) : LineItemCollection {
        return $this->lineItemRepository->getLineItemsByOrderID($orderEntity->ID());
    }
}

class OrderEntity {
    private $ID;
    private $lineItems;

    public function getLineItems(IOrderServiceInternal $orderService) {
        if(!is_null($this->lineItems)) {
            $this->lineItems = $orderService->getLineItems($this);
        }
        return $this->lineItems;
    }
}

¿Es esa la forma aceptada de implementar una sintaxis fluida en las entidades sin violar los principios básicos de DDD y OOP? A mí me parece bien, ya que solo estamos exponiendo la capa de servicio, no la capa de infraestructura (que está anidada dentro del servicio)

e_i_pi
fuente

Respuestas:

9

Está totalmente bien pasar un servicio de dominio en una llamada de entidad. Digamos que necesitamos calcular una suma de factura con algún algoritmo complicado que puede depender, por ejemplo, de un tipo de cliente. Así es como podría verse:

class Invoice
{
    private $currency;
    private $customerId;

    public function __construct()
    {
    }

    public function sum(InvoiceCalculator $calculator)
    {
        $sum =
            new SumRecord(
                $calculator->calculate($this)
            )
        ;

        if ($sum->isZero()) {
            $this->events->add(new ZeroSumCalculated());
        }

        return $sum;
    }
}

Sin embargo, otro enfoque es separar una lógica de negocios que se encuentra en el servicio de dominio a través de eventos de dominio . Tenga en cuenta que este enfoque implica solo servicios de aplicación diferentes, pero el mismo alcance de transacción de la base de datos.

El tercer enfoque es el que estoy a favor: si me encuentro usando un servicio de dominio, eso probablemente significa que me perdí algún concepto de dominio, ya que modelo mis conceptos principalmente con sustantivos , no verbos. Entonces, idealmente, no necesito un servicio de dominio en absoluto y una buena parte de toda mi lógica de negocios reside en decoradores .

Vadim Samokhin
fuente
6

Me sorprende leer algunas de las respuestas aquí.

Es perfectamente válido pasar servicios de dominio a métodos de entidad en DDD para delegar algunos cálculos comerciales. Como ejemplo, imagine que su raíz agregada (una entidad) necesita acceder a un recurso externo a través de http para hacer algo de lógica comercial y generar un evento. Si no inyecta el servicio a través del método comercial de la entidad, ¿de qué otra manera lo haría? ¿Instanciaría un cliente http dentro de su entidad? Eso suena como una idea terrible.

Lo que es incorrecto es inyectar servicios en agregados a través de su constructor. Pero a través de un método comercial está bien y es perfectamente normal.

diegosasw
fuente
1
¿Por qué el caso que ha dado no sería responsabilidad de un Servicio de dominio?
e_i_pi
1
Es un servicio de dominio, pero se inyecta en el método comercial. La capa de aplicación es sólo un Orchestrator,
diegosasw
No tengo experiencia en DDD pero no se debe llamar al Servicio de dominio desde el Servicio de aplicación y, después de la validación del Servicio de dominio, ¿continuaré llamando a los métodos de Entidad a través de ese Servicio de aplicación? Estoy enfrentando el mismo problema en mi proyecto, porque el Servicio de dominio ejecuta una llamada a la base de datos a través del repositorio ... No sé si esto está bien.
Muflix
El servicio de dominio debe orquestar, si lo llama desde la aplicación más adelante, significa que de alguna manera procesa la respuesta y luego hace algo con ella. Tal vez eso suena como lógica de negocios. Si es así, pertenece a la capa Dominio y la aplicación luego simplemente resuelve la dependencia y la inyecta en el agregado. El servicio de dominio podría haber inyectado un repositorio cuya base de datos de implementación debería pertenecer a la capa de infraestructura (solo la implementación, no la interfaz / contrato). Si describe su idioma ubicuo, pertenece al dominio.
diegosasw
5

¿Está dentro de las mejores prácticas de DDD y OOP inyectar servicios en llamadas de método de entidad?

No, no debe inyectar nada dentro de su capa de dominio (esto incluye entidades, objetos de valor, fábricas y servicios de dominio). Esta capa debe ser independiente de cualquier marco, bibliotecas o tecnología de terceros y no debe realizar ninguna llamada de E / S.

$order->getLineItems($orderService)

Esto está mal, ya que el Agregado no debería necesitar nada más que sí mismo para devolver los artículos del pedido. La totalidad de agregado debe estar ya cargado antes de su llamada al método. Si crees que esto debería estar cargado de manera diferida, entonces hay dos posibilidades:

  1. Los límites de tus agregados son incorrectos, son demasiado grandes.

  2. En este caso de uso, usted usa el Agregado solo para leer. La mejor solución es dividir el modelo de escritura del modelo de lectura (es decir, usar CQRS ). En esta arquitectura más limpia no se le permite consultar el Agregado sino un modelo de lectura.

Constantin Galbenu
fuente
Si necesito una llamada a la base de datos para la validación, ¿tengo que llamarlo en el servicio de aplicación y pasar un resultado al servicio de dominio o directamente a la raíz agregada en lugar de inyectar el repositorio en el servicio de dominio?
Muflix
1
@Muflix sí, es cierto
Constantin Galbenu
3

La idea clave en los patrones tácticos DDD: la aplicación accede a todos los datos en la aplicación actuando en una raíz agregada. Esto implica que las únicas entidades a las que se puede acceder fuera del modelo de dominio son las raíces agregadas.

La raíz agregada Order nunca generará una referencia a su colección de elementos de línea que le permita modificar la colección, ni generará una colección de referencias a ninguna línea de pedido que le permita modificarla. Si desea cambiar el agregado de la Orden, se aplica el principio de Hollywood: "Dígale, no pregunte".

Devolver valores desde dentro del agregado está bien, porque los valores son inherentemente inmutables; No puede cambiar mis datos cambiando su copia.

Usar un servicio de dominio como argumento para ayudar al agregado a proporcionar los valores correctos es una cosa perfectamente razonable.

Normalmente no usaría un servicio de dominio para proporcionar acceso a los datos que están dentro del agregado, porque el agregado ya debería tener acceso a él.

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems($orderService) as $lineItem) {
  ...
}

De modo que esa ortografía es extraña si estamos intentando acceder a la colección de valores de líneas de pedido de este pedido. La ortografía más natural sería

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems() as $lineItem) {
  ...
}

Por supuesto, esto supone que las líneas de pedido ya se han cargado.

El patrón habitual es que la carga del agregado incluirá todo el estado requerido para el caso de uso particular. En otras palabras, puede tener varias formas diferentes de cargar el mismo agregado; Sus métodos de repositorio son adecuados para su propósito .

Este enfoque no es algo que encontrará en el Evans original, donde supuso que un agregado tendría un único modelo de datos asociado. Cae más naturalmente fuera de CQRS.

VoiceOfUnreason
fuente
Gracias por esto. Ahora he leído aproximadamente la mitad del "libro rojo", y tuve mi primer contacto con la aplicación adecuada del Principio de Hollywood en la capa de infraestructura. Al releer todas estas respuestas, todas tienen buenos puntos, pero creo que la suya tiene algunos puntos muy importantes con respecto al alcance lineItems()y la precarga en la primera recuperación de la Raíz Agregada.
e_i_pi
3

En términos generales, los objetos de valor que pertenecen al agregado no tienen repositorio por sí mismos. Es responsabilidad de la raíz agregada poblarlos. En su caso, es responsabilidad de su OrderRepository llenar los objetos de valores Order entidad y OrderLine.

En cuanto a la implementación de infraestructura del OrderRepository, en el caso de ORM, es una relación de uno a muchos, y puede elegir cargar con impaciencia o con pereza la OrderLine.

No estoy seguro de qué significan exactamente sus servicios. Está bastante cerca del "Servicio de aplicaciones". Si este es el caso, generalmente no es una buena idea inyectar los servicios a Aggregate root / Entity / Value Object. Application Service debe ser el cliente de Aggregate root / Entity / Value Object and Domain Service. Otra cosa sobre sus servicios es que exponer objetos de valor en Application Service tampoco es una buena idea. Se debe acceder por la raíz agregada.

ivenxu
fuente
2

La respuesta es: definitivamente NO, evite pasar servicios en métodos de entidad.

La solución es simple: simplemente deje que el repositorio de pedidos devuelva el pedido con todos sus artículos de línea. En su caso, el agregado es Order + LineItems, por lo que si el repositorio no devuelve un agregado completo, entonces no está haciendo su trabajo.

El principio más amplio es: mantener los bits funcionales (por ejemplo, lógica de dominio) separados de los bits no funcionales (por ejemplo, persistencia).

Una cosa más: si puedes, trata de evitar hacer esto:

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems() as $lineItem) {
  ...
}

Haz esto en su lugar

$order = $orderService->getOrderByID($orderID);
$order->doSomethingSignificant();

En el diseño orientado a objetos, tratamos de evitar pescar en los datos de un objeto. Preferimos pedirle al objeto que haga lo que queramos.

xpmatteo
fuente