上一篇:《DDD 领域驱动设计-领域模型中的用户设计?》
开源地址:https://github.com/yuezhongxin/CNBlogs.Apply.Sample(代码已更新)
在之前的项目开发中,只有一个 JsPermissionApply 实体(JS 权限申请),所以,CNBlogs.Apply.Domain 设计的有些不全面,或者称之为不完善,因为在一些简单的项目开发中,一般只会存在一个实体,单个实体的设计,我们可能会忽略很多的东西,从而以后会导致一些问题的产生,那如果再增加一个实体,CNBlogs.Apply.Domain 该如何设计呢?
按照实际项目开发需要,CNBlogs.Apply.Domain 需要增加一个 BlogChangeApply 实体(博客地址更改申请)。
在 BlogChangeApply 实体设计之前,我们按照之前 JsPermissionApply 实体设计过程,先大致画一下流程图:

流程图很简单,并且和之前的 JS 权限申请和审核很相似,我们再看一下之前的 JsPermissionApply 实体设计代码:
根据博客地址更改申请和审核的流程图,然后再结合上面 JsPermissionApply 实体代码,我们就可以幻想出 BlogChangeApply 的实体代码,具体是怎样的了,如果你实现一下,会发现和上面的代码简直一摸一样,区别就在于多了一个 TargetBlogApp(目标博客地址),然后后面的 Repository 和 Application.Services 复制粘贴就行了,没有任何的难度,这样设计实现也没什么问题,但是项目中的重复代码简直太多了,领域驱动设计慢慢就变成了一个脚手架,没有任何的一点用处。
该如何解决上面的问题呢?我们需要思考下 CNBlogs.Apply.Domain 所包含的含义,CNBlogs.Apply.Domain 顾名思议是申请领域,并不是 CNBlogs.JsPermissionApply.Domain,也不是 CNBlogs.BlogChangeApply.Domain,实体的产生是根据聚合根的设计,那 CNBlogs.Apply.Domain 的聚合根是什么呢?在之前的设计中只有 IAggregateRoot 和 IEntity,具体代码:
现在再来看上面这种设计,完全是错误的,聚合根接口怎么能继承实体接口呢,还有一个问题,就是如果有多个实体设计,是继承 IAggregateRoot?还是 IEntity?IEntity 在这样的设计中,没有任何的作用,并且闲的很多余,IAggregateRoot 到最后也只是一个抽象的接口,CNBlogs.Apply.Domain 中并没有具体的实现。
解决上面混乱的问题,就是抽离出 ApplyAggregateRoot(申请聚合根),然后 JsPermissionApply 和 BlogChangeApply 实体都是由它进行产生,在这之前,我们先定义一下 IAggregateRoot:
然后根据 JS 权限申请/审核和博客地址更改申请/审核的流程图,抽离出 ApplyAggregateRoot,并且继承自 IAggregateRoot,具体实现代码:
ApplyAggregateRoot 的实现,基本上是抽离出 JsPermissionApply 和 BlogChangeApply 实体产生的重复代码,比如不管什么类型的申请,都包含申请理由、申请人信息、通过或拒绝等操作,这些也就是 ApplyAggregateRoot 所体现的领域含义,我们再来看下 BlogChangeApply 实体的实现代码:
BlogChangeApply 继承自 ApplyAggregateRoot,并且单独的 TargetBlogApp 操作,其他一些实现都是基本的参数传递操作,没有具体实现,JsPermissionApply 的实体代码就不贴了,和 BlogChangeApply 比较类似,只不过有一些不同的业务实现。
CNBlogs.Apply.Domain 改造之后,还要对应改造下 Repository,之前的代码大家可以看下 Github,这边我简单说下改造的过程,首先 IRepository 的设计不变:
IRepository 对应 BaseRepository 实现,它的作用就是抽离出所有聚合根的 Repository 操作,并不单独包含 ApplyAggregateRoot,所以,我们还需要一个对 ApplyAggregateRoot 操作的 Repository 实现,定义如下:
大家如果熟悉之前代码的话,会发现 IApplyRepository 的定义和 IJsPermissionApplyRepository 的定义是一摸一样的,设计 IApplyRepository 的好处就是,对于申请实体的相同操作,我们就不需要再写重复代码了,比如 IJsPermissionApplyRepository 和 IBlogChangeApplyRepository 的定义:
当然,除了上面的代码改造,还有一些其他功能的添加,比如 ApplyAuthenticationService 领域服务增加了 VerfiyForBlogChange 等等,具体的一些改变,大家可以查看提交。
CNBlogs.Apply.Sample 开发进行到这,对于现阶段的我来说,应用领域驱动设计我是比较满意的,虽然还有一些不完善的地方,但至少除了 CNBlogs.Apply.Domain,在其他项目中是看不到业务实现代码的,如果业务需求发生变化,首先更改的是 CNBlogs.Apply.Domain,而不是不是其它项目,这是一个基本点。
先设计 CNBlogs.Apply.Domain 和 CNBlogs.Apply.Domain.Tests,就能完成整个的业务系统设计,其它都是一些技术实现或工作流程实现,这个路子我觉得是正确的,以后边做边完善并学习。
开源地址:https://github.com/yuezhongxin/CNBlogs.Apply.Sample(代码已更新)
在之前的项目开发中,只有一个 JsPermissionApply 实体(JS 权限申请),所以,CNBlogs.Apply.Domain 设计的有些不全面,或者称之为不完善,因为在一些简单的项目开发中,一般只会存在一个实体,单个实体的设计,我们可能会忽略很多的东西,从而以后会导致一些问题的产生,那如果再增加一个实体,CNBlogs.Apply.Domain 该如何设计呢?
按照实际项目开发需要,CNBlogs.Apply.Domain 需要增加一个 BlogChangeApply 实体(博客地址更改申请)。
在 BlogChangeApply 实体设计之前,我们按照之前 JsPermissionApply 实体设计过程,先大致画一下流程图:

