初探领域驱动设计

概述

上一篇我们算是粗略的介绍了一下DDD,我们提到了实体、值类型和领域服务,也稍微讲到了DDD中的分层结构。但这只能算是一个很简单的介绍,并且我们在上篇的末尾还留下了一些问题,其中大家讨论比较多的,也是我本人之前有一些疑问的地方就是Repository。我之前觉得IRepository和三层里面的IDAL很像,为什么要整出这么个东西来;有人说用EF的话就不需要Repository了;IRepository是鸡肋等等。 我觉得这些问题都很好,我自己也觉得有问题,带着这些问题我们就来看一看Repository在DDD中到底起着一个什么样的角色,它为什么存在?有一句真理不是说“存在即合理”么?
那我们就要找到它存在的理由,去更好的理解它,或者说我们能不能针对不同的需求去改造它呢?注:本文讨论的是Repository在DDD中的应用,与EF该不该用Repoistory不是同一个话题。

EF与Repository

在上一篇《初探领域驱动设计(1)为复杂业务而生》中,我们已经实现了一个用户注册的例子,但是并不完整。我们还没有具体的实现Repository,即使是在测试的时候我们使用的也是一个Mock。那么今天,我们就来实现一个EntityFramework的Repository。有人说EF没有必要套一个Repository,我是同意的。但是不同的场景,不同的使用方法,我们下面再具体讲。我们在上一篇中已经提到了IRepository的接口定义,下面是我们的简单实现:

// EFRepository.cs

namespace RepositoryAndEf.Data
{
    public class EfRepository<T> : IRepository<T> where T : BaseEntity
    {
        private DbContext _context;

public EfRepository(DbContext context)

{

if (context == null)

{

throw new ArgumentNullException("context");

}

_context = context;

}

public T GetById(Guid id)

{

return _context.Set<T>().Find(id);

}

public bool Insert(T entity)

{

_context.Set<T>().Add(entity);

_context.SaveChanges();

return true;

}

public bool Update(T entity)

{

_context.Set<T>().Attach(entity);

_context.Entry<T>(entity).State = EntityState.Modified;

_context.SaveChanges();

return true;

}

public bool Delete(T entity)

{

_context.Set<T>().Remove(entity);

_context.SaveChanges();

return true;

}

public IEnumerable<T> Get(Expression<Func<T, bool>> predicate)

{

return _context.Set<T>().Where(predicate).ToList();

}

}

}

// 应用层UserService.cs

public class UserService : IUserService
{
    private IRepository _userRepository;

    public UserService(IRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public User Register(string email, string name, string password)
    {
        var domainUserService = new Domain.UserService(_userRepository);
        var user = domainUserService.Register(email, name, password);
        return user;
    }
}

// 领域层UserService.cs

namespace RepositoryAndEf.Domain
{
    public class UserService
    {
        private IRepository _userRepository;

        public UserService(IRepository userRepsoitory)
        {
            _userRepository = userRepsoitory;
        }

