Domain Model(领域模型)

上一篇:《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 应用很有挑战,也会很有意思。

Domain Model(领域模型)的更多相关文章

  1. 一缕阳光:DDD(领域驱动设计)应对具体业务场景,如何聚焦 Domain Model(领域模型)?

    写在前面 阅读目录: 问题根源是什么? <领域驱动设计-软件核心复杂性应对之道>分层概念 Repository(仓储)职责所在? Domain Model(领域模型)重新设计 Domain ...

  2. 死去活来,而不变质:Domain Model(领域模型) 和 EntityFramework 如何正确进行对象关系映射?

    写在前面 阅读目录: 设计误区 数据库已死 枚举映射 关联映射 后记 在上一篇<一缕阳光:DDD(领域驱动设计)应对具体业务场景,如何聚焦 Domain Model(领域模型)?>博文中, ...

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

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

  4. 理解领域模型Domain Model

    定义 业务对象模型(也叫领域模型 domain model)是描述业务用例实现的对象模型.它是对业务角色和业务实体之间应该如何联系和协作以执行业务的一种抽象.业务对象模型从业务角色内部的观点定义了业务 ...

  5. 领域模型(Domain Model)

    领域模型(Domain Model) 一:面向对象设计中最简单的部分与最难的部分 如果说事务脚本是 面向过程 的,那么领域模型就是 面向对象 的.面向对象的一个很重要的点就是:“把事情交给最适合的类去 ...

  6. DDD(领域驱动设计)应对具体业务场景,Domain Model(领域模型)到底如何设计?

    DDD(领域驱动设计)应对具体业务场景,Domain Model(领域模型)到底如何设计? 写在前面 阅读目录: 迷雾森林 找回自我 开源地址 后记 毫无疑问,领域驱动设计的核心是领域模型,领域模型的 ...

  7. Domain Model(领域模型) 和 EntityFramework 如何正确进行对象关系映射?

    Domain Model(领域模型) 和 EntityFramework 如何正确进行对象关系映射? 写在前面 阅读目录: 设计误区 数据库已死 枚举映射 关联映射 后记 在上一篇<一缕阳光:D ...

  8. DDD(领域驱动设计)应对具体业务场景,如何聚焦 Domain Model(领域模型)?

    DDD(领域驱动设计)应对具体业务场景,如何聚焦 Domain Model(领域模型)? 阅读目录: 问题根源是什么? <领域驱动设计-软件核心复杂性应对之道>分层概念 Repositor ...

  9. 贫血模型;DTO:数据传输对象(Data Transfer Object);AutoMapper ;Domain Model(领域模型);DDD(领域驱动设计)

    ====================== 我自己的理解 ========================== 一:  DTO  我自己的理解,就是 比如你有一个类,跟数据库的table表结构一模一 ...

随机推荐

  1. 编写高质量代码改善java程序的151个建议——[52-57]String !about String How to use them?

    原创地址:   http://www.cnblogs.com/Alandre/  (泥沙砖瓦浆木匠),须要转载的,保留下! Thanks Although the world is full of s ...

  2. zoj 1134 - Strategic Game

    题目:给你一棵树.找到最小的顶点集合,使得全部的边至少有一个顶点在这个集合中. 分析:树形dp,图论,最小顶点覆盖. 方案1:树形dp.分别记录每一个节点取和不取的最优解f(k.0)与f(k,1): ...

  3. windows live writer插件说明文档(附录网盘地址)

    百度云地址:http://pan.baidu.com/s/1hqnjzjY 1.Screen Capture tool 用于直接在WLWriter中进行截图的一个插件,要配合SnagIt 这个软件使用 ...

  4. TCP与UDP在socket编程中的区别 (网络收集转载)

    http://blog.chinaunix.net/uid-26421509-id-3814684.html 一.TCP与UDP的区别 基于连接与无连接  对系统资源的要求(TCP较多,UDP少)  ...

  5. 基于visual Studio2013解决C语言竞赛题之1091多项式

        题目 解决代码及点评 /************************************************************************/ /* ...

  6. poj 2299 逆序数

    http://poj.org/problem?id=2299 坑:答案是long long 输出……!!!!! 题意是:求一个数组进行冒泡排序交换的次数 题解:求逆序数 题解Ⅰ: 归并排序求逆序数 归 ...

  7. Oracle误删除表数据后的恢复具体解释

    Oracle误删除表数据后的恢复具体解释 測试环境: SYSTEM:IBM AIX 5L                         Oracle Version:10gR2 1. undo_re ...

  8. 修改合同号的bapi

    这个例子是在合同号中新增项目号: 1.先要读取该合同号的信息用一个BAPI BAPI_CONTRACT_GETDETAIL 2.调用修改合同号的BAPI. 代码如下: REPORT ztest_cla ...

  9. Linux-C语言中gettimeofday()函数的使用方法(转载)

    1.简介: 在C语言中可以使用函数gettimeofday()函数来得到时间.它的精度可以达到微妙 2.函数原型: #include<sys/time.h> int gettimeofda ...

  10. jQuery事件大全

    jQuery事件大全 attribute:  $(" p" ).addclass(css中定义的样式类型) 给某个元素添加样式 $(" img" ).attr( ...