流程图很简单,并且和之前的 JS 权限申请和审核很相似,我们再看一下之前的 JsPermissionApply 实体设计代码:
namespace CNBlogs.Apply.Domain { public class JsPermissionApply : IAggregateRoot { private IEventBus eventBus; public JsPermissionApply() { } public JsPermissionApply(string reason, User user, string ip) { if (string.IsNullOrEmpty(reason)) { throw new ArgumentException("申请内容不能为空"); } if (reason.Length > 3000) { throw new ArgumentException("申请内容超出最大长度"); } if (user == null) { throw new ArgumentException("用户为null"); } if (user.Id == 0) { throw new ArgumentException("用户Id为0"); } this.Reason = HttpUtility.HtmlEncode(reason); this.User = user; this.Ip = ip; this.Status = Status.Wait; } public int Id { get; private set; } public string Reason { get; private set; } public virtual User User { get; private set; } public Status Status { get; private set; } = Status.Wait; public string Ip { get; private set; } public DateTime ApplyTime { get; private set; } = DateTime.Now; public string ReplyContent { get; private set; } public DateTime? ApprovedTime { get; private set; } public bool IsActive { get; private set; } = true; public async Task<Status> GetStatus(string userAlias) { if (await BlogService.HaveJsPermission(userAlias)) { return Status.Pass; } else { if (this.Status == Status.Deny && DateTime.Now > this.ApplyTime.AddDays(3)) { return Status.None; } if (this.Status == Status.Pass) { return Status.None; } return this.Status; } } public async Task<bool> Pass() { if (this.Status != Status.Wait) { return false; } this.Status = Status.Pass; this.ApprovedTime = DateTime.Now; this.ReplyContent = "恭喜您!您的JS权限申请已通过审批。"; eventBus = IocContainer.Default.Resolve<IEventBus>(); await eventBus.Publish(new JsPermissionOpenedEvent() { UserAlias = this.User.Alias }); return true; } public bool Deny(string replyContent) { if (this.Status != Status.Wait) { return false; } this.Status = Status.Deny; this.ApprovedTime = DateTime.Now; this.ReplyContent = replyContent; return true; } public bool Lock() { if (this.Status != Status.Wait) { return false; } this.Status = Status.Lock; this.ApprovedTime = DateTime.Now; this.ReplyContent = "抱歉!您的JS权限申请没有被批准,并且申请已被锁定,具体请联系contact@cnblogs.com。"; return true; } public async Task Passed() { if (this.Status != Status.Pass) { return; } eventBus = IocContainer.Default.Resolve<IEventBus>(); await eventBus.Publish(new MessageSentEvent() { Title = "您的JS权限申请已批准", Content = this.ReplyContent, RecipientId = this.User.Id }); } public async Task Denied() { if (this.Status != Status.Deny) { return; } eventBus = IocContainer.Default.Resolve<IEventBus>(); await eventBus.Publish(new MessageSentEvent() { Title = "您的JS权限申请未通过审批", Content = this.ReplyContent, RecipientId = this.User.Id }); } public async Task Locked() { if (this.Status != Status.Lock) { return; } eventBus = IocContainer.Default.Resolve<IEventBus>(); await eventBus.Publish(new MessageSentEvent() { Title = "您的JS权限申请未通过审批", Content = this.ReplyContent, RecipientId = this.User.Id }); } } }
根据博客地址更改申请和审核的流程图,然后再结合上面 JsPermissionApply 实体代码,我们就可以幻想出 BlogChangeApply 的实体代码,具体是怎样的了,如果你实现一下,会发现和上面的代码简直一摸一样,区别就在于多了一个 TargetBlogApp(目标博客地址),然后后面的 Repository 和 Application.Services 复制粘贴就行了,没有任何的难度,这样设计实现也没什么问题,但是项目中的重复代码简直太多了,领域驱动设计慢慢就变成了一个脚手架,没有任何的一点用处。
该如何解决上面的问题呢?我们需要思考下 CNBlogs.Apply.Domain 所包含的含义,CNBlogs.Apply.Domain 顾名思议是申请领域,并不是 CNBlogs.JsPermissionApply.Domain,也不是 CNBlogs.BlogChangeApply.Domain,实体的产生是根据聚合根的设计,那 CNBlogs.Apply.Domain 的聚合根是什么呢?在之前的设计中只有 IAggregateRoot 和 IEntity,具体代码:
namespace CNBlogs.Apply.Domain { public interface IAggregateRoot : IEntity { } } namespace CNBlogs.Apply.Domain { public interface IEntity { int Id { get; } } }
现在再来看上面这种设计,完全是错误的,聚合根接口怎么能继承实体接口呢,还有一个问题,就是如果有多个实体设计,是继承 IAggregateRoot?还是 IEntity?IEntity 在这样的设计中,没有任何的作用,并且闲的很多余,IAggregateRoot 到最后也只是一个抽象的接口,CNBlogs.Apply.Domain 中并没有具体的实现。
解决上面混乱的问题,就是抽离出 ApplyAggregateRoot(申请聚合根),然后 JsPermissionApply 和 BlogChangeApply 实体都是由它进行产生,在这之前,我们先定义一下 IAggregateRoot:
namespace CNBlogs.Apply.Domain { public interface IAggregateRoot { int Id { get; } } }
然后根据 JS 权限申请/审核和博客地址更改申请/审核的流程图,抽离出 ApplyAggregateRoot,并且继承自 IAggregateRoot,具体实现代码:
namespace CNBlogs.Apply.Domain { public class ApplyAggregateRoot : IAggregateRoot { private IEventBus eventBus; public ApplyAggregateRoot() { } public ApplyAggregateRoot(string reason, User user, string ip) { if (string.IsNullOrEmpty(reason)) { throw new ArgumentException("申请内容不能为空"); } if (reason.Length > 3000) { throw new ArgumentException("申请内容超出最大长度"); } if (user == null) { throw new ArgumentException("用户为null"); } if (user.Id == 0) { throw new ArgumentException("用户Id为0"); } this.Reason = HttpUtility.HtmlEncode(reason); this.User = user; this.Ip = ip; this.Status = Status.Wait; } public int Id { get; protected set; } public string Reason { get; protected set; } public virtual User User { get; protected set; } public Status Status { get; protected set; } = Status.Wait; public string Ip { get; protected set; } public DateTime ApplyTime { get; protected set; } = DateTime.Now; public string ReplyContent { get; protected set; } public DateTime? ApprovedTime { get; protected set; } public bool IsActive { get; protected set; } = true; protected async Task<bool> Pass<TEvent>(string replyContent, TEvent @event) where TEvent : IEvent { if (this.Status != Status.Wait) { return false; } this.Status = Status.Pass; this.ApprovedTime = DateTime.Now; this.ReplyContent = replyContent; eventBus = IocContainer.Default.Resolve<IEventBus>(); await eventBus.Publish(@event); return true; } public bool Deny(string replyContent) { if (this.Status != Status.Wait) { return false; } this.Status = Status.Deny; this.ApprovedTime = DateTime.Now; this.ReplyContent = replyContent; return true; } protected bool Lock(string replyContent) { if (this.Status != Status.Wait) { return false; } this.Status = Status.Lock; this.ApprovedTime = DateTime.Now; this.ReplyContent = replyContent; return true; } protected async Task Passed(string title) { if (this.Status != Status.Pass) { return; } await SendMessage(title); } protected async Task Denied(string title) { if (this.Status != Status.Deny) { return; } await SendMessage(title); } protected async Task Locked(string title) { if (this.Status != Status.Lock) { return; } await SendMessage(title); } private async Task SendMessage(string title) { eventBus = IocContainer.Default.Resolve<IEventBus>(); await eventBus.Publish(new MessageSentEvent() { Title = title, Content = this.ReplyContent, RecipientId = this.User.Id }); } } }
ApplyAggregateRoot 的实现,基本上是抽离出 JsPermissionApply 和 BlogChangeApply 实体产生的重复代码,比如不管什么类型的申请,都包含申请理由、申请人信息、通过或拒绝等操作,这些也就是 ApplyAggregateRoot 所体现的领域含义,我们再来看下 BlogChangeApply 实体的实现代码:
namespace CNBlogs.Apply.Domain { public class BlogChangeApply : ApplyAggregateRoot { public BlogChangeApply() { } public BlogChangeApply(string targetBlogApp, string reason, User user, string ip) : base(reason, user, ip) { if (string.IsNullOrEmpty(targetBlogApp)) { throw new ArgumentException("博客地址不能为空"); } targetBlogApp = targetBlogApp.Trim(); if (targetBlogApp.Length < 4) { throw new ArgumentException("博客地址至少4个字符!"); } if (!Regex.IsMatch(targetBlogApp, @"^([0-9a-zA-Z_-])+$")) { throw new ArgumentException("博客地址只能使用英文、数字、-连字符、_下划线!"); } this.TargetBlogApp = targetBlogApp; } public string TargetBlogApp { get; private set; } public Status GetStatus() { if (this.Status == Status.Deny && DateTime.Now > this.ApplyTime.AddDays(3)) { return Status.None; } return this.Status; } public async Task<bool> Pass() { var replyContent = $"恭喜您!您的博客地址更改申请已通过,新的博客地址:<a href='{this.TargetBlogApp}' target='_blank'>{this.TargetBlogApp}</a>"; return await base.Pass(replyContent, new BlogChangedEvent() { UserAlias = this.User.Alias, TargetUserAlias = this.TargetBlogApp }); } public bool Lock() { var replyContent = "抱歉!您的博客地址更改申请没有被批准,并且申请已被锁定,具体请联系contact@cnblogs.com。"; return base.Lock(replyContent); } public async Task Passed() { await base.Passed("您的博客地址更改申请已批准"); } public async Task Denied() { await base.Passed("您的博客地址更改申请未通过审批"); } public async Task Locked() { await Denied(); } } }
BlogChangeApply 继承自 ApplyAggregateRoot,并且单独的 TargetBlogApp 操作,其他一些实现都是基本的参数传递操作,没有具体实现,JsPermissionApply 的实体代码就不贴了,和 BlogChangeApply 比较类似,只不过有一些不同的业务实现。
CNBlogs.Apply.Domain 改造之后,还要对应改造下 Repository,之前的代码大家可以看下 Github,这边我简单说下改造的过程,首先 IRepository 的设计不变:
namespace CNBlogs.Apply.Repository.Interfaces { public interface IRepository<TAggregateRoot> where TAggregateRoot : class, IAggregateRoot { IQueryable<TAggregateRoot> Get(int id); IQueryable<TAggregateRoot> GetAll(); } }
IRepository 对应 BaseRepository 实现,它的作用就是抽离出所有聚合根的 Repository 操作,并不单独包含 ApplyAggregateRoot,所以,我们还需要一个对 ApplyAggregateRoot 操作的 Repository 实现,定义如下:
namespace CNBlogs.Apply.Repository.Interfaces { public interface IApplyRepository<TApplyAggregateRoot> : IRepository<TApplyAggregateRoot> where TApplyAggregateRoot : ApplyAggregateRoot { IQueryable<TApplyAggregateRoot> GetByUserId(int userId); IQueryable<TApplyAggregateRoot> GetWaiting(int userId); IQueryable<TApplyAggregateRoot> GetWaiting(); } }
大家如果熟悉之前代码的话,会发现 IApplyRepository 的定义和 IJsPermissionApplyRepository 的定义是一摸一样的,设计 IApplyRepository 的好处就是,对于申请实体的相同操作,我们就不需要再写重复代码了,比如 IJsPermissionApplyRepository 和 IBlogChangeApplyRepository 的定义:
namespace CNBlogs.Apply.Repository.Interfaces { public interface IJsPermissionApplyRepository : IApplyRepository<JsPermissionApply> { } } namespace CNBlogs.Apply.Repository.Interfaces { public interface IBlogChangeApplyRepository : IApplyRepository<BlogChangeApply> { IQueryable<BlogChangeApply> GetByTargetAliasWithWait(string targetBlogApp); } }
当然,除了上面的代码改造,还有一些其他功能的添加,比如 ApplyAuthenticationService 领域服务增加了 VerfiyForBlogChange 等等,具体的一些改变,大家可以查看提交。
CNBlogs.Apply.Sample 开发进行到这,对于现阶段的我来说,应用领域驱动设计我是比较满意的,虽然还有一些不完善的地方,但至少除了 CNBlogs.Apply.Domain,在其他项目中是看不到业务实现代码的,如果业务需求发生变化,首先更改的是 CNBlogs.Apply.Domain,而不是不是其它项目,这是一个基本点。
先设计 CNBlogs.Apply.Domain 和 CNBlogs.Apply.Domain.Tests,就能完成整个的业务系统设计,其它都是一些技术实现或工作流程实现,这个路子我觉得是正确的,以后边做边完善并学习。
相关文章推荐
- Linux_Centos使用mutt+msmtp发送邮件
- 转:java native
- HttpURLConnection 进行WebService请求
- 【51CTO学院三周年】大数据
- 有时间一天看一次
- (8)LR翻页脚本并在每页实现业务操作
- 那些最好的轮子 - PHP篇
- Android优化应用启动速度
- 风云办公室
- 报错集合
- LeetCode Integer Break(整数拆分问题)
- 获取栈顶Activity与判断是否有网络
- js使用cookies存取用户名和密码
- Elasticsearch java API (12)Search API MultiSearch API
- Mysql 配置文件详解
- Windows 7 32 安装Numpy
- xml解析+刷新
- 轻量级数据库 ----SQLite
- svm 核函数
- ftl总结