        public virtual User Register(string email, string name, string password)
        {
            if (_userRepository.Get().Any(u => u.Email == email))
            {
                throw new ArgumentException("The email is already taken");
            }

            var user = new User
            {
                Id = Guid.NewGuid(),
                Email = email,
                Name = name,
                Password = password
            };

            user.CreateShoppingCart();
            _userRepository.Insert(user);
            return user;
        }
    }
}

上面领域层UserService中的代码和我们上一篇中的代码是一样的,netfocus兄提出来一个问题“是不是把user对象加入到repository中就算完成注册了?” 现在看来,如果代码这样写,好像就已经完成了注册的功能。 但是如果真这样写,我又觉得问题更大,也就是为什么我会在上篇的未必留下那个问题,“Domain -> Repository -> Database” 和“BLL -> Dal -> Database” 有区别么?撇开这个问题不说,看看我们上面的EfRepository有没有什么问题?
好用么?现在好像没有办法使用事务啊!带着这个问题我们来看看Unit Of Work能怎么帮我们。

Unit Of Work 与 Repository

我们EfRepository的实现中,每一次Insert/Update/Delete操作被执行之后,变更就会立即同步到数据库中去。第一,我们没有为多个操作添加一个事务的能力;第二,这会为我们带来性能上的损失。而Unit Of Work模式正好解决了我们的问题,下面是Martin Fowler 对于该模式的解释:

“A Unit of Work keep track of everything you do during a business transaction that can affect the database. When you’re done, it figures out everything that need to be done to alter the database as a result of your work.”

Unit of Work负责跟踪所有业务事务过程中数据库的变更。当事务完成之后,它找出需要处理的变更,并更新数据库。

正如我们大家一直讨论的那样,在EF中,DBContext它本身就已经是一个Unit Of Work的模式,因为上面说的功能它都有。那我们有必要自己再给它包上一层吗?我的答案是肯定的,这个和我们为Repository建立接口是一样的,EF中的IDbSet就是一个Repository模式,但是他们都是EF里面的东西,如果哪天我们换成NHibernate了,我们不可能为了这一个接口和基类把EF这个dll也加进来是么? 我们要做的并不多,因为DbContext.SaveChanges它本身就是有事务的,所以我们只需要创建一个带有SaveChanges的接口就可以了。

// IUnitOfWork.cs

namespace RepositoryAndEf.Core.Data
{
    public interface IUnitOfWork : IDisposable
    {
        int SaveChanges();
    }
}

接着就是让我们的Context,继承DbContex和我们上面的接口。

namespace RepositoryAndEf.Data
{
    public class RepositoryAndEfContext : DbContext, IUnitOfWork
    {
        public RepositoryAndEfContext() { }

        public RepositoryAndEfContext(string nameOrConnectionString)
            : base(nameOrConnectionString)
        {
            Configuration.LazyLoadingEnabled = true;
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            var typesToRegister = Assembly.GetExecutingAssembly().GetTypes()
            .Where(type => !String.IsNullOrEmpty(type.Namespace))
            .Where(type => type.BaseType != null
                && type.BaseType.IsGenericType
                && type.BaseType.GetGenericTypeDefinition() == typeof(EntityTypeConfiguration<>));

            foreach (var type in typesToRegister)
            {
                dynamic configurationInstance = Activator.CreateInstance(type);
                modelBuilder.Configurations.Add(configurationInstance);
            }
            //...or do it manually below. For example,
            //modelBuilder.Configurations.Add(new LanguageMap());

            base.OnModelCreating(modelBuilder);
        }
    }
}

哦,对了,别忘了把Repository里面的SaveChanges方法去掉。

namespace RepositoryAndEf.Data
{
    public class EfRepository<T> : IRepository<T> where T : BaseEntity
    {
        private DbContext _context;

public EfRepository(IUnitOfWork uow)

{

if (uow == null)

{

throw new ArgumentNullException("uow");

}

_context = uow as DbContext;

}

public T GetById(Guid id)

{

return _context.Set<T>().Find(id);

}

public bool Insert(T entity)

{

_context.Set<T>().Add(entity);

return true;

}

public bool Update(T entity)

{

_context.Set<T>().Attach(entity);

_context.Entry<T>(entity).State = EntityState.Modified;

return true;

}

public bool Delete(T entity)

{

_context.Set<T>().Remove(entity);

return true;

}

public IEnumerable<T> Get(Expression<Func<T, bool>> predicate)

{

return _context.Set<T>().Where(predicate).ToList();

}

}

}

那么我们应用层的UserService就可以这样写了。

namespace RepositoryAndEf.Service
{
    public class UserService : IUserService
    {
        private IRepository _userRepository;
        private IUnitOfWork _uow =
            EngineContext.Current.Resolve();
        public UserService(IRepository userRepository)
        {
            _userRepository = userRepository;
        }

        public User Register(string email, string name, string password)
        {
            var domainUserService = new Domain.UserService(_userRepository);
            var user = domainUserService.Register(email, name, password);

            // 在调用SaveChnages()之前,做其它的更新操作
            // 它们会一起在同一个事务中执行。
            _uow.SaveChanges();
            return user;
        }
    }
}

如果光看这段代码有没有觉得很奇怪?没有任何对_userRepository的操作,就做了SaveChanges,因为我们在领域服务里面就已经把新创建的用户实体放到那个userRepository中去了。我想这个问题@田园的蟋蟀纠结过很久:) ,也就是领域服务那里面持有repository的引用,它可以自己将要更新的实体添加到repository中,但是如果对于一些不涉及到领域服务的操作,那这一点就需要在应用层来做了,比如添加商品到购物车的操作。

