Organização do código em microsserviços e minha abordagem do uso da arquitetura hexagonal e DDD


Olá Habr! No Monolith, todo o código deve estar no mesmo estilo e, em diferentes microsserviços, você pode usar diferentes abordagens, linguagens de programação e estruturas. Para microsserviços simples com 1 a 2 controladores e 1 a 10 ações, não há um sentido particular em bloquear camadas de abstrações. Para microsserviços complexos com estados diferentes e a lógica de transição entre eles, pelo contrário, é melhor não ser preguiçoso inicialmente. Quero falar sobre minha experiência na organização de códigos e no uso das abordagens DDD, portas e adaptadores para ambos os casos. Há um breve resumo do artigo: junho - escreve o código no controlador. Meio - escreve um monte de abstrações. Sênior - sabe quando escrever código no controlador e quando são necessárias abstrações.Aqui você precisa colocar imediatamente um fim para I - Portas em C # são interface ou resumo, e Adaptadores são implementações concretas (classe, estrutura). Para referência, recomendo a leitura desta tradução de DDD, Hexagonal, Cebola, Limpa, CQRS ... como eu reuni tudo e este artigo de equívoco da Arquitetura Limpa . Aqui, lembre-se de que a abordagem é descrita para um monólito grande e complexo. Quero falar sobre as variações dessa abordagem em microsserviços, 80% dos quais são simples e 20 de média ou alta complexidade.

Terminologia


1) PrimaryAdapters


Introduzir uma interface amigável para um sistema de chamada externa . Ligue e use SecondaryApdapters and Logic. Eles dizem ao aplicativo para fazer alguma coisa. Pontos de entrada para o seu código. Representantes típicos: ItemsService, CreateItemCommandHandler. Use Logic e SecondaryAdapters para o trabalho deles.

2) Adaptadores secundários


Fornece uma interface para um sistema chamado externo , conveniente para uso por nosso sistema. Ele recebe comandos do aplicativo. Representantes típicos: ItemsRepository, EmailSender, Logger, DistributedCache. Eles usam bibliotecas e estruturas como EntityFramework para seu trabalho.

3) Lógica


3.1) Entidades


Combine dados e lógica. Objetos OOP típicos. Item, Dinheiro, Usuário. Há também ValueObjects e eventos. Apenas outros objetos da camada Entidades são usados. Em teoria, essa camada é o núcleo do aplicativo que não depende de ninguém. Todos dependem dele.

3.2) Serviços de Domínio


Computação nua. Eles são necessários para a lógica que não pode ser anexada a uma entidade específica. Use Entidades ou outros Serviços de Domínio. Não use PrimaryAdapters (eles sempre usam eles mesmos) e SecondaryAdapters. Geralmente, esse é todo o tipo de AmountCalculator, Validator e muito mais.

O fluxo de chamada de código sempre deve estar em uma direção PrimaryAdapters -> Logic e SecondaryAdapters.

Domínio


Essa é a área de assunto para a qual o sistema está sendo desenvolvido. Por exemplo, para a Loja Online do Domínio, é Comércio Eletrônico.

Contexto limitado


