一、前言
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 { ////// 事务对象,Dapper为IDbTransaction,EF为DbContext /// object VirtualTransaction { get; } void Commit(); } public interface IRepositorywhere 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{ } //基础设施层中使用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请求内数据的事务一致性。