上一篇:《DDD 领域驱动设计-如何 DDD?

开源地址:https://github.com/yuezhongxin/CNBlogs.Apply.Sample(代码已更新)

阅读目录:

  • JsPermissionApply 生命周期
  • 改进 JsPermissionApply 实体
  • 重命名 UserAuthenticationService
  • 改进 JsPermissionApplyRepository
  • 改进领域单元测试

如何完善领域模型?指的是完善 JS 权限申请领域模型,也就是 JsPermissionApply Domain Model

在上篇博文中,关于 JsPermissionApply 领域模型的设计,只是一个刚出生的“婴儿”,还不是很成熟,并且很多细致的业务并没有考虑到,本篇将对 JsPermissionApply 领域模型进行完善,如何完善呢?望着解决方案中的项目和代码,好像又束手无策,这时候如果没有一点思考,而是直接编写代码,到最后你会发现 DDD 又变成了脚本式开发,所以,我们在做领域模型开发的时候,需要一个切入点,把更多的精力放在业务上,而不是实现的代码上,那这个切入点是什么呢?没错,就是上篇博文中的“业务流程图”,又简单完善了下:

1. JsPermissionApply 生命周期

在完善 JsPermissionApply 领域模型之前,我们需要先探讨下 JsPermissionApply 实体的生命周期,这个在接下来完善的时候会非常重要,能影响 JsPermissionApply 实体生命周期的唯一因素,就是改变其自身的状态,从上面的业务流程图中,我们就可以看到改变状态的地方:“申请状态为待审核”、“申请状态改为通过”、“申请状态改为未通过”、“申请状态改为锁定”,能改变实体状态的行为都是业务行为,这个在领域模型设计的时候,要重点关注。

用户申请 JS 权限的最终目的是开通 JS 权限,对于 JsPermissionApply 实体而言,就是自身状态为“通过”,所以,我们可以认为,当 JsPermissionApply 实体状态为“通过”的时候,那么 JsPermissionApply 实体的生命周期就结束了,JsPermissionApply 生命周期开始的时候,就是创建 JsPermissionApply 实体对象的时候,也就是实体状态为“待审核”的时候。

好,上面的分析听起来很有道理,感觉应该没什么问题,但在实现 JsPermissionApplyRepository 的时候,就会发现有很多问题(后面会说到),JsPermissionApply 的关键字是 Apply(申请),对于一个申请来说,生命周期的结束就是其经过了审核,不论是通过还是不通过,锁定还是不锁定,这个申请的生命周期就结束了,再次申请就是另一个 JsPermissionApply 实体对象了,对于实体生命周期有效期内,其实体必须是唯一性的。

导致上面两种分析的不同,主要是关注点不同,第一种以用户为中心,第二种以申请为中心,以用户为中心的分析方式,在我们平常的开发过程中会经常遇到,因为我们开发的系统基本上都是给人用的,所以很多业务都是围绕用户进行展开,好像没有什么不对,但如果这样进行分析设计,那么每个系统的核心域都是用户了,领域模型也变成了用户领域模型,所以,我们在分析业务系统的时候,最好进行细分,并把用户的因素隔离开,最后把核心和非核心进行区分开。

2. 改进 JsPermissionApply 实体

先看下之前 JsPermissionApply 实体的部分代码:

namespace CNBlogs.Apply.Domain
{
public class JsPermissionApply : IAggregateRoot
{
private IEventBus eventBus; ... public void Process(string replyContent, Status status)
{
this.ReplyContent = replyContent;
this.Status = status;
this.ApprovedTime = DateTime.Now; eventBus = IocContainer.Default.Resolve<IEventBus>();
if (this.Status == Status.Pass)
{
eventBus.Publish(new JsPermissionOpenedEvent() { UserId = this.UserId });
eventBus.Publish(new MessageSentEvent() { Title = "系统通知", Content = "审核通过", RecipientId = this.UserId });
}
else if (this.Status == Status.Deny)
{
eventBus.Publish(new MessageSentEvent() { Title = "系统通知", Content = "审核不通过", RecipientId = this.UserId });
}
}
}
}

