上一篇:《DDD 领域驱动设计-领域模型中的用户设计?

开源地址: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,就能完成整个的业务系统设计,其它都是一些技术实现或工作流程实现,这个路子我觉得是正确的,以后边做边完善并学习。

DDD 领域驱动设计-两个实体的碰撞火花的更多相关文章

  1. DDD 领域驱动设计-“臆想”中的实体和值对象

    其他博文: DDD 领域驱动设计-三个问题思考实体和值对象 DDD 领域驱动设计-三个问题思考实体和值对象(续) 以下内容属于博主"臆想",如有不当,请别当真. 扯淡开始: 诺兰的 ...

  2. DDD 领域驱动设计-三个问题思考实体和值对象(续)

    上一篇:DDD 领域驱动设计-三个问题思考实体和值对象 说实话,整理现在这一篇博文的想法,在上一篇发布出来的时候就有了,但到现在才动起笔来,而且写之前又反复读了上一篇博文的内容及评论,然后去收集资料, ...

  3. DDD 领域驱动设计-三个问题思考实体和值对象

    消息场景:用户 A 发送一个消息给用户 B,用户 B 回复一个消息给用户 A... 现有设计:消息设计为实体并为聚合根,发件人.收件人设计为值对象. 三个问题: 实体最重要的特性是什么? Messag ...

  4. DDD领域驱动设计之聚合、实体、值对象

    关于具体需求,请看前面的博文:DDD领域驱动设计实践篇之如何提取模型,下面是具体的实体.聚合.值对象的代码,不想多说什么是实体.聚合等概念,相信理论的东西大家已经知晓了.本人对DDD表示好奇,没有在真 ...

  5. 浅谈我对DDD领域驱动设计的理解

    从遇到问题开始 当人们要做一个软件系统时,一般总是因为遇到了什么问题,然后希望通过一个软件系统来解决. 比如,我是一家企业,然后我觉得我现在线下销售自己的产品还不够,我希望能够在线上也能销售自己的产品 ...

  6. DDD 领域驱动设计-商品建模之路

    最近在做电商业务中,有关商品业务改版的一些东西,后端的架构设计采用现在很流行的微服务,有关微服务的简单概念: 微服务是一种架构风格,一个大型复杂软件应用由一个或多个微服务组成.系统中的各个微服务可被独 ...

  7. DDD 领域驱动设计-谈谈 Repository、IUnitOfWork 和 IDbContext 的实践(3)

    上一篇:<DDD 领域驱动设计-谈谈 Repository.IUnitOfWork 和 IDbContext 的实践(2)> 这篇文章主要是对 DDD.Sample 框架增加 Transa ...

  8. DDD 领域驱动设计-领域模型中的用户设计

    上一篇:<DDD 领域驱动设计-如何控制业务流程?> 开源地址:https://github.com/yuezhongxin/CNBlogs.Apply.Sample(代码已更新,并增加了 ...

  9. DDD 领域驱动设计-如何完善 Domain Model(领域模型)?

    上一篇:<DDD 领域驱动设计-如何 DDD?> 开源地址:https://github.com/yuezhongxin/CNBlogs.Apply.Sample(代码已更新) 阅读目录: ...

随机推荐

  1. 【流量劫持】躲避 HSTS 的 HTTPS 劫持

    前言 HSTS 的出现,对 HTTPS 劫持带来莫大的挑战. 不过,HSTS 也不是万能的,它只能解决 SSLStrip 这类劫持方式.但仔细想想,SSLStrip 这种算劫持吗? 劫持 vs 钓鱼 ...

  2. UML课程复习重点

    第一章 一.UML图示建模工具 二.UML--统一建模语言,以图形符号为基础,描述软件模型既简洁又清晰.它不是开发方法,是独立于任何开发方法之外的语言.它用于描述软件系统分析.设计和实施中的各种模型. ...

  3. wordpress多站点配置

    wordpress作为全球第一的个人博客搭建平台一直在国内外有着较高的人气,从3.0版本开始就已经支持多站点的搭建.该功能可以让子站点运行主站点的程序,不需要再每个站点分别存放网站程序.最近更新的4. ...

  4. TypeScript为Zepto编写LazyLoad插件

    平时项目中使用的全部是jQuery框架,但是对于做webapp来说jQuery太过于庞大,当然你可以选择jQuery 2.*针对移动端的版本. 这里我采用移动端使用率比较多的zepto框架,他跟jqu ...

  5. 编写高质量代码:改善Java程序的151个建议(第8章:异常___建议114~117)

    建议114:不要在构造函数中抛出异常 Java异常的机制有三种: Error类及其子类表示的是错误,它是不需要程序员处理也不能处理的异常,比如VirtualMachineError虚拟机错误,Thre ...

  6. QQ空间动态爬虫

    作者:虚静 链接:https://zhuanlan.zhihu.com/p/24656161 来源:知乎 著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 先说明几件事: 题目的意 ...

  7. Html 制作相册

    本文主要讲述采用Html5+jQuery+CSS 制作相册的小小记录. 主要功能点: Html5进行布局 调用jQuery(借用官网的一句话:The Write Less, Do More)极大的简化 ...

  8. android Handler介绍

    Handler使用介绍: Handler根据接收的消息,处理UI更新.Thread线程发出消息,通知Handler更新UI. Handler mHandler = new Handler() {  p ...

  9. SQL-union

    集合运算符是对两个集合操作的,两个集合必须具有相同的列数,列具有相同的数据类型(至少能隐式转换的),最终输出的集合的列名由第一个集合的列名来确定.(可以用来连接多个结果)联合(union)与连接(jo ...

  10. 使用Hystrix提高系统可用性

    今天稍微复杂点的互联网应用,服务端基本都是分布式的,大量的服务支撑起整个系统,服务之间也难免有大量的依赖关系,依赖都是通过网络连接起来. (图片来源:https://github.com/Netfli ...