// 应用层ShoppingCartService.cs

namespace RepositoryAndEf.Service
{
    public class ShoppingCartService : IShoppingCartService
    {
        private IRepository _shoppingCartRepository;
        private IRepository _productRepository;
        private IUnitOfWork _uow;

        public ShoppingCartService(IUnitOfWork uow,
            IRepository shoppingCartRepository,
            IRepository productRepository)
        {
            _uow = uow;
            _shoppingCartRepository = shoppingCartRepository;
            _productRepository = productRepository;
        }

        public ShoppingCart AddToCart(Guid cartId,
            Guid productId,
            int quantity)
        {
            var cart = _shoppingCartRepository.GetById(cartId);
            var product = _productRepository.GetById(productId);
            cart.AddItem(product, quantity);

            _shoppingCartRepository.Update(cart);
            _uow.SaveChanges();
            return cart;
        }
    }
}

这就是属于职责定义不明确的问题,特别是上面注册用户的例子。应用层也有_userRepository,并且领域服务还给我返回了一个user的实体,那我是把它加到这个_userRepository中呢还是不加好呢?

我觉得我们应该有这样的一个定义,在领域层那里不使用repository的更新类操作(即Insert/Update/Delete),只使用查询类操作即(GetById,或者是Get)。把所有的更新类操作都放到应用层,这样由应用层去决定什么时候把实体更新到repository,以及什么时候去提交到数据库中。那我们就彻底与持久层,甚至领域实体生命期管理的功能撇开有关系了,从此用更OO的方式专注于业务。

后面我们要做的更改就是把_userRepository.Insert(user)从我们User的领域服务中移除掉,并且在应用层的Register方法中加入这句话。 我想到这里,也算是回答了我自己的问题: IRepository正如它的名字一样,它就像一个容器,允许我们把东西放进去或者取出来,它离真正的数据库还有一步之遥,并且通过Unit Of Work,把对事务以及持久化的控制都交到了外面。而不是像DAL那样直接就反映到数据库中去了。除此之外呢?IRepository解除了领域层对基础设施层的依懒,这个也是大家经常提到了Repository的优点之一。但是未必这一点一定非得需要IRepository,把IDAL接口移个位置同样也可以实现,不信您看看洋葱架构。

洋葱架构与IRepository

洋葱架构很早就有,只不过08年的时候Jeffery给它取了个名字,让它成为了一个模式。说起来好像很高大上,但是希望大家不要被这些名字所迷惑,所正如Jeffery所说,在这种设计有了一个名字之后,更方便大家去讨论和传播以及使用这种模式。 并且洋葱架构也是一种多层架构,所以会出现“传统” 的多层架构 和“现代”的多层架构。 我更是认为,所谓的洋葱架构只是作出了一点点思想层面上的转变,仅此而已。 究竟是哪一点思想上的转变,可以让它成为一种模式呢? 依懒关系!

Jeffery说在传统的多层架构中,上层对下层有着较强的依懒关系,UI没了BLL就没法工作,BLL少了DAL也无法正常运行。当然他说这句话的时候是08年,并且他的确是在前面加了“传统” 两个字。 我们很难找到到底是什么时候,这种传统的多层架构演变成了“现代” 的多层架构,但是我们能知道的是在08年7月以后我们对于多层架构又有了一个新的名词。即便如此,它的转变却是非常简单的 —— 也就是把IDAL接口从DAL层分离出去。

如果把IDAL接口定义在DataAccess层,第一是造成了BLL对DataAccess的依懒;第二是造成了IDAL的责任不明确。如果说小A负责开发BLL,小C负责开发DAL,他们是不是需要协调该怎么样去定义IDAL接口? 是DAL为BLL服务,还是BLL的最终目地是把自己移交给DAL? 在最开始的时候,大家对IDAL的定义是为了支持不同的访问层设计,大家想的都是现在我们用SQL,将来有可能会有MySql。所以IDAL放在哪里也就无所谓了,为了方便就直接和实现一起放在DAL吧。

