Organización de código en microservicios y mi enfoque de usar arquitectura hexagonal y DDD


Hola Habr! En Monolith, todo el código debe estar en el mismo estilo, y en diferentes microservicios puede usar diferentes enfoques, lenguajes de programación y marcos. Para microservicios simples con 1 a 2 controladores y 1 a 10 acciones, no tiene sentido particular bloquear capas de abstracciones. Para microservicios complejos con diferentes estados y la lógica de transición entre ellos, por el contrario, es mejor no ser flojo inicialmente. Quiero hablar sobre mi experiencia con la organización del código y el uso de los enfoques DDD, Puertos y Adaptadores para ambos casos. Hay un breve resumen del artículo: junio: escribe código en el controlador. Medio: escribe un montón de abstracciones. Senior: sabe cuándo escribir código en el controlador y cuándo se necesitan abstracciones.Aquí debe poner fin inmediatamente a I: los puertos en C # son interfaz o resumen, y los adaptadores son implementaciones concretas (clase, estructura). Como referencia, recomiendo leer esta traducción de DDD, Hexagonal, Onion, Clean, CQRS ... cómo lo puse todo junto y este artículo de concepción errónea de Clean Architecture . Aquí, solo tenga en cuenta que el enfoque se describe para un monolito grande y complejo. Quiero hablar sobre las variaciones de este enfoque en microservicios, el 80 por ciento de los cuales son simples y 20 de complejidad media o alta.

Terminología


1) Adaptadores primarios


Introducir una interfaz fácil de usar para un sistema de llamadas externo . Llame y use SecondaryApdapters y Logic. Le dicen a la aplicación que haga algo. Puntos de entrada a su código. Representantes típicos: ItemsService, CreateItemCommandHandler. Use Logic y SecondaryAdapters para su trabajo.

2) adaptadores secundarios


Proporciona una interfaz a un sistema externo llamado conveniente para su uso por nuestro sistema. Recibe comandos de la aplicación. Representantes típicos: ItemsRepository, EmailSender, Logger, DistributedCache. Utilizan bibliotecas y marcos como EntityFramework para su trabajo.

3) lógica


3.1) Entidades


Combina datos y lógica. Objetos típicos de OOP. Artículo, dinero, usuario. También hay ValueObjects y Events. Solo se utilizan otros objetos de la capa Entidades. En teoría, esta capa es el núcleo de la aplicación que no depende de nadie. Todo depende de él.

3.2) Servicios de dominio


Informática desnuda. Son necesarios para la lógica que no se puede conectar a una entidad específica. Use entidades u otros servicios de dominio. No use Adaptadores primarios (siempre se usan por sí mismos) y Adaptadores secundarios. Por lo general, este es todo tipo de AmountCalculator, Validator y más.

El flujo de la llamada de código siempre debe estar en una dirección. Adaptadores primarios -> Adaptadores lógicos y secundarios.

Dominio


Esta es el área temática para la cual se está desarrollando el sistema. Por ejemplo, para la tienda en línea de dominio, esto es comercio electrónico.

Contexto limitado


BoundedContext se utiliza para dividir el sistema en partes aisladas con una cierta responsabilidad. Una de las formas de éxito en el diseño del sistema es encontrar y resaltar todos sus BoundedContexts en él. En una arquitectura de microservicio, 1 microservicio = 1 BoundedContext. Por ejemplo: una tienda en línea puede tener un carrito de compras BoundedContext y BoundedContext trabajando con pedidos, BoundedContext trabajando con archivos. Y un gran BoundedContext se puede romper en pedazos pequeños dentro. Por ejemplo, para el BoundedContext de la cesta de mercancías, puede hacer una división en el contexto de agregar mercancías a la cesta y el contexto de eliminar mercancías de la cesta. En el monolito 1, un gran BoundedContext es un módulo. Aquí es necesario hacer un comentario de que todas las Entidades viven dentro de un contexto específico, es decirArtículo del contexto de la canasta de bienes y Artículo del contexto de la exhibición de bienes, estos pueden ser diferentes clases con diferentes campos y métodos. En un monolito, simplemente se mapean entre sí y usan algún ItemDto que EF pasa para trabajar con la base de datos y que tiene campos que son comunes a todos, es decir, si Item del contexto (módulo) de la Cesta tiene la propiedad Property1, y Item del contexto del escaparate de los bienes tiene la propiedad Property2, entonces ItemDto tendrá Property1 y Property2. En cada contexto habrá su propio repositorio que extraerá el Elemento, que ya es característico de este contexto, de la base de datos. En microservicios, esto es fácil. Cada uno tiene su propia base de datos y su propia esencia. Cada servicio mundial tiene sus propias clases. SimplementeEn un monolito, simplemente se mapean entre sí y usan algún ItemDto que EF pasa para trabajar con la base de datos y que tiene campos que son comunes a todos, es decir, si Item del contexto (módulo) de la Cesta tiene la propiedad Property1, y Item del contexto del escaparate de los bienes tiene la propiedad Property2, entonces ItemDto tendrá Property1 y Property2. En cada contexto habrá su propio repositorio que extraerá el Elemento, que ya es característico de este contexto, de la base de datos. En microservicios, esto es fácil. Cada uno tiene su propia base de datos y su propia esencia. Cada servicio mundial tiene sus propias clases. SimplementeEn un monolito, simplemente se mapean entre sí y usan algún ItemDto que EF pasa para trabajar con la base de datos y que tiene campos que son comunes a todos, es decir, si Item del contexto (módulo) de la Cesta tiene la propiedad Property1, y Item del contexto del escaparate de los bienes tiene la propiedad Property2, entonces ItemDto tendrá Property1 y Property2. En cada contexto habrá su propio repositorio que extraerá el Elemento, que ya es característico de este contexto, de la base de datos. En microservicios, esto es fácil. Cada uno tiene su propia base de datos y su propia esencia. Cada servicio mundial tiene sus propias clases. SimplementeEn cada contexto habrá su propio repositorio que extraerá el Elemento, que ya es característico de este contexto, de la base de datos. En microservicios, esto es fácil. Cada uno tiene su propia base de datos y su propia esencia. Cada servicio mundial tiene sus propias clases. SimplementeEn cada contexto habrá su propio repositorio que extraerá el Elemento, que ya es característico de este contexto, de la base de datos. En microservicios, esto es fácil. Cada uno tiene su propia base de datos y su propia esencia. Cada servicio mundial tiene sus propias clases. Simplementerecuerde que Item y ItemsRepository de diferentes BoundedContext pueden ser diferentes objetos con diferentes métodos y propiedades. A menudo, BoundedContext en microservicios es violado por la introducción de algunas bibliotecas comunes. El resultado es una clase que tiene varias docenas de métodos o propiedades y cada microservicio usa 1 - 2 solo lo que necesita, por lo que debe tener cuidado con las bibliotecas compartidas en microservicios.