Process 的设计会让领域专家看不懂,为什么?看下对应的单元测试:

[Fact]
public async Task ProcessApply()
{
var userId = 1;
var jsPermissionApply = await _jsPermissionApplyRepository.GetByUserId(userId).FirstOrDefaultAsync();
Assert.NotNull(jsPermissionApply); jsPermissionApply.Process("审核通过", Status.Pass);
_unitOfWork.RegisterDirty(jsPermissionApply);
Assert.True(await _unitOfWork.CommitAsync());
}

Process 是啥?如果领域专家不是开发人员,通过一个申请,他会认为应该有一个直接通过申请的操作,而不是调用一个不知道干啥的 Process 方法,然后再传几个不知道的参数,在 IDDD 书中,代码也是和领域专家交流的通用语言之一,所以,开发人员编写的代码需要让领域专家看懂,至少代码要表达一个最直接的业务操作。

所以,对于申请的处理,通过就是通过,不通过就是不通过,要用代码表达的简单粗暴

改进代码

namespace CNBlogs.Apply.Domain
{
public class JsPermissionApply : IAggregateRoot
{
private IEventBus eventBus; ... public async Task Pass()
{
this.Status = Status.Pass;
this.ApprovedTime = DateTime.Now;
this.ReplyContent = "恭喜您!您的JS权限申请已通过审批。"; eventBus = IocContainer.Default.Resolve<IEventBus>();
await eventBus.Publish(new JsPermissionOpenedEvent() { UserId = this.UserId });
await eventBus.Publish(new MessageSentEvent() { Title = "您的JS权限申请已批准", Content = this.ReplyContent, RecipientId = this.UserId });
} public async Task Deny(string replyContent)
{
this.Status = Status.Deny;
this.ApprovedTime = DateTime.Now;
this.ReplyContent = $"抱歉!您的JS权限申请没有被批准,{(string.IsNullOrEmpty(replyContent) ? "" : $"具体原因:{replyContent}<br/>")}麻烦您重新填写申请理由。"; eventBus = IocContainer.Default.Resolve<IEventBus>();
await eventBus.Publish(new MessageSentEvent() { Title = "您的JS权限申请未通过审批", Content = this.ReplyContent, RecipientId = this.UserId });
} public async Task Lock()
{
this.Status = Status.Lock;
this.ApprovedTime = DateTime.Now;
this.ReplyContent = "抱歉!您的JS权限申请没有被批准,并且申请已被锁定,具体请联系contact@cnblogs.com。"; eventBus = IocContainer.Default.Resolve<IEventBus>();
await eventBus.Publish(new MessageSentEvent() { Title = "您的JS权限申请已被锁定", Content = this.ReplyContent, RecipientId = this.UserId });
}
}
}

这样改进还有一个好处,就是改变 JsPermissionApply 状态会变的更加明了,也更加受保护,什么意思?比如之前的 Process 的方法,我们可以通过参数任意改变 JsPermissionApply 的状态,这是不被允许的,现在我们只能通过三个操作改变对应的三种状态。

JsPermissionApply 实体改变了,对应的单元测试也要进行更新(后面讲到)。

3. 重命名 UserAuthenticationService

UserAuthenticationService 是领域服务,一看到这个命名,会认为这是关于用户验证的服务,我们再看上面的流程图,会发现有一个“验证用户信息”操作,但前面还有一个“验证申请状态”操作,而在之前的设计实现中,这两个操作都是放在 UserAuthenticationService 中的,如下:

namespace CNBlogs.Apply.Domain.DomainServices
{
public class UserAuthenticationService : IUserAuthenticationService
{
private IJsPermissionApplyRepository _jsPermissionApplyRepository; public UserAuthenticationService(IJsPermissionApplyRepository jsPermissionApplyRepository)
{
_jsPermissionApplyRepository = jsPermissionApplyRepository;
} public async Task<string> Verfiy(int userId)
{
if (!await UserService.IsHasBlog(userId))
{
return "必须先开通博客,才能申请JS权限";
}
var entity = await _jsPermissionApplyRepository.GetByUserId(userId).FirstOrDefaultAsync();
if (entity != null)
{
if (entity.Status == Status.Pass)
{
return "您的JS权限申请正在处理中,请稍后";
}
if (entity.Status == Status.Lock)
{
return "您暂时无法申请JS权限,请联系contact@cnblogs.com";
}
}
return string.Empty;
}
}
}

