记录一次使用UnifOfWork改造项目的过程。

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了记录一次使用UnifOfWork改造项目的过程。相关的知识,希望对你有一定的参考价值。

一、前言

  UnifOfWork模式,一般称为“工作单元”模式,是DDD(Domain-Driven Design,领域驱动设计)的一个组成部分,用于在应用服务方法中,控制一个应用服务方法的所有数据库操作,都在一个事务内。最近对个人的一个Web项目进行了改造,按照DDD的设计模式进行了分层,虽然在领域层没有实现完整的领域对象(实体+值对象+领域服务 ),但是DDD也并不是铁板一块,架构设计指南最终也是要服务于具体项目的,领悟思想为我所用才是一个提高的过程,记录下来,以备日后温习。

 

二、DDD模式分析

技术分享

 

  

  经典的DDD架构,由如图四层组成,第一层是用户层,是直接面向用户的UI界面层,放到开发环境中,可以指代比如浏览器前端、移动端APP、Windows客户端等。第二层是指应用服务层,这层直接面向的是现实的需求,比如某公司的“新员工入职”,这一层根据DDD理论,应该是很薄的一层,主要是组合领域层中的不同对象,来实现最终的现实需求。本文主要实现的UnitOfWork也是在这一层。

  第三层领域层,这是整个系统中最核心的一层,表达了业务的逻辑,按照通用语言、和使用通用语言对于业务范围进行限界上下文的划分,划分出一块一块的领域出来,并使用“实体对象、值对象、Repository、领域服务”等概念进行组织。其中Repository应该是抽象的接口,为了解耦合具体的ORM,Repository模式应该是在领域层定义接口、在第四层基础设施层实现(以备日后可能的进行更换ORM行为)。

  第四层基础设施层,是项目中一些具体底层操作的实现,比如领域层的Repository的实现,文件的保存,邮件的发送,等等。

  领域驱动设计是一门精深的学问,以上只简单介绍,如果有兴趣,可以去看看这两本书: 《领域驱动设计 软件核心复杂性应对之道》《实现领域驱动设计》。

 

三、项目分层简介

 个人的项目规模也不大,但是会有“在同一次Http请求内同一事务内组合业务逻辑”的需求,结合ASP.NET的已有功能,分层如下:

  用户界面层:Web SPA(Angular、React等,前端分离另行开发,故VS中不会有相关的项目)

  应用服务层:ASP.NET WebAPI

  领域层:一个单独的类库项目,定义Model(贫血模型,只包含属性)、服务的接口IXXXService以及其实现XXXService,仓储接口IRepository。

  基础设施层:一个单独的类库项目:实现了领域层中定义的IRepository,目前已有了EF以及Dapper的两种实现。

 

  项目引用(依赖)顺序:最顶层是领域层,无任何依赖;应用访问层(WebAPI)依赖领域层以及基础设施层。

  在领域层中,我定义如下两种基础接口,其中IUnitOfWork接口包含一个虚拟事务对象、以及一个Commit()方法。IRepository对象用于实现对于每一个实体(model或者叫entity都可以)的持久化操作,处于简单期间并没有加入比如分页查询方法等。

    public interface IUnitOfWork
    {
        /// <summary>
        /// 事务对象,Dapper为IDbTransaction,EF为DbContext
        /// </summary>
        object VirtualTransaction { get; }

        void Commit();
    }

    public interface IRepository<T> where T:class
    {
        int Insert(T t);

        T Get(string id);

        int Update(T t);

        int Delete(T t);       
    }

  因为ASP.NET Core自带了一个方便的IOC容器,可以实现Transient(每次使用都创建新的对象)、Scope(每个Http请求内只创建唯一的对象)、Singletion(单例)三种生命周期的依赖注入,结合ASP.NET的工作原理:对于每一个Http请求,都会实例化一个Controller对其处理,因此我们可以进行如下设计:

  在ASP.NET Core的StartUp中,将IUnitOfWork与其实现类UnitOfwork,进行Scope方式注入。

  这样,每次Http请求中,因为自始至终只会有一个UnitOfWork对象,这个对象里面保存着一个用于事务的对象(EF是DbContext,Dapper是IDBTransaction),在Action结束的时候,调用UnitOfWork的Commit()方法,进行提交,即实现了每个Controller中只会有一个事务。

  //UnitOfWork会被注入到Repo对象中,Repo对象会被注入到Service对象中,Service对象会被注入到Controller对象中

  services.AddScoped<IUnitOfWork,UnitOfWork>();

  services.AddScoped<IUserRepository, UserRepository>();
  services.AddScoped<IBookRepository, BookRepository>();

  services.AddScoped<IAccountService, AccountService>();
  services.AddScoped<IPublishService, PublishService>();