Dependencias entre capas




Opción para microservicios simples de los cuales ley de Pareto 80 por ciento


Deseche la capa PrimaryAdatapters y SecondaryAdapters. Deje solo la capa lógica. Más precisamente, en una aplicación ASP.NET típica que utilizamos: PimaryAdater es el controlador y SecondaryAdapter es el DbContext de EF. Si el controlador de repente se vuelve demasiado grande, entonces lo cortamos en pedazos usando parcial. Esto es mucho mejor que generar abstracciones innecesarias y dedicarles tiempo. Por ejemplo, Microsoft hizo esto para el servicio BasketMicrots en el ejemplo de su aplicación EShop en contenedores acoplables. Sí, solo escriba el código en el controlador. Simplemente no escriba el código de espagueti. Importanterecuerde que la capa lógica permanece con nosotros y todavía usamos entidades con su lógica y servicios de dominio con sus cálculos y no solo escribimos un muro de código de espagueti en el controlador. Simplemente tiramos el típico ItemService y ItemRepository. El buen viejo ItemValidator, ItemMapper, AmountCalculator, etc. en este espíritu todavía permanecen con nosotros. Como todavía tenemos una capa lógica formada, podemos cambiar a una opción más complicada en cualquier momento envolviendo abstracciones adicionales con ItemsService, ItemsRepository.

Ejemplo


Un servicio que calcula el precio final de un producto con descuento. En teoría, es parte de la Tienda en línea de dominio y representa su catálogo de productos BoundedContext. Por simplicidad, salté toda la lógica de validación. Se puede describir en algún tipo de ItemValidator y DiscountValidator.

1) Carpeta lógica:
1.1) Carpeta de entidades:

Descuento:

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

        public decimal Value { get; set; }
    }

Dinero:

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

Producto:

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

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

1.2) Carpeta DomainServices

Calculadora de precios del producto , incluidos descuentos:

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


Opción para microservicios complejos de los cuales 20 por ciento y para la mayoría de los monolitos


Aquí usamos un enfoque estándar con el máximo aislamiento de las capas entre sí. Agregue una clase para PrimaryAdapter (ItemService) y para SecondaryAdapter (ItemRepository). En la carpeta Logic, todo permanece como estaba antes.

Ejemplo


1) Carpeta 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) Carpeta 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;
        }
    }

Nuestro controlador ahora usa nuestro 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;
        }
    }

Se agregó mucho código adicional. En cambio, mayor flexibilidad del sistema. Ahora es más fácil cambiar el Contoller y, en general, ASP.NET Core a otra cosa o cambiar el DbContext y EntityFramework Core a otra cosa o agregar el almacenamiento en caché a Redis. El primer enfoque gana en microservicios simples y monolitos muy simples y pequeños. El segundo en todos los demás casos, cuando necesitará agregar y finalizar este código por más de un año. Bueno, en el segundo enfoque, además del Artículo de Entidad y el Descuento, es útil hacer DTO separados que usarán DbContex EF y DTO (Modelos) separados que usará el Controlador ASP.NET, es decir. ItemDto y ItemModel. Esto hará que los modelos de dominio sean aún más independientes. Y finalmente, un ejemplo de una aplicación simple que escribí usando el segundo enfoque de TestRuvds. De hecho, él es redundante aquí, y todas estas abstracciones están aquí por ejemplo.

Ejemplo de implementación


VirtualServersModule

Expresiones de gratitud


gracias canxes y AndrewDemb por errores gramaticales encontrados.

All Articles