把IDAL接口从DAL移出去之后会发生什么 ?

在把IDAL接口移到BLL层之后,箭头的方向就变了。现在一切都是以BLL为中心,BLL也不需要依懒于任何其它层了,作为独立的一块,我们可以更容易的进行单元测试,重构等。另外也明确了IDAL是为BLL服务的,也就是解决了我们上面提到的第二个问题。

这个一个很简单的转变就是洋葱架构的主要思想,如果你还不能很好的领悟洋葱架构和传统多层架构之间的区别,希望下面这张图能用最直接,最简单的方式告诉你。

传统多层架构与现代(洋葱架构)多层架构的区别

你要是愿意,把IDAL直接放到Bll里面也是可以的。当Jeffery给这种架构起名叫“洋葱架构”再往前推4年,DDD问世的时候已经包含了这种思想。IRepository属于领域层而非基础架构层中的数据访问模块,就直接避免了领域层对基础设施层的依懒,或者说不定这种思想也是从DDD引申出来的,所以你会发现很多人现在依然用DAL。但是并没有什么问题,因为在这种新的多层架构下,扩展性和可维护性同样也可以被保持的很好。

重新定义IRepository

现在,我们再回过头去看Repository。它的两大职责:

1.对领域实体的生命周期进行管理(从数据库重建,以及持久化到数据库) ——被推迟到了应用层

2.解除领域层对基础设施的依懒

在第一点生效后,所有更新类的操作都推迟到应用层去执行。那IRepository中的那些更新类方法放在领域层是不是就多余了呢? 毕竟我们现在只需要用到查询的功能。我们可以单独建一个IQuery的接口给领域层使用。

// IQuery.cs

namespace RepositoryAndEf.Core.Data
{
    public interface IQuery
    {
        T GetById(Guid id);
        IQueryable Table { get; }
    }
}

// IRepository.cs

namespace RepositoryAndEf.Core.Data
{
    public partial interface IRepository:
        IQuery where T : BaseEntity
    {
        bool Insert(T entity);
        bool Update(T entity);
        bool Delete(T entity);
    }
}

我们直接让IRepository继承了IQuery,IQuery就相当于IRepository的一个功能子集,只提供读的功能。 而在EfRepository中,我们只要暴露DbSet<T>.AsQueryAble()就可以了。

// EfRepository IQuery的实体部分

public T GetById(Guid id)
{
    return _context.Set().Find(id);
}

public IQueryable Table
{
    get
    {
        return _context.Set().AsQueryable();
    }
}

// 领域层 UserService.cs

namespace RepositoryAndEf.Domain
{
    public class UserService
    {
        private IQuery _userQuery;

        public UserService(IQuery userQuery)
        {
            _userQuery = userQuery;
        }

        public virtual User Register(string email, string name, string password)
        {
            if (_userQuery.Table.Any(u => u.Email == email))
            {
                throw new ArgumentException("The email is already taken");
            }

            var user = new User
            {
                Id = Guid.NewGuid(),
                Email = email,
                Name = name,
                Password = password
            };

            user.CreateShoppingCart();
            return user;
        }
    }
}

// 客户端调用应用层Service代码

namespace RepositoryAndEf.Domain
{
    public class UserService
    {
        private IQuery _userQuery;

        public UserService(IQuery userQuery)
        {
            _userQuery = userQuery;
        }

        public virtual User Register(string email, string name, string password)
        {
            if (_userQuery.Table.Any(u => u.Email == email))
            {
                throw new ArgumentException("The email is already taken");
            }

            var user = new User
            {
                Id = Guid.NewGuid(),
                Email = email,
                Name = name,
                Password = password
            };

            user.CreateShoppingCart();
            return user;
        }
    }
}

