微服务中的代码组织以及我使用六角形体系结构和DDD的方法


哈Ha!在Monolith中,所有代码应采用相同的样式,并且在不同的微服务中可以使用不同的方法,编程语言和框架。对于具有1到2个控制器和1到10个动作的简单微服务,阻塞抽象层没有特别的意义。相反,对于具有不同状态以及它们之间转换逻辑的复杂微服务,最好不要一开始就懒惰。我想谈谈我在两种情况下组织代码以及使用DDD,Ports和Adapters方法的经验。该文章有一个简短摘要:June-在控制器中编写代码。中-写一堆抽象。高级-知道何时在控制器中编写代码,以及何时需要抽象。在这里,您需要立即结束I-C#中的端口是接口或抽象,而适配器是具体的实现(类,结构)。作为参考,我建议阅读DDD,六角形,洋葱形,清洁,CQRS的翻译……我如何将它们综合在一起,以及这篇有关清洁建筑的误解在这里,请记住,该方法是针对大型且复杂的整体描述的。我想谈谈这种方法在微服务中的变化,其中80%是简单的,而20种是中复杂的。

术语


1)主适配器


介绍用于外部呼叫系统的用户友好界面调用并使用SecondaryApdapters和Logic。他们告诉应用程序做某事。入口指向您的代码。典型代表:ItemsService,CreateItemCommandHandler。使用Logic和SecondaryAdapters进行工作。

2)SecondaryAdapters


提供与外部调用系统的接口,方便我们的系统使用。他从应用程序接收命令。典型代表:ItemsRepository,EmailSender,Logger,DistributedCache。他们使用诸如EntityFramework之类的库和框架进行工作。

3)逻辑


3.1)实体


结合数据和逻辑。典型的OOP对象。项目,金钱,用户。也有ValueObjects和Events。仅使用“实体”层中的其他对象。从理论上讲,该层是不依赖任何人的应用程序的核心。一切都取决于他。

3.2)DomainServices


裸计算。它们是无法附加到一个特定实体的逻辑所必需的。使用实体或其他DomainServices。不要使用PrimaryAdapters(它们总是自己使用)和SecondaryAdapters。通常,这是各种AmountCalculator,Validator等。

代码调用流应始终沿一个方向设置PrimaryAdapters-> Logic和SecondaryAdapters。


这是系统正在开发的主题领域。例如对于域在线商店,这是电子商务。

有界上下文


BoundedContext用于将系统划分为具有一定责任的隔离部分。在系统设计中成功的方法之一是在其中找到并突出显示其所有BoundedContext。在微服务架构中,1个微服务= 1 BoundedContext。例如:在线商店可能有一个BoundedContext购物车和BoundedContext处理订单,BoundedContext处理文件。一个大的BoundedContext可以分解成小块。例如,对于一篮子商品的BoundedContext,您可以将添加商品到购物篮的上下文和从篮子中删除商品的上下文进行划分。在整体1中,大的BoundedContext是一个模块。在这里有必要说明所有实体都生活在特定的上下文中,即从购物篮中的物品和从陈列柜中的物品来看,这些物品可以是具有不同字段和方法的不同类别。在整体中,它们简单地相互映射,并使用EF传递给数据库使用的一些ItemDto来使用,并且该字段具有所有人共同的字段,也就是说,如果来自购物篮上下文(模块)的Item具有属性Property1,而来自店面上下文的Item的商品具有属性Property2,则ItemDto将同时具有Property1和Property2。在每个上下文中,将有其自己的存储库,该存储库将从数据库中提取该上下文已具有的Item。在微服务中,这很容易。每个都有自己的数据库和本质。每个世界服务都有自己的类。只是在整体中,它们简单地相互映射,并使用EF传递给数据库工作的ItemDto,该ItemD具有所有人都通用的字段,即,如果Basket的上下文(模块)中的Item具有属性Property1,而店面的上下文中的Item的商品具有属性Property2,则ItemDto将同时具有Property1和Property2。在每个上下文中,将有其自己的存储库,该存储库将从数据库中提取该上下文已具有的Item。在微服务中,这很容易。每个都有自己的数据库和本质。每个世界服务都有自己的类。只是在整体中,它们简单地相互映射,并使用EF传递给数据库使用的一些ItemDto来使用,并且该字段具有所有人共同的字段,也就是说,如果来自购物篮上下文(模块)的Item具有属性Property1,而来自店面上下文的Item的商品具有属性Property2,则ItemDto将同时具有Property1和Property2。在每个上下文中,将有其自己的存储库,该存储库将从数据库中提取该上下文已具有的Item。在微服务中,这很容易。每个都有自己的数据库和本质。每个世界服务都有自己的类。只是在每个上下文中,将有其自己的存储库,该存储库将从数据库中提取该上下文已具有的Item。在微服务中,这很容易。每个都有自己的数据库和本质。每个世界服务都有自己的类。只是在每个上下文中,将有其自己的存储库,该存储库将从数据库中提取该上下文已具有的Item。在微服务中,这很容易。每个都有自己的数据库和本质。每个世界服务都有自己的类。只是请记住,来自不同BoundedContext的Item和ItemsRepository可以是具有不同方法和属性的不同对象。通常,微服务中的BoundedContext被引入一些常见的库而被违反。结果是一个类具有数十种方法或属性,并且每个微服务仅使用所需的1-2,因此您在使用微服务中的共享库时要格外小心。

