Persistindo Entidades Grandes / Complexas com o Padrão de Comando - Estou fazendo certo?

5

Estou no processo de projetar e construir um software como serviço de gerenciamento de inventário em larga escala que, esperançosamente, levará uma vida longa e frutífera. Portanto, estou me esforçando muito para garantir que o aplicativo seja passível de manutenção e seja executado horizontalmente. Eu realmente gostei esta palestra de Mathias Verraes sobre o desacoplamento que inspirou a experimentar o Padrão de Comando, uma vez que empresta a um código altamente desacoplado e muito explícito.

Em termos de tecnologia, estou usando o Laravel 5 e o Doctrine 2, apoiados por um banco de dados MySQL.

Embora Mathias fale sobre o assunto "sair da mentalidade CRUD", acho que todos podemos concordar que existem muitas situações em que a linguagem de negócios de fato reflete o CRUD. Por exemplo, o gerente de logística diz "Preciso CRIAR um Pedido de Compra que eu ENVIARÁ ao Fornecedor". A parte "ENVIAR" acende como um ótimo caso de uso para um comando, mas não tanto para a parte CREATE.

Deixe-me explicar minha posição na esperança de que alguém possa concordar / discordar e talvez me aponte para uma direção melhor.

Eu sinto como se o Padrão de Comando não fosse particularmente adequado para o ato de manipular uma solicitação CREATE para um objeto complexo, como um Pedido de Compra. No meu domínio, o pedido de compra contém informações como:

  • Número / identificador do pedido definido pelo usuário (por exemplo, PO1234)
  • Fornecedor para o qual o pedido será enviado para
  • O endereço de entrega
  • O endereço para cobrança
  • O método de envio solicitado e Courier
  • Termos da NET
  • A moeda monetária
  • Um pedido de venda e um cliente, se o pedido tiver que ser enviado remotamente
  • Um ou mais itens de linha

Se bem entendi, o Objeto de Comando deve ser essencialmente um relativamente simples DTO que forneça um contrato para o mundo externo para interagir com o aplicativo. "Se você quiser criar um novo pedido, preencha este objeto com dados e envie-o pelo barramento" . Como desenvolvedor, é muito explícito emitir um CreatePurchaseOrderCommand para o aplicativo - eu gosto de como isso soa, mas quando se trata de execução, parece um pouco desajeitado.

Por meio desajeitado, parece estranho extrapolar todos os dados acima do objeto Request e atualizar um CreatePurchaseOrderCommand com não menos que 9 argumentos , alguns dos quais seriam matrizes , talvez um ValueObject ou dois (ou isso introduziria o acoplamento ??).

Eu criei um código semelhante a este:

EDIT Eu atualizei o OrderController para refletir minhas necessidades de saída de um objeto codificado JSON após um CREATE bem-sucedido para satisfazer as necessidades da minha estrutura do lado do cliente. Isso é considerado uma boa abordagem? Não tenho nenhum problema em injetar o repositório no Controlador, pois quase certamente será necessário para outros métodos de leitura.

OrderController.php

public function __construct(ValidatingCommandBus $commandBus, OrderRepositoryInterface $repository) { .. }
public function create(CreateOrderRequest $request){
     $uuid = UUID::generate();
     $command = new CreatePurchaseOrderCommand($uuid, $request->all());

     try
     {
         $this->commandBus->execute($command);
         $order = $this->repository->findByUuid($uuid);
         return $this->response()->json([
             'success' => true,
             'data'    => $order->jsonSerialize()
         ]);
     }catch(OrderValidationException $e)
     {
         return $this->response()->json([
             'success' => false,
             'message' => $e->getMessage()
         ]);
     }
}

ValidatingCommandBus.php (decora BaseCommandBus)

public function __construct(BaseCommandBus $baseCommandBus, IoC $container, CommandTranslator $translator) { .. }
public function execute(Command $command){
    // string manipulation to CreatePurchaseOrderValidator
    $validator = $this->translator->toValidator($command);

    // build validator class from IOC container
    // validates the command's data, might throw exception
    // does *not* validate set constraints e.g., uniqueness of order number
    // this is answering "does this look like a valid command?"
    $this->container->make($validator)->validate($command) 

    // pass off to the base command bus to execute
    // invokes CreatePurchaseOrderCommandHandler->handle($command)
    $this->baseCommandBus->execute($command)
}

CreatePurchaseOrderCommandHandler.php

public function __construct(PurchaseOrderBuilderService $builder, PurchaseOrderRepositoryInterface $repository){ .. }
public function handle(CreatePurchaseOrderCommand $command){

    // this again? i'm pulling the same data as I pulled from the
    // Request object back in the Controller, now I'm just getting
    // the same data out of the Command object. Seems repetitive...
    $order = $this->builder->build([
       $command->order_number,
       $command->supplier_id,
    ]);

    // now maybe I should handle set constraints?
    // ensure order number is unique, order is not stale... etc.
    $orderNumberIsUnique = new OrderNumberIsUniqueSpecification($this->repository);
    if ( ! $orderNumberIsUnique->isSatisfiedBy($order) ){
        throw new \ValidationException("The Order Number is not unique");
    }

    // ok now I can persist the entity...
    try
    {
        // start a transaction
        $this->repository->persist($order);
    }catch(SomeDbException $e)
    {
        // roll back transaction
        // cant return anything so i'll throw another exception?
        throw new ErrorException('Something went wrong', $e);
    }

    // no return here as that breaks the CommandBus pattern :|
}

De uma perspectiva de código, acho que parece ser o caminho lógico para fazer as coisas em termos de posicionamento de validação e tal. Mas, no final do dia, não parece a solução certa.

Eu também não gosto que eu não possa retornar um valor do Command Bus, o que me deixa com a opção de mudar para gerar um UUID antes de persistir (atualmente confiando em AUTO INC do MySQL) ou fazer uma pesquisa o identificador único outro (o número do pedido) que pode não funcionar para todas as entidades (ou seja, nem toda entidade / agregado terá alguma outra forma de identificador exclusivo além do ID do banco de dados).

Estou usando e entendendo o padrão de comando corretamente? As minhas preocupações são válidas (veja comentários em exemplos de código). Estou faltando alguma coisa sobre o padrão de comando? Qualquer entrada seria ótima !!

    
por John Hall 17.08.2015 / 18:12
fonte

2 respostas

2

Mantenha o seu DTO simples

Você está certo de que ter um comando com 9 argumentos no construtor é feio. Mas você realmente tem que colocar essas coisas no construtor? Torne seus campos públicos, crie seu comando com um construtor vazio e apenas designe para os campos. O ponto dos argumentos do construtor é ter entidades válidas garantidas, uma vez que você está apenas usando um DTO e a validação será executada por outra entidade, não há muito sentido em ocultar dados ou ter construtores complicados. Campos públicos até os comandos.

Nada de errado com o valor-DTO

Se você tiver um Comando composto por um conjunto de campos que estejam juntos, sinta-se à vontade para mapeá-los para um objeto de valor que pertença ao seu DTO. Não há regra dura que diga que você não deveria fazer isso.

Os comandos não retornam nada

Um comando é uma alteração nos objetos do seu domínio. Você não deve precisar retornar nada do seu comando. Por que você precisaria devolver o id? Você criou o pedido, conforme solicitado pelo comando, e é isso. Há uma exceção que você pode considerar no entanto e isso é ...

Validação

Validação simples, como não nula / não vazia, datas válidas, regex-para-email, tudo pertence a um validador. A chamada para este validador é encadeada antes que a chamada para o seu commandhandler para o manipulador seja garantida para receber um comando válido.

Para uma validação complexa, existem diferentes opiniões que você pode analisar. Algumas pessoas dizem que mesmo a validação complexa que requer que você toque no banco de dados ('username already taken') não deva pertencer ao validador, alguns preferem ter toda essa validação no validador (que requer repositórios) e não executar nenhuma validação no comando qualquer. No final, você precisa decidir sobre uma estratégia para lidar com falhas na validação de comandos.

Dependendo de sua preferência, isso pode levar você a que os ValidationResults sejam retornados de seu comando, interceptando e exibindo CommandValidationExceptions quando você chamar o comando ...

    
por 19.08.2015 / 16:09
fonte
0

No meu aplicativo, o fluxo lógico é

  • O controlador constrói o DTO
  • Controladores despacham para o barramento de comando
  • Eventos de chamadas do barramento de comando
  • Os manipuladores de eventos também enviam para o barramento de comando
  • O Manipulador de Comandos retorna um DTO de resposta ao controlador
  • O controlador gera uma resposta para o cliente com base na resposta

Seu aplicativo parece semelhante. O desacoplamento também é um problema para mim. No entanto, o problema de encadear (ou aninhar) comandos ou eventos não foi um problema. Mantenha uma lista de UUIDs e objetos associados gerados em cada cadeia de comandos, se possível.

    
por 19.08.2015 / 07:05
fonte