现在,恐怕你再想在领域模型里面去使用Repository的更新类操作也不行了吧。 Table作为IQueryable返回,那我们想怎么查就随意了。因为是IQueryable,所以也是只会返回我们所查询的内容,和直接用EF查询是一个道理。下面是我们_userQuery.Table.Any()所生成的SQL语句。

var uow = new RepositoryAndEfContext("ConnStr");
var userRepository = new EfRepository(uow);
var userService = new UserService(uow, userRepository);
var newUser = userService.Register(
    "[email protected]",
    "Jesse Liu",
    "jesseliu");

可有可无的Repository

我们把IRepository移出领域层之后,再加上我们对洋葱架构的理解。我们就可以知道Repository在应用层已经可以被替换成别的东西,IDAL也可以啊:)。当然有人也许会建议直接拿EF来用多好,其实我不建议这样去做,考虑到以后把EF换掉的可能性。并且我们加这样一个接口真的不会碍着我们什么事。如果有人觉得在读取数据的时候加一个Repository在中间,少掉了很多EF提供的功能,觉得很不爽,倒是可以试试像我们的IQuery接口一样直接对DbSet来查询。我们甚至可以学习CQRS架构,将“读”的服务完全分离开,我们就可以单独针对“读”来独立设计。

但是Repository给我们带来的优点,这些优点也是我们不能轻易丢掉它的原因:

1.提供一个简单的模型,来获取持久对象并管理期生命周期

2.把应用和领域设计从持久技术、多种数据库策略解耦出来

3.容易被替换成哑实现(Mock)以便我们在测试中使用

如果你的项目属于短期的项目,或者说你不用考虑更换数据访问层,那么你就可以忽略第一和第二个优点。而第三个优点,借助于一些测试框架我们也可以实现,所以如果你不想用Repository,那就不用,前提条件是你所做的项目允许你这样做,并且你也能够找到好的替代方案来弥补Repository的优势。比如说对洋葱架构中的IDAL再进行一些改造等等。关于更多单元测试的话题,我们将在下一篇中一起来探讨。如果大家对Repository有什么其它的看法,也欢迎一起参与讨论。

时间: 2024-08-06 07:57:58

初探领域驱动设计的相关文章

初探领域驱动设计(1)为复杂业务而生

概述 领域驱动设计也就是3D(Domain-Driven Design)已经有了10年的历史,我相信很多人或多或都都听说过这个名词,但是有多少人真正懂得如何去运用它,或者把它运用好呢?于是有人说,DDD和TDD这些玩意是一些形而上的东西,只是一茶余饭后的谈资,又或是放到简历上提升逼格而已.前面这句话我写完之后犹豫了,犹豫要不要把它删掉,因为它让我看起来像个喷子,我确实感到不解,为什么别人10年前创造总结出来的东西,我们在10年之后对它的理解还处于这么低的一个层次.开篇就说远了,我也是最近才开始认

初探领域驱动设计(2)Repository在DDD中的应用

概述 上一篇我们算是粗略的介绍了一下DDD,我们提到了实体.值类型和领域服务,也稍微讲到了DDD中的分层结构.但这只能算是一个很简单的介绍,并且我们在上篇的末尾还留下了一些问题,其中大家讨论比较多的,也是我本人之前有一些疑问的地方就是Repository.我之前觉得IRepository和三层里面的IDAL很像,为什么要整出这么个东西来:有人说用EF的话就不需要Repository了:IRepository是鸡肋等等. 我觉得这些问题都很好,我自己也觉得有问题,带着这些问题我们就来看一看Repo

【无私分享:ASP.NET CORE 项目实战(第三章)】EntityFramework下领域驱动设计的应用