IsHasBlog 属于用户验证,但下面的 jsPermissionApply.Status 验证就不属于了,放在 UserAuthenticationService 中也不合适,我的想法是把这部分验证独立出来,用 ApplyAuthenticationService 领域服务实现,后来仔细一想,似乎和上面实体生命周期遇到的问题有些类似,误把用户当作核心考虑了,在 JS 权限申请和审核系统中,对于用户的验证,其实就是对申请的验证,所验证的最终目的是:某个用户是否符合要求进行申请操作?

所以,对于申请相关的验证操作,应该命名为 ApplyAuthenticationService,并且验证代码都放在其中。

改进代码

namespace CNBlogs.Apply.Domain.DomainServices
{
public class ApplyAuthenticationService : IApplyAuthenticationService
{
private IJsPermissionApplyRepository _jsPermissionApplyRepository; public ApplyAuthenticationService(IJsPermissionApplyRepository jsPermissionApplyRepository)
{
_jsPermissionApplyRepository = jsPermissionApplyRepository;
} public async Task<string> Verfiy(int userId)
{
if (!await UserService.IsHasBlog(userId))
{
return "必须先开通博客,才能申请JS权限";
}
var entity = await _jsPermissionApplyRepository.GetEffective(userId).FirstOrDefaultAsync();
if (entity != null)
{
if (entity.Status == Status.Pass)
{
return "您的JS权限申请已开通,请勿重复申请";
}
if (entity.Status == Status.Wait)
{
return "您的JS权限申请正在处理中,请稍后";
}
if (entity.Status == Status.Lock)
{
return "您暂时无法申请JS权限,请联系contact@cnblogs.com";
}
}
return string.Empty;
}
}
}

除了 UserAuthenticationService 重命名为 ApplyAuthenticationService,还增加了对 JsPermissionApply 状态为 Lock 的验证,并且 IJsPermissionApplyRepository 的 GetByUserId 调用改为了 GetEffective,这个下面会讲到。

4. 改进 JsPermissionApplyRepository

原先的 IJsPermissionApplyRepository 设计:

namespace CNBlogs.Apply.Repository.Interfaces
{
public interface IJsPermissionApplyRepository : IRepository<JsPermissionApply>
{
IQueryable<JsPermissionApply> GetByUserId(int userId);
}
}

这样的 IJsPermissionApplyRepository 的设计,看似没什么问题,并且问题也不出现在实现,而是出现在调用的时候,GetByUserId 会在两个地方调用:

  • ApplyAuthenticationService.Verfiy 调用:获取 JsPermissionApply 实体对象,用于状态的验证,判断是否符合申请的要求。
  • 领域的单元测试代码中(或者应用层):获取 JsPermissionApply 实体对象,用于更新其状态。

对于上面两个调用方来说,GetByUserId 太模糊了,甚至不知道调用的是什么东西?并且这两个地方的调用,获取的 JsPermissionApply 实体对象也并不相同,严格来说,应该是不同状态下的 JsPermissionApply 实体对象,我们仔细分析下:

  • ApplyAuthenticationService.Verfiy 调用:判断是否符合申请的要求。什么情况下会符合申请要求呢?就是当状态为“未通过”的时候,对于申请验证来说,可以称之为“有效的”申请,相反,获取用于申请验证的 JsPermissionApply 实体对象,应该称为“无效的”,调用命名为 GetInvalid
  • 领域的单元测试代码中(或者应用层):用于更新 JsPermissionApply 实体状态。什么状态下的 JsPermissionApply 实体,可以更新其状态呢?答案就是状态为“待审核”,所以这个调用应该获取状态为“待审核”的 JsPermissionApply 实体对象,调用命名为 GetWaiting

改进代码

namespace CNBlogs.Apply.Repository
{
public class JsPermissionApplyRepository : BaseRepository<JsPermissionApply>, IJsPermissionApplyRepository
{
public JsPermissionApplyRepository(IDbContext dbContext)
: base(dbContext)
{ } public IQueryable<JsPermissionApply> GetInvalid(int userId)
{
return _entities.Where(x => x.UserId == userId && x.Status != Status.Deny && x.IsActive);
} public IQueryable<JsPermissionApply> GetWaiting(int userId)
{
return _entities.Where(x => x.UserId == userId && x.Status == Status.Wait && x.IsActive);
}
}
}

5. 改进领域单元测试

原先的单元测试代码:

namespace CNBlogs.Apply.Domain.Tests
{
public class JsPermissionApplyTest
{
private IUserAuthenticationService _userAuthenticationService;
private IJsPermissionApplyRepository _jsPermissionApplyRepository;
private IUnitOfWork _unitOfWork; public JsPermissionApplyTest()
{
CNBlogs.Apply.BootStrapper.Startup.Configure(); _userAuthenticationService = IocContainer.Default.Resolve<IUserAuthenticationService>();
_jsPermissionApplyRepository = IocContainer.Default.Resolve<IJsPermissionApplyRepository>();
_unitOfWork = IocContainer.Default.Resolve<IUnitOfWork>();
} [Fact]
public async Task Apply()
{
var userId = 1;
var verfiyResult = await _userAuthenticationService.Verfiy(userId);
Console.WriteLine(verfiyResult);
Assert.Empty(verfiyResult); var jsPermissionApply = new JsPermissionApply("我要申请JS权限", userId, "");
_unitOfWork.RegisterNew(jsPermissionApply);
Assert.True(await _unitOfWork.CommitAsync());
} [Fact]
public async Task ProcessApply()
{
var userId = 1;
var jsPermissionApply = await _jsPermissionApplyRepository.GetByUserId(userId).FirstOrDefaultAsync();
Assert.NotNull(jsPermissionApply); jsPermissionApply.Process("审核通过", Status.Pass);
_unitOfWork.RegisterDirty(jsPermissionApply);
Assert.True(await _unitOfWork.CommitAsync());
}
}
}

看起来似乎没什么问题,一个申请和一个审核测试,但我们仔细看上面的业务流程图,会发现这个测试代码并不能完全覆盖所有的业务,并且这个测试代码也有些太敷衍了,在测试驱动开发中,测试代码就是所有的业务表达,它应该是项目中最全面和最精细的代码,在领域驱动设计中,当领域层的代码完成后,领域专家查看的时候,不会看领域层,而是直接看单元测试中的代码,因为领域专家不懂代码,并且他也不懂你是如何实现的,它关心的是我该如何使用它?我想要的业务操作,你有没有完全实现?单元测试就是最好的体现。

我们该如何改进呢?还是回归到上面的业务流程图,并从中归纳出领域专家想要的几个操作:

  • 填写 JS 权限申请(需要填写申请理由)
  • 通过 JS 权限申请
  • 拒绝 JS 权限申请(需要填写拒绝原因)
  • 锁定 JS 权限申请
  • 删除(待考虑)

上面这几个操作,都必须在单元测试代码中有所体现,并且尽量让测试颗粒化,比如一个验证操作,你可以对不同的参数编写不同的单元测试代码。

改进代码

namespace CNBlogs.Apply.Domain.Tests
{
public class JsPermissionApplyTest
{
private IApplyAuthenticationService _applyAuthenticationService;
private IJsPermissionApplyRepository _jsPermissionApplyRepository;
private IUnitOfWork _unitOfWork; public JsPermissionApplyTest()
{
CNBlogs.Apply.BootStrapper.Startup.Configure(); _applyAuthenticationService = IocContainer.Default.Resolve<IApplyAuthenticationService>();
_jsPermissionApplyRepository = IocContainer.Default.Resolve<IJsPermissionApplyRepository>();
_unitOfWork = IocContainer.Default.Resolve<IUnitOfWork>();
} [Fact]
public async Task ApplyTest()
{
var userId = 1;
var verfiyResult = await _applyAuthenticationService.Verfiy(userId);
Console.WriteLine(verfiyResult);
Assert.Empty(verfiyResult); var jsPermissionApply = new JsPermissionApply("我要申请JS权限", userId, "");
_unitOfWork.RegisterNew(jsPermissionApply);
Assert.True(await _unitOfWork.CommitAsync());
} [Fact]
public async Task ProcessApply_WithPassTest()
{
var userId = 1;
var jsPermissionApply = await _jsPermissionApplyRepository.GetWaiting(userId).FirstOrDefaultAsync();
Assert.NotNull(jsPermissionApply); await jsPermissionApply.Pass();
_unitOfWork.RegisterDirty(jsPermissionApply);
Assert.True(await _unitOfWork.CommitAsync());
} [Fact]
public async Task ProcessApply_WithDenyTest()
{
var userId = 1;
var jsPermissionApply = await _jsPermissionApplyRepository.GetWaiting(userId).FirstOrDefaultAsync();
Assert.NotNull(jsPermissionApply); await jsPermissionApply.Deny("理由太简单了。");
_unitOfWork.RegisterDirty(jsPermissionApply);
Assert.True(await _unitOfWork.CommitAsync());
} [Fact]
public async Task ProcessApply_WithLockTest()
{
var userId = 1;
var jsPermissionApply = await _jsPermissionApplyRepository.GetWaiting(userId).FirstOrDefaultAsync();
Assert.NotNull(jsPermissionApply); await jsPermissionApply.Lock();
_unitOfWork.RegisterDirty(jsPermissionApply);
Assert.True(await _unitOfWork.CommitAsync());
}
}
}

改进好了代码之后,对于开发人员来说,任务似乎完成了,但对于领域专家来说,仅仅是个开始,因为他必须要通过提供的四个操作,来验证各种情况下的业务操作是否正确,我们来归纳下:

  • 申请 -> 申请:ApplyTest -> ApplyTest
  • 申请 -> 通过:ApplyTest -> ProcessApply_WithPassTest
  • 申请 -> 拒绝:ApplyTest -> ProcessApply_WithDenyTest
  • 申请 -> 锁定:ApplyTest -> ProcessApply_WithLockTest
  • 申请 -> 通过 -> 申请:ApplyTest -> ProcessApply_WithPassTest -> ApplyTest
  • 申请 -> 拒绝 -> 申请:ApplyTest -> ProcessApply_WithDenyTest -> ApplyTest
  • 申请 -> 锁定 -> 申请:ApplyTest -> ProcessApply_WithLockTest -> ApplyTest

确认上面的所有测试都通过之后,就说明 JsPermissionApply 领域模型设计的还算可以。

DDD 倾向于“测试先行,逐步改进”的设计思路。测试代码本身便是通用语言在程序中的表达,在开发人员的帮助下,领域专家可以阅读测试代码来检验领域对象是否满足业务需求。

当领域层的代码基本完成之后,就可以在地基上添砖加瓦了,后面的实现都是工作流程的实现,没有任何业务的包含,比如上面对领域层的单元测试,其实就是应用层的实现,在添砖加瓦的过程中,切记地基的重要性,否则即使盖再高的摩天大楼,地基不稳,也照样垮塌。

实际项目的 DDD 应用很有挑战,也会很有意思。

DDD 领域驱动设计-如何完善 Domain Model(领域模型)?的更多相关文章

  1. DDD 领域驱动设计-如何控制业务流程?

    上一篇:<DDD 领域驱动设计-如何完善 Domain Model(领域模型)?> 开源地址:https://github.com/yuezhongxin/CNBlogs.Apply.Sa ...

  2. 关于DDD领域驱动设计的理论知识收集汇总

    原文:关于DDD领域驱动设计的理论知识收集汇总 最近一直在学习领域驱动设计(DDD)的理论知识,从网上搜集了一些个人认为比较有价值的东西,贴出来和大家分享一下: 我一直觉得不要盲目相信权威,比如不能一 ...

  3. DDD领域驱动设计落地实践(十分钟看完,半小时落地)

    一.引子 不知今年吹了什么风,忽然DDD领域驱动设计进入大家视野.该思想源于2003年 Eric Evans编写的"Domain-Driven Design领域驱动设计"简称DDD ...

  4. DDD领域驱动设计-概述-Ⅰ

     如果我看得更远,那是因为我站在巨人的肩膀上.(If I have seen further it is by standing on ye shoulder of Giants.)         ...

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

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

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

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

  7. DDD 领域驱动设计-两个实体的碰撞火花

    上一篇:<DDD 领域驱动设计-领域模型中的用户设计?> 开源地址:https://github.com/yuezhongxin/CNBlogs.Apply.Sample(代码已更新) 在 ...

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

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

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

    前言:上篇介绍了DDD设计Demo里面的聚合划分以及实体和聚合根的设计,这章继续来说说DDD里面最具争议的话题之一的仓储Repository,为什么Repository会有这么大的争议,博主认为主要原 ...

随机推荐

  1. Android指纹解锁

    Android6.0及以上系统支持指纹识别解锁功能:项目中用到,特此抽离出来,备忘. 功能是这样的:在用户将app切换到后台运行(超过一定的时长,比方说30秒),再进入程序中的时候就会弹出指纹识别的界 ...

  2. .NET 4.6.2正式发布带来众多特性

    虽然大多数人的注意力都集中在.NET Core上,但与原来的.NET Framework相关的工作还在继续..NET Framework 4.6.2正式版已于近日发布,其重点是安全和WinForms/ ...

  3. 一步步开发自己的博客 .NET版(10、前端对话框和消息框的实现)

    关于前端对话框.消息框的优秀插件多不胜数.造轮子是为了更好的使用轮子,并不是说自己造的轮子肯定好.所以,这个博客系统基本上都是自己实现的,包括日志记录.响应式布局.评论功能等等一些本可以使用插件的.好 ...

  4. Linq之旅:Linq入门详解(Linq to Objects)

    示例代码下载:Linq之旅:Linq入门详解(Linq to Objects) 本博文详细介绍 .NET 3.5 中引入的重要功能:Language Integrated Query(LINQ,语言集 ...

  5. 【前端性能】高性能滚动 scroll 及页面渲染优化

    最近在研究页面渲染及web动画的性能问题,以及拜读<CSS SECRET>(CSS揭秘)这本大作. 本文主要想谈谈页面优化之滚动优化. 主要内容包括了为何需要优化滚动事件,滚动与页面渲染的 ...

  6. DDD 领域驱动设计-看我如何应对业务需求变化,愚蠢的应对?

    写在前面 阅读目录: 具体业务场景 业务需求变化 "愚蠢"的应对 消息列表实现 消息详情页实现 消息发送.回复.销毁等实现 回到原点的一些思考 业务需求变化,领域模型变化了吗? 对 ...

  7. 02.SQLServer性能优化之---牛逼的OSQL----大数据导入

    汇总篇:http://www.cnblogs.com/dunitian/p/4822808.html#tsql 上一篇:01.SQLServer性能优化之----强大的文件组----分盘存储 http ...

  8. AutoMapper随笔记

    平台之大势何人能挡? 带着你的Net飞奔吧! http://www.cnblogs.com/dunitian/p/4822808.html#skill 先看效果:(完整Demo:https://git ...

  9. 10个最好用的HTML/CSS 工具、插件和资料库

    大家在使用HTML/CSS开发项目的过程中,有使用过哪些工具,插件和库?下面介绍的10种HTML/CSS工具,插件和资料库,是国外程序员经常用到的. Firebug Lite FirebugLite ...

  10. Unity 序列化

    Script Serialization http://docs.unity3d.com/Manual/script-Serialization.html 自定义序列化及例子: http://docs ...