zoukankan      html  css  js  c++  java
  • eShopOnWeb 项目结构分析

    目的:学习系统的设计架构模式,为下一步的微服务做准备。

    项目结构一览:

     非常标准常见的 .NET 源程序,项目源文件放于src文件夹中,功能集成测试等放于tests文件夹中。


     ApplicationCore:

      

       如其名,是整个项目的核心,其中包含了实体定义,自定义异常,自定义拓展,接口,服务等功能。

      

       通过观察实体的结构,我们可以发现此项目使用的DDD的设计模式,将每一个业务对象分离聚合设计为聚合根。那么什么又是聚合根呢,我的理解为一个对象的行为和特征都由自己来控制,举个例子,我们作为人有自己的姓名,身高,体重,我们也有着别人不可帮我们替代的事情需要我们自己做,比如睡觉,吃饭等。这里以Basket(购物车)为例,来理解一下它的设计。

    public class Basket : BaseEntity, IAggregateRoot
    {
        public string BuyerId { get; private set; }
        private readonly List<BasketItem> _items = new List<BasketItem>();
        public IReadOnlyCollection<BasketItem> Items => _items.AsReadOnly();
    
        public Basket(string buyerId)
        {
            BuyerId = buyerId;
        }
    
        public void AddItem(int catalogItemId, decimal unitPrice, int quantity = 1)
        {
            if (!Items.Any(i => i.CatalogItemId == catalogItemId))
            {
                _items.Add(new BasketItem(catalogItemId, quantity, unitPrice));
                return;
            }
            var existingItem = Items.FirstOrDefault(i => i.CatalogItemId == catalogItemId);
            existingItem.AddQuantity(quantity);
        }
    
        public void RemoveEmptyItems()
        {
            _items.RemoveAll(i => i.Quantity == 0);
        }
    
        public void SetNewBuyerId(string buyerId)
        {
            BuyerId = buyerId;
        }
    }

      它集成了BaseEntity(基类型,通常设计为只含有ID的泛型类),实现了IAggregate(一个空的mark接口,表示它为一个聚合根)接口。这里的Id(从基类集成而来),BuyerId,BasketItem都是它所包含有的属性,那么和它相关的自身行为,也应该被设计到聚合根中,如类中的AddItem(将商品加入到购物车中),RemoveEmptyItems(将个数为0的商品移除购物车),SetNewBuyerId等,这些行为都是与它或与它的属性相关的。

      再来看与他相关联的BasketItem:

    public class BasketItem : BaseEntity
    {
        public decimal UnitPrice { get; private set; }
        public int Quantity { get; private set; }
        public int CatalogItemId { get; private set; }
        public int BasketId { get; private set; }
    
        public BasketItem(int catalogItemId, int quantity, decimal unitPrice)
        {
            CatalogItemId = catalogItemId;
            UnitPrice = unitPrice;
            SetQuantity(quantity);
        }
    
        public void AddQuantity(int quantity)
        {
            Guard.Against.OutOfRange(quantity, nameof(quantity), 0, int.MaxValue);
    
            Quantity += quantity;
        }
    
        public void SetQuantity(int quantity)
        {
            Guard.Against.OutOfRange(quantity, nameof(quantity), 0, int.MaxValue);
    
            Quantity = quantity;
        }
    }

      可以看到它也有着自己的属性和业务行为,但它明显没有设计成一个聚合根(没有实现IAggregateRoot接口),为什么呢?我认为是它的业务包含能力是小于Basket的,在这个系统中,没有直接将一个商品直接购买的情况,流程都是要先加入购物车在进行付款购买。类似于我们在告诉别人的我们吃饭了的时候,都是说的“我吃了”,而不是“我的胃吃了”。如果在这个系统中,也有着直接将某个商品进行购买的业务场景,那么我认为也可以将BasketItem设计成聚合根。

      在一个聚合根中同样还包含了自定义异常,这里的异常我认为是可以统一放到一个文件夹中,但只限于聚合根和异常不多的情况下,如果异常多了起来放在一个文件夹就显得很杂乱,所以归类于每个聚合根中不妨是一种不错的选择。

      

       异常,拓展文件夹通常用于存放自己定义的异常和拓展。

      

      系统中常用的接口,这里对以下几个接口谈谈理解:

      IAggregateRoot,表示一个实体是否为聚合根接口,它是一个marker interface,即接口中没有定义属性,索引器,方法等。

    public interface IAggregateRoot
    { }

      它的主要作用是用来标记区分实体是否为聚合根,以及作为某些泛型接口或泛型方法中的泛型约束。

      如我们这里的IAsyncRepository接口定义为:

    public interface IAsyncRepository<T> where T : BaseEntity, IAggregateRoot
    {
        Task<T> GetByIdAsync(int id, CancellationToken cancellationToken = default);
        Task<IReadOnlyList<T>> ListAllAsync(CancellationToken cancellationToken = default);
        Task<IReadOnlyList<T>> ListAsync(ISpecification<T> spec, CancellationToken cancellationToken = default);
        Task<T> AddAsync(T entity, CancellationToken cancellationToken = default);
        Task UpdateAsync(T entity, CancellationToken cancellationToken = default);
        Task DeleteAsync(T entity, CancellationToken cancellationToken = default);
        Task<int> CountAsync(ISpecification<T> spec, CancellationToken cancellationToken = default);
        Task<T> FirstAsync(ISpecification<T> spec, CancellationToken cancellationToken = default);
        Task<T> FirstOrDefaultAsync(ISpecification<T> spec, CancellationToken cancellationToken = default);
    }

      此接口的定义,保证了只有聚合根对象才能够操纵数据的能力,因此就保证了业务的一致性。如我们就不能以BasketItem创建一个仓储,因为添加了购物车物品到数据库,却没有购物车信息这样的逻辑是不正确的,而这种设计就让我们在代码编写时就提供了一种业务封装的保护。

      其他的如处理具体业务的IBasketService,IOrderService的定义,以及IOrderService对IAsyncRepository接口中的存在的方法基于自己的业务进行特殊的扩充。其他的就没有什么特别的了。

     

      服务的实现,注意Service里定义的方法应该是对应于每一个业务场景的实现,而不只是简简单单的对数据库进行改变,那是Repository的职责。

     

      提供了一些具体查询的服务,利用了 Ardails.Specification 这个包,我们可以将对某些记录的筛选条件拓展于此,可以避免我们在Service,Reposity中编写过多根据不同参数获取实体的方法,如GetBasketItemWith....等等等,我们将我们自定义的筛选参数拓展后,如通过Name,BasketId,BuyerId等。

    public sealed class BasketWithItemsSpecification : Specification<Basket>
    {
        public BasketWithItemsSpecification(int basketId) 
        {
            Query
                .Where(b => b.Id == basketId)
                .Include(b => b.Items);
        }
    
        public BasketWithItemsSpecification(string buyerId)
        {
            Query
                .Where(b => b.BuyerId == buyerId)
                .Include(b => b.Items);
        }
    }

    在Services中,我们利用此对象构建我们的Query。

     我们来看看在Repository中是如何使用的:

     ApplySpecification方法,接受了我们的构造的查询对象,Query出了符合条件的数据并返回类型为IQueryable<Basket>的结果,我们可以根据直接返回该结果,或在对该结果进行Linq处理,这里是返回了集合的第一个元素。

    因此这种设计使得我们的业务查询更加灵活和易拓展,我们的Repository可以完全泛型化,只需要接受Spec对象即可,在Service层中,我们只用根据具体业务创建具体的查询对象,即可以正确的获取到我们想得到的数据。而复杂杂乱的Get...By...放在我们的Specification中即可。而过滤以及分页同样可使用此方式完成。

    这些即是ApplicationCore的大概内容。它的主要职责是business logic and domain model。且接口的设计中,将Infrastructure的实现的接口定义在ApplicationCore中,使得Infrastructure对ApplicationCore构建依赖并且‎我们手动构建依赖性方向,以便基础架构依赖于应用核心‎,这是符合DIP设计原则的。即high level modules should not depend on low level modules; both should depend on abstractions. Abstractions should not depend on details.  Details should depend upon abstractions。


    Infrastructure:

      基础设施层:主要集成功能有数据建设如(DbContext和Configurations),以及一些系统通用的底层方法等。

      

      Config存放了我们使用 EntityFrameWork Core 创建数据库时,创建表用的配置文件。这里我偏向于使用Config配置文件来创建数据库,而非Data Annotation。

    有以下几点原因:

    1. Config配置文件通常于DbContext归类于一个Folder,易于维护。而本项目中的Entity和DbContext甚至横跨Layer。
    2. 实现IEntityTypeConfiguration<T>接口的配置类有着更灵活强大的能力,可以便携的处理关系数据库中的关系,以及各个字段的约束等。
    3. 而Data Annotation 虽然也有着数据规范功能,如maxLength,Required等,但它更多是用于与客户端进行表单验证或数据交互等逻辑,如ViewModel或DTO等。
    4. Config配置文件使用简单:

      实现IEnittyTypeConfiguration接口:在Configure方法中使用EntityTypeBuilder

    public class BasketConfiguration : IEntityTypeConfiguration<Basket>
    {
        public void Configure(EntityTypeBuilder<Basket> builder)
        {        
            var navigation = builder.Metadata.FindNavigation(nameof(Basket.Items));
            navigation.SetPropertyAccessMode(PropertyAccessMode.Field);
    
            builder.Property(b => b.BuyerId)
                .IsRequired()
                .HasMaxLength(40);
        }
    }

      在DbContext中应用配置类:

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
        builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
    }

      除此之外,还有我们的数据库迁移文件,DbContext类,数据初始化种子,以及IAsyncRepository的实现EfRepository等。

      

       顾名思义,使用Asp.Net Core Identity 创建用户数据库,这里的ApplicationUser集成了IdentityUser,我们可以在此基础上拓展我们的用户属性,以及Token颁发服务。

      

       最后即是用于系统中多处会使用到的基础功能如Logger,WebFileSystem。


     Web:

      身份认证的部分功能,可以使用Asp.Net Core Identity 基架生成或自定义。

      

       在Startup.cs中,我们不需要将所有的服务配置都杂糅的放在一堆,这样可读性和可维护性就大大降低了。如下图,我们可以将一些配套的设置重新写在一个类中,这个类需要是 IServiceCollection 接口的拓展方法即可。比如我们在这里区分了Core层的服务注册和Web层的服务注册,提高了可维护性,我们在新增了服务时,只需区分的将服务新增进这两个方法即可完成注册。

      

      Controller: 分别为定义了基础的ApiController的基类,以及文件上传,用户信息管理,订单管理(MediatR通知),用户管理等。

       

       Extensions: Web 层对业务所需要的功能进行的拓展,如 CacheHelper 是用来处理与 Cache 相关的业务,这里还有 EmailSerder 和 Url 的拓展,在我们常用的开发中,如图片处理,短信验证,分页处理等都可以在此结构中进行拓展。

      

       Features: 包含了基于MediatR采用 通知订阅者模式 设计的获取用户订单,展示订单详情两个业务功能。

      

       HealthChecks: Asp.Net Core 自带的活性健康检查,这里对主页和Api的健康性进行了检测

      

       Interfaces: 这里对Web Layer中的业务模型定义接口,由于在Web中我们的Entity大多为ViewModel,这样命名也可以和Core中的接口区分开来。

      

       Services:对应接口的Services,其中CachedCatalog是将品牌,种类,以及首页的前十个商品写入MemoryCache加快页面加载。

      

       Pages:Admin是与Blazor assembly服务相同的Page页面,Shared中包括Components,以及分布页等。

      

       ViewModels, View:整合了Web所需要用到的VM,Views是对应Controller的视图。

      

       一些配置文件:如bundleconfig.json可在项目生成时,将我们的静态文件打包。

      

       SlugifyParameterTransformer为自定义的Rotue转换策略。(需实现IOutboundParameterTransformer接口)

    public class SlugifyParameterTransformer : IOutboundParameterTransformer
    {
        public string TransformOutbound(object value)
        {
            if (value == null) { return null; }
    
            // Slugify value
            return Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2").ToLower();
        }
    }

          

  • 相关阅读:
    Linux打包&压缩 tar,gzip,bzip2
    Linux递归计算目录md5
    fetion飞信登录异常,错误码10033201、10033202
    Linuxscp如何实现nohup &后台启动
    Linux显示日文4字节半角字符
    Linux目录配置的依据FHS
    Linux解决中文乱码问题: vim/pdf/gedit
    ery validator addMethod 方法的使用
    查询今天发帖量 sql
    JAVA反射机制
  • 原文地址:https://www.cnblogs.com/Xieyiincuit/p/14945605.html
Copyright © 2011-2022 走看看