目录索引 [无私分享:ASP.NET CORE 项目实战]目录索引 简介 在我们 [无私分享:从入门到精通ASP.NET MVC] 系列中,我们其实也是有DDD思想的,但是没有完全的去实现,因为并不是所有的好的东西都必须要用到的,还是根据实际情况,DDD在大型的系统中是非常好的一种设计思想,这点不否认.但是根据具体情况而言,在我们小型的项目中,我们设计框架的更多考虑的是让使用者快速.便捷的开发,能快速的了解框架进行项目开发. 重构我们的思路 最近研究了一下几位大神的博客,特别是:@腾飞(Jess

C#进阶系列——DDD领域驱动设计初探(六):领域服务

前言:之前一直在搭建项目架构的代码,有点偏离我们的主题(DDD)了,这篇我们继续来聊聊DDD里面另一个比较重要的知识点:领域服务.关于领域服务的使用,书中也介绍得比较晦涩,在此就根据博主自己的理解谈谈这个知识点的使用. DDD领域驱动设计初探系列文章: C#进阶系列——DDD领域驱动设计初探(一):聚合 C#进阶系列——DDD领域驱动设计初探(二):仓储Repository(上) C#进阶系列——DDD领域驱动设计初探(三):仓储Repository(下) C#进阶系列——DDD领域驱动设计初探

C#进阶系列——DDD领域驱动设计初探(二):仓储Repository(上)

前言:上篇介绍了DDD设计Demo里面的聚合划分以及实体和聚合根的设计,这章继续来说说DDD里面最具争议的话题之一的仓储Repository,为什么Repository会有这么大的争议,博主认为主要原因无非以下两点:一是Repository的真实意图没有理解清楚,导致设计的紊乱,随着项目的横向和纵向扩展,到最后越来越难维护:二是赶时髦的为了“模式”而“模式”,仓储并非适用于所有项目,这就像没有任何一种架构能解决所有的设计难题一样.本篇通过这个设计的Demo来谈谈博主对仓储的理解,有不对的地方还望

DDD领域驱动设计初探(一):聚合

前言:又有差不多半个月没写点什么了,感觉这样很对不起自己似的.今天看到一篇博文里面写道:越是忙人越有时间写博客.呵呵,似乎有点道理,博主为了证明自己也是忙人,这不就来学习下DDD这么一个听上去高大上的东西.前面介绍了下MEF和AOP的相关知识,后面打算分享Automapper.仓储模式.WCF等东西的,可是每次准备动手写点什么的时候,就被要写的Demo难住了,比如仓储模式,使用过它的朋友应该知道,如果你的项目不是按照DDD的架构而引入仓储的设计,那么会让它变得很“鸡肋”,用不好就会十分痛苦,之前

C#进阶系列——DDD领域驱动设计初探(一)

前言:又有差不多半个月没写点什么了,感觉这样很对不起自己似的.今天看到一篇博文里面写道:越是忙人越有时间写博客.呵呵,似乎有点道理,博主为了证明自己也是忙人,这不就来学习下DDD这么一个听上去高大上的东西.前面介绍了下MEF和AOP的相关知识,后面打算分享Automapper.仓储模式.WCF等东西的,可是每次准备动手写点什么的时候,就被要写的Demo难住了,比如仓储模式,使用过它的朋友应该知道,如果你的项目不是按照DDD的架构而引入仓储的设计,那么会让它变得很“鸡肋”,用不好就会十分痛苦,之前

DDD「领域驱动设计」分层架构初探

前言 基于 DDD 传统分层架构实现. 项目 github地址:https://github.com/WuMortal/DDDSample 这个分层架构是工作中项目正在使用的分层架构,使用了一段时间发现受益匪浅,所以整理好我对该分层架构的一些理解分享给大家,我对于该分层架构还处于学习阶段理解有误的地方请指出.本次会以一个案例来说明各个分层的作用以及他们之间的调用关系,还有本次的重点不在于DDD,因为这个我还未能完全理解,当然避免不了中间会涉及DDD的一些概念. DDD 简单介绍 DDD 什么?为

DDD领域驱动设计仓储Repository

DDD领域驱动设计初探(二):仓储Repository(上) 前言:上篇介绍了DDD设计Demo里面的聚合划分以及实体和聚合根的设计,这章继续来说说DDD里面最具争议的话题之一的仓储Repository,为什么Repository会有这么大的争议,博主认为主要原因无非以下两点:一是Repository的真实意图没有理解清楚,导致设计的紊乱,随着项目的横向和纵向扩展,到最后越来越难维护:二是赶时髦的为了“模式”而“模式”,仓储并非适用于所有项目,这就像没有任何一种架构能解决所有的设计难题一样.本篇