再贴一下我的Controller的示意代码

    [Route("api/[controller]")]
    public class BusinessController : Controller
    {
        private readonly IAccountService _accountService;
        private readonly IPublishService _publishService;
        private readonly IUnitOfWork _unitOfWork;

        public BusinessController( IPublishService publishService, IAccountService accountService, IUnitOfWork unitOfWork)
        {
            
            _accountService = accountService;
            _publishService = publishService;
            _unitOfWork = unitOfWork;
        }

         //这里写具体的WebAPI的Action方法,方法中可以调用各种Service中的业务逻辑方法,然后在方法的末尾,加上_unitOfWork.Commit()即可
       
    }

  WebAPI作为整个项目的应用服务层、自身没有任何业务逻辑,而是组合业务逻辑层中的各种Service,完成具体的现实的用户使用需求。

  比如一个现实需求是“一个作家,注册成为了作协会员,并登记了他的出版作品”。这个需求可以拆解为两个业务逻辑:1.注册 2.登记出版物 。我们在领域层中实现了这两个业务逻辑后,就可以在应用访问层(我们的Controller中)进行组合、并在同一个事务过程中控制他们了。有人可能会问:你这么做,有什么必要呢,我直接三层架构那样,Controller调用业务逻辑层BLL中的一个“注册并登记出版物”方法,然后这个BLL方法直接去调用DAL中各种数据库操作方法,不也可以实现你所说的吗?

  好,这时候,应用服务层+UnitOfWork模式的优点就可以体现出来了。如果是按照传统三层那样,一个BLL方法“注册并登记出版物”,他就只能用于“注册并登记出版物”了,无法实现业务逻辑的组合复用。而使用DDD提倡的编程模式,就可以实现多种业务逻辑组合复用,比如我前面距离的“注册”“登记出版物”这两个领域服务方法,同时还可以组合其他业务逻辑实现更多不同的现实需求中,比如“注册”+“缴费”、“登记出版物”+“领取津贴”,这里每一个需求都是在同一个事务内的,也可以保证数据的一致性。

  贴一个IService方法和Service,都在领域层中

    public interface IPublishService
    {
        void PublishNewBook(Book book);
    }

    public class PublishService : IPublishService
    {
        private readonly IBookRepository _bookRepository;

        public PublishService(IBookRepository bookRepository)
        {
            _bookRepository = bookRepository;
        }

        public void PublishNewBook(Book book)
        {
            _bookRepository.Insert(book);
        }
    }

贴一个IRepository(领域层中)和它的对应实现(Repository)

  

    //领域层中的Book仓储接口,这里简单起见,没有使用更多的接口方法
    public interface IBookRepository:IRepository<Book>
    {
        
    }

    //基础设施层中使用Dapper操作数据库的实现
    public abstract class BaseRepository
    {

        private readonly IDbTransaction _dbTransaction;

        protected internal IDbConnection DbConnection { get; }


        protected BaseRepository(IUnitOfWork unitOfWork)
        {
            _dbTransaction = (unitOfWork.VirtualTransaction) as IDbTransaction;

            DbConnection = _dbTransaction.Connection;
        }

        public CommandDefinition GenCmd(string cmdText, object paramObj)
        {
            CommandDefinition cmd = new CommandDefinition(cmdText, paramObj, _dbTransaction);

            return cmd;
        }
    }

    public class BookRepository :BaseRepository, IBookRepository
    {
        

        public BookRepository(IUnitOfWork unitOfWork):base(unitOfWork)
        {
            
        }

        //这里只写了Insert方法,其他的可类比
        public int Insert(Book t)
        {
            string cmdText = "INSERT INTO Book (Id,BookName,Author,Price,PublishTime) VALUES (@Id,@BookName,@Author,@Price,@PublishTime)";

            var cmd = GenCmd(cmdText, t);

      
var result = DbConnection.Execute(cmd); return result; } } //另外一个基础设施层项目中,使用EF操作数据库的实现 public abstract class BaseRepository { protected internal DbContext DbContext { get; } protected BaseRepository(IUnitOfWork unitOfWork) { DbContext = unitOfWork.VirtualTransaction as DbContext; } } public class BookRepository : BaseRepository ,IBookRepository { public BookRepository(IUnitOfWork unitOfWork):base(unitOfWork) { } //这里只写了Inert方法,其他的可以类比实现 public int Insert(Book t) { DbContext.Book.Add(t);
       //这里不能写DbContext.SaveChanges(),因为EF的SaveChanges()是一个对所有更改的事务性提交,而根据我们的设计事务不在此处提交,而是在应用服务层的Controller中
return 1; } }

然后,我们就可以在Controller中组合不同Service提供的业务逻辑了,并且可以在同一个事务内,有效的提高了业务逻辑层的复用程度同时保证了同一个Http请求内数据的事务一致性。









以上是关于记录一次使用UnifOfWork改造项目的过程。的主要内容,如果未能解决你的问题,请参考以下文章

一次sendmsg的改造过程

一个项目的SpringCloud微服务改造过程

一个项目的SpringCloud微服务改造过程

记一次改造react脚手架的过程

8人/天,小记一次 JAVA(APP后台) 项目改造 .NET 过程(后台代码已完整开源于 Github)

主机Redis服务迁移到现有Docker Overlay环境