BoundedContext é usado para dividir o sistema em partes isoladas com uma certa responsabilidade. Uma das maneiras de obter sucesso no design do sistema é encontrar e destacar todos os seus BoundedContexts nele. Em uma arquitetura de microsserviço, 1 microsserviço = 1 BoundedContext. Por exemplo: Uma loja on-line pode ter um BoundedContext Shopping Cart e BoundedContext trabalhando com pedidos, BoundedContext trabalhando com arquivos. E um grande BoundedContext pode ser dividido em pequenos pedaços dentro. Por exemplo, para o BoundedContext da cesta de mercadorias, você pode fazer uma divisão no contexto de adicionar mercadorias à cesta e no contexto de remover mercadorias da cesta. No monólito 1, um BoundedContext grande é um módulo. Aqui é necessário fazer uma observação de que todas as Entidades vivem dentro de um contexto específico, ou seja,Item do contexto da cesta de mercadorias e Item do contexto da vitrine de mercadorias, podem ser diferentes classes com diferentes campos e métodos. Em um monólito, eles simplesmente mapeiam um ao outro e usam algum ItemD para que o EF passe para trabalhar com o banco de dados e que possui campos comuns a todos, ou seja, se Item do contexto (módulo) do Cesto tiver a propriedade Property1 e Item do contexto da fachada da loja de mercadorias possui a propriedade Property2, o ItemDto terá a Property1 e a Property2. Em cada contexto, haverá seu próprio repositório, que retirará o Item, que já é característico desse contexto, do banco de dados. Em microsserviços, isso é fácil. Cada um tem seu próprio banco de dados e sua própria essência. Cada serviço mundial tem suas próprias classes. SimplesmenteEm um monólito, eles simplesmente mapeiam um ao outro e usam algum ItemD para que o EF passe para trabalhar com o banco de dados e que possui campos comuns a todos, ou seja, se Item do contexto (módulo) do Cesto tiver a propriedade Property1 e Item do contexto da montra de mercadorias possui a propriedade Property2, o ItemDto terá a Property1 e a Property2. Em cada contexto, haverá seu próprio repositório, que retirará o Item, que já é característico desse contexto, do banco de dados. Em microsserviços, isso é fácil. Cada um tem seu próprio banco de dados e sua própria essência. Cada serviço mundial tem suas próprias classes. SimplesmenteEm um monólito, eles simplesmente mapeiam um ao outro e usam algum ItemD para que o EF passe para trabalhar com o banco de dados e que possui campos comuns a todos, ou seja, se Item do contexto (módulo) do Cesto tiver a propriedade Property1 e Item do contexto da montra de mercadorias possui a propriedade Property2, o ItemDto terá a Property1 e a Property2. Em cada contexto, haverá seu próprio repositório, que retirará o Item, que já é característico desse contexto, do banco de dados. Em microsserviços, isso é fácil. Cada um tem seu próprio banco de dados e sua própria essência. Cada serviço mundial tem suas próprias classes. SimplesmenteEm cada contexto, haverá seu próprio repositório, que retirará o Item, que já é característico desse contexto, do banco de dados. Em microsserviços, isso é fácil. Cada um tem seu próprio banco de dados e sua própria essência. Cada serviço mundial tem suas próprias classes. SimplesmenteEm cada contexto, haverá seu próprio repositório, que retirará o Item, que já é característico desse contexto, do banco de dados. Em microsserviços, isso é fácil. Cada um tem seu próprio banco de dados e sua própria essência. Cada serviço mundial tem suas próprias classes. Simplesmentelembre-se de que Item e ItemsRepository de diferentes BoundedContext podem ser objetos diferentes com métodos e propriedades diferentes. Freqüentemente, o BoundedContext nos microsserviços é violado pela introdução de algumas bibliotecas comuns. O resultado é uma classe que possui várias dezenas de métodos ou propriedades e cada microsserviço usa 1 - 2 apenas o necessário, portanto, você deve ter cuidado com as bibliotecas compartilhadas nos microsserviços.

Dependências entre camadas




Opção para microsserviços simples, dos quais 80% da lei de Pareto


Jogue fora a camada PrimaryAdatapters e SecondaryAdapters. Deixe apenas a camada lógica. Mais precisamente, em um aplicativo ASP.NET típico que usamos - PimaryAdater é Controller e SecondaryAdapter é DbContext da EF. Se o Controlador de repente se tornar muito grande, cortamos em pedaços usando parcial. Isso é muito melhor do que criar abstrações desnecessárias e gastar tempo com elas. Por exemplo, a Microsoft fez isso para o BasketMicrotservice no exemplo do aplicativo de contêiner EShop docker. Sim, basta escrever o código no controlador. Só não escreva código espaguete. Importantelembre-se de que a camada lógica permanece conosco e ainda usamos Entidades com sua lógica e DomainServices com seus cálculos e não apenas escrevemos uma parede de código espaguete no controlador. Acabamos de jogar fora o ItemService e o ItemRepository típicos. O bom e velho ItemValidator, ItemMapper, AmountCalculator etc. nesse espírito ainda permanecem conosco. Como ainda temos a camada Lógica, podemos mudar para uma opção mais complexa a qualquer momento, agrupando abstrações adicionais com ItemsService, ItemsRepository.

Exemplo


Um serviço que calcula o preço final de um produto com desconto. Em teoria, faz parte da Domain Online Store e representa seu catálogo de produtos BoundedContext. Para simplificar, pulei toda a lógica de validação. Pode ser descrito em algum tipo de ItemValidator e DiscountValidator.

1) Pasta lógica:
1.1) Pasta entidades:

Desconto:

    public class Discount
    {
        [Key]
        public Guid Id { get; set; }

        public decimal Value { get; set; }
    }

Dinheiro:

    [Owned]
    public class Money
    {
        public decimal Amount { get; set; }

        public Money Apply(Discount discount)
        {
            var amount = Amount * (1 - discount.Value);
            return new Money()
            {
                Amount = amount
            };
        }
    }

Produtos:

    public class Item
    {
        [Key]
        public Guid Id { get; set; }

        public Money Price { get; set; } = new Money();
    }

1.2) Pasta DomainServices

Calculadora de preços de produtos , incluindo descontos:

    public interface IPriceCalculator
    {
        Money WithDiscounts(Item item, IEnumerable<Discount> discounts);
    }

     public sealed class PriceCalculator:IPriceCalculator
    {
        public Money WithDiscounts(Item item, IEnumerable<Discount> discounts)
        {
            return discounts.Aggregate(item.Price, (m, d) => m.Apply(d));
        }
    }

2) DbContext

    public class ItemsDbContext : DbContext
    {
        public ItemsDbContext(DbContextOptions<ItemsDbContext> options) : base(options)
        {

        }

        public DbSet<Item> Items { get; set; }

        public DbSet<Discount> Discounts { get; set; }
    }

3) Controlador

    [ApiController]
    [Route("api/v1/items")]
    public class ItemsController : ControllerBase
    {
        private readonly ItemsDbContext _context;
        private readonly IPriceCalculator _priceCalculator;

        public ItemsController(ItemsDbContext context, IPriceCalculator priceCalculator)
        {
            _context = context;
            _priceCalculator = priceCalculator;
        }

        [HttpGet("{id}/price-with-discounts")]
        public async Task<decimal> GetPriceWithDiscount(Guid id)
        {
            var item = await _context.Items.FindAsync(id);
            var discounts = await _context.Discounts.ToListAsync();
            return _priceCalculator.WithDiscounts(item, discounts).Amount;
        }
    }


Opção para microsserviços complexos, dos quais 20% e para a maioria dos monólitos


Aqui, usamos uma abordagem padrão com o máximo isolamento das camadas uma da outra. Adicione uma classe para PrimaryAdapter (ItemService) e SecondaryAdapter (ItemRepository). Na pasta Lógica, tudo permanece como antes.

Exemplo


1) Pasta SecondaryAdapters

    public interface IItemsRepository
    {
        Task<Item> Get(Guid id);
    }

    public sealed class ItemsRepository : IItemsRepository
    {
        private readonly ItemsDbContext _context;

        public ItemsRepository(ItemsDbContext context)
        {
            _context = context;
        }

        public async Task<Item> Get(Guid id)
        {
            var item = await _context.Items.FindAsync(id);
            return item;
        }
    }

    public interface IDiscountsRepository
    {
        Task<List<Discount>> Get();
    }

    public sealed class DiscountsRepository : IDiscountsRepository
    {
        private readonly ItemsDbContext _context;

        public DiscountsRepository(ItemsDbContext context)
        {
            _context = context;
        }

        public Task<List<Discount>> Get()
        {
            return _context.Discounts.ToListAsync();
        }
    }


2) Pasta PrimaryAdapters

    public interface IItemService
    {
        Task<decimal> GetPriceWithDiscount(Guid id);
    }

    public class ItemService : IItemService
    {
        private readonly IItemsRepository _items;
        private readonly IDiscountsRepository _discounts;
        private readonly IPriceCalculator _priceCalculator;

        public ItemService(IItemsRepository items, IDiscountsRepository discounts, IPriceCalculator priceCalculator)
        {
            _items = items;
            _discounts = discounts;
            _priceCalculator = priceCalculator;
        }

        public async Task<decimal> GetPriceWithDiscount(Guid id)
        {
            var item = await _items.Get(id);
            var discounts = await _discounts.Get();
            return _priceCalculator.WithDiscounts(item, discounts).Amount;
        }
    }

Nosso controlador agora usa nosso IItemService

    [ApiController]
    [Route("api/v1/items")]
    public class ItemsController : ControllerBase
    {
        private readonly IItemService _service;

        public ItemsController(IItemService service)
        {
            _service = service;
        }

        [HttpGet("{id}/price-with-discounts")]
        public async Task<decimal> GetPriceWithDiscount(Guid id)
        {
            var result = await _service.GetPriceWithDiscount(id);
            return result;
        }
    }

Adicionado muito código adicional. Em vez disso, maior flexibilidade do sistema. Agora é mais fácil alterar o Contoller e, geralmente, o ASP.NET Core para outra coisa, ou alterar o DbContext e o EntityFramework Core para outra coisa ou adicionar o cache ao Redis. A primeira abordagem vence em microsserviços simples e monólitos muito simples e pequenos. O segundo em todos os outros casos, quando você precisará adicionar e concluir esse código por mais de um ano. Bem, na segunda abordagem, além do Item da entidade e do desconto, é útil criar DTOs separados que usarão o DbContex EF e DTOs (modelos) que o ASP.NET Controller usará, por exemplo. ItemDto e ItemModel. Isso tornará os modelos de domínio ainda mais independentes. E, finalmente, um exemplo de um aplicativo simples que escrevi usando a segunda abordagem do TestRuvds. De fato, ele é redundante aqui, e todas essas abstrações estão aqui como exemplo.

Exemplo de implementação


VirtualServersModule

Agradecimentos


obrigado canxes e AndrewDemb para erros gramaticais encontrados.

All Articles