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 DomainServicesCalculadora 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
VirtualServersModuleAgradecimentos
obrigado canxes e AndrewDemb para erros gramaticais encontrados.