层之间的依赖性




帕累托法则80%的简单微服务选项


丢弃层PrimaryAdatapters和SecondaryAdapters。仅保留逻辑层。更确切地说,在典型的ASP.NET应用程序中,我们使用-PimaryAdater是Controller,而SecondaryAdapter是EF中的DbContext。如果Controller突然变得太大,则可以使用局部将其切成碎片。这比繁殖不必要的抽象并花一些时间来更好。例如,Microsoft在docker容器上的EShop应用程序示例中针对BasketMicrotservice做到了这一点。是的,只需将代码写入控制器即可。只是不要写意粉代码。重要请记住,逻辑层仍然存在,我们仍然将实体与它们的逻辑结合使用,并将DomainServices与它们的计算结合使用,而不仅仅是在控制器中编写意大利面条式代码墙。我们只是丢弃了典型的ItemService和ItemRepository。本着这种精神,好的旧的ItemValidator,ItemMapper,AmountCalculator等仍然存在。由于我们仍然具有逻辑层,因此可以随时通过使用ItemsService,ItemsRepository包装其他抽象来切换到更复杂的选项。


以折扣价计算产品最终价格的服务。从理论上讲,它是Domain Online Store的一部分,代表其BoundedContext产品目录。为简单起见,我跳过了所有验证逻辑。可以用某种ItemValidator和DiscountValidator来描述。

1)逻辑文件夹:
1.1)实体文件夹:

折扣:

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

        public decimal Value { get; set; }
    }

钱:

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

产品:

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

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

1.2)DomainServices文件夹产品

价格计算器,包括折扣:

    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)控制器

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


面向20%的复杂微服务以及大多数整体的选项


在这里,我们使用一种标准方法,将各层之间的隔离程度最大化。为PrimaryAdapter(ItemService)和SecondaryAdapter(ItemRepository)添加一个类。在Logic文件夹中,所有内容均保持不变。


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

我们的控制器现在使用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;
        }
    }

添加了很多其他代码。相反,增加了系统灵活性。现在,将Contoller(通常是ASP.NET Core)更改为其他名称,或将DbContext和EntityFramework Core更改为其他名称,或将缓存添加到Redis变得更加容易。第一种方法在简单的微服务以及非常简单和小的整体中获胜。在所有其他情况下,第二个则需要您添加并完成此代码一年以上。好吧,在第二种方法中,除了实体项目和折扣之外,制作将使用DbContex EF的单独的DTO和ASP.NET Controller将使用的单独的DTO(模型)也很有用。 ItemDto和ItemModel。这将使域模型更加独立。最后,是我使用第二次TestRuvds方法编写的一个简单应用程序的示例实际上,他在这里是多余的,所有这些抽象都在此处作为示例。

实施实例


虚拟服务器模块

致谢


谢谢 Canxes安德鲁·登布 对于发现的语法错误。

All Articles