Serviços de injeção de DDD em chamadas de métodos de entidade

11

Formato curto da pergunta

Está dentro das melhores práticas de DDD e OOP injetar serviços em chamadas de método de entidade?

Exemplo de formato longo

Digamos que temos o caso clássico Order-LineItems no DDD, onde temos uma Entidade de Domínio chamada Order, que também atua como Raiz Agregada, e que a Entidade é composta não apenas pelos Objetos de Valor, mas também por uma coleção de Itens de Linha Entidades.

Suponha que desejemos sintaxe fluente em nosso aplicativo, para que possamos fazer algo assim (observando a sintaxe na linha 2, onde chamamos o getLineItemsmétodo):

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

Não queremos injetar nenhum tipo de LineItemRepository no OrderEntity, pois isso é uma violação de vários princípios em que posso pensar. Mas, a fluência da sintaxe é algo que realmente queremos, porque é fácil de ler e manter, além de testar.

Considere o seguinte código, observando o método getLineItemsem 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;
    }
}

Essa é a maneira aceita de implementar sintaxe fluente nas Entidades sem violar os princípios básicos de DDD e OOP? Para mim, parece bom, pois estamos apenas expondo a camada de serviço, não a camada de infraestrutura (que está aninhada no serviço)

e_i_pi
fonte

Respostas:

9

É totalmente bem para passar um serviço de domínio em uma chamada entidade. Digamos, precisamos calcular uma soma da fatura com algum algoritmo complicado que possa depender, digamos, de um tipo de cliente. Aqui está o que pode parecer:

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

Outra abordagem, porém, é separar uma lógica de negócios localizada no serviço de domínio por meio de eventos do domínio . Lembre-se de que essa abordagem implica apenas serviços de aplicativos diferentes, mas o mesmo escopo de transação do banco de dados.

A terceira abordagem é a que sou a favor: se eu me encontrar usando um serviço de domínio, isso provavelmente significa que perdi algum conceito de domínio, pois modelo meus conceitos principalmente com substantivos , não verbos. Portanto, idealmente, não preciso de nenhum serviço de domínio e boa parte de toda a minha lógica de negócios reside em decoradores .

Vadim Samokhin
fonte
6

Estou chocado ao ler algumas das respostas aqui.

É perfeitamente válido passar serviços de domínio para métodos de entidade no DDD para delegar alguns cálculos de negócios. Como exemplo, imagine que sua raiz agregada (uma entidade) precise acessar um recurso externo por meio de http para realizar alguma lógica de negócios e gerar um evento. Se você não injeta o serviço através do método comercial da entidade, de que outra forma o faria? Você instanciaria um cliente http dentro de sua entidade? Parece uma péssima ideia.

O que está incorreto é injetar serviços em agregados por meio de seu construtor. Mas, através de um método comercial, está tudo bem e perfeitamente normal.

diegosasw
fonte
1
Por que o caso que você deu não é de responsabilidade de um serviço de domínio?
e_i_pi
1
é um serviço de domínio, mas é injetado no método comercial. A camada de aplicação é apenas uma orquestradora,
diegosasw
Não tenho experiência com DDD, mas o Serviço de Domínio não deve ser chamado pelo Serviço de Aplicativo e, após a validação do Serviço de Domínio, continuar a chamar os métodos de Entidade por meio desse Serviço de Aplicativo? Estou enfrentando o mesmo problema no meu projeto, porque o Serviço de Domínio executa a chamada do banco de dados via repositório ... Não sei se está tudo bem.
Muflix 20/05/19
O serviço de domínio deve orquestrar; se você chamá-lo mais tarde do aplicativo, significa que você processa a resposta de alguma forma e depois faz alguma coisa com ela. Talvez isso pareça lógica de negócios. Nesse caso, ele pertence à camada Domínio e o aplicativo mais tarde simplesmente resolve a dependência e a injeta no agregado. O serviço de domínio pode ter injetado um repositório cujo banco de dados de execução de implementação deve pertencer à camada de infraestrutura (apenas a implementação, não a interface / contrato). Se ele descreve sua linguagem onipresente, ele pertence ao domínio.
Diegosasw 20/05/19
5

Está dentro das melhores práticas de DDD e OOP injetar serviços em chamadas de método de entidade?

Não, você não deve injetar nada na sua camada de domínio (isso inclui entidades, objetos de valor, fábricas e serviços de domínio). Essa camada deve ser independente de qualquer estrutura, bibliotecas ou tecnologia de terceiros e não deve fazer chamadas de E / S.

$order->getLineItems($orderService)

Isso está errado, pois o agregado não deve precisar de mais nada além de si próprio para devolver os itens do pedido. O toda Aggregate já deve ser carregada antes de sua chamada de método. Se você acha que isso deve ser carregado preguiçosamente, existem duas possibilidades:

  1. Seus limites de agregados estão incorretos, são muito grandes.

  2. Neste caso de uso, você usa o agregado apenas para leitura. A melhor solução é separar o modelo de gravação do modelo de leitura (por exemplo, use CQRS ). Nesta arquitetura mais limpa , você não tem permissão para consultar o modelo Agregado, mas um modelo de leitura.

Constantin Galbenu
fonte
Se eu precisar de uma chamada de banco de dados para validação, preciso chamá-la no serviço de aplicativo e passar um resultado ao serviço de domínio ou diretamente na raiz agregada, em vez de injetar o repositório no serviço de domínio?
Muflix 20/05/19
1
@Muflix sim, isso mesmo #
Constantin Galbenu
3

A ideia principal nos padrões táticos DDD: o aplicativo acessa todos os dados no aplicativo, agindo em uma raiz agregada. Isso implica que as únicas entidades acessíveis fora do modelo de domínio são as raízes agregadas.

A raiz agregada do pedido nunca produziria uma referência à sua coleção de itens de linha que permitiria modificar a coleção, nem produziria uma coleção de referências a qualquer item de linha que permitiria modificá-la. Se você deseja alterar o agregado do pedido, aplica-se o princípio de hollywood: "Diga, não pergunte".

Retornar valores de dentro do agregado é bom, porque os valores são inerentemente imutáveis; você não pode alterar meus dados alterando sua cópia deles.

Usar um serviço de domínio como argumento, para ajudar o agregado a fornecer os valores corretos, é uma coisa perfeitamente razoável de se fazer.

Normalmente, você não usaria um serviço de domínio para fornecer acesso aos dados que estão dentro do agregado, porque o agregado já deve ter acesso a ele.

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

Portanto, essa ortografia é estranha, se estivermos tentando acessar a coleção de valores de itens de linha desse pedido. A ortografia mais natural seria

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

Obviamente, isso pressupõe que os itens de linha já foram carregados.

O padrão usual é que a carga do agregado inclua todo o estado necessário para o caso de uso específico. Em outras palavras, você pode ter várias maneiras diferentes de carregar o mesmo agregado; seus métodos de repositório são adequados para a finalidade .

Essa abordagem não é algo que você encontrará no Evans original, onde ele assumiu que um agregado teria um único modelo de dados associado a ela. Ele cai mais naturalmente no CQRS.

VoiceOfUnreason
fonte
Obrigado por isso. Agora li cerca de metade do "livro vermelho" e tive o primeiro gosto de aplicar corretamente o Princípio de Hollywood na camada de infraestrutura. Relendo todas essas respostas, todas são boas, mas acho que a sua possui alguns pontos muito importantes em relação ao escopo lineItems()e à pré-carga após a primeira recuperação da Raiz Agregada.
e_i_pi
3

De um modo geral, objetos de valor pertencentes a agregados não têm repositório por si mesmos. É responsabilidade agregada da raiz preenchê-los. No seu caso, é responsabilidade do seu OrderRepository preencher os objetos de entidade Order e OrderLine.

Em relação à implementação da infraestrutura do OrderRepository, no caso do ORM, é um relacionamento de um para muitos, e você pode optar por carregar ansiosamente ou com preguiça o OrderLine.

Não sei ao certo o que seus serviços significam exatamente. É bem próximo do "Serviço de Aplicativo". Se for esse o caso, geralmente não é uma boa ideia injetar os serviços no objeto agregado raiz / entidade / valor. O Serviço de Aplicativo deve ser o cliente do Objeto Raiz / Entidade / Valor Agregado e Serviço de Domínio. Outra coisa sobre seus serviços é: expor objetos de valor no Application Service também não é uma boa ideia. Eles devem ser acessados ​​por raiz agregada.

ivenxu
fonte
2

A resposta é: definitivamente NÃO, evite passar serviços nos métodos de entidade.

A solução é simples: basta permitir que o repositório de pedidos retorne o pedido com todos os seus itens de linha. No seu caso, o agregado é Order + LineItems; portanto, se o repositório não retornar um agregado completo, ele não fará seu trabalho.

O princípio mais amplo é: manter os bits funcionais (por exemplo, lógica do domínio) separados dos bits não funcionais (por exemplo, persistência).

Mais uma coisa: se você puder, tente evitar fazer isso:

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

Faça isso

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

No design orientado a objetos, tentamos evitar a pesca nos dados de um objeto. Preferimos pedir ao objeto para fazer o que queremos.

xpmatteo
fonte