DDD 实战记录——实现「借鉴学习计划」
「借鉴学习计划」的核心是:复制一份别人的学习计划到自己的计划中,并同步推送学习任务给自己,并且每个操作都要发送通知给对方。
它们的类图如下:

它们的关系是一对多:
// Schedule
entity.HasOne(x => x.Parent).WithMany(x => x.Children).HasForeignKey(x => x.ParentId).OnDelete(DeleteBehavior.Restrict);
entity.HasIndex(nameof(Schedule.UserId), nameof(Schedule.ParentId)).IsUnique().HasFilter($"[{nameof(Schedule.Deleted)}]=0 and [{nameof(Schedule.ParentId)}] is not null");
// ScheduleItem
entity.HasOne(i => i.Schedule).WithMany(s => s.Items).HasForeignKey(i => i.ScheduleId);
entity.HasOne(i => i.Html).WithOne(h => h.Item).HasForeignKey<ScheduleItemHtml>(h => h.ScheduleItemId);
entity.HasOne(x => x.Parent).WithMany(x => x.Children).HasForeignKey(x => x.ParentId).OnDelete(DeleteBehavior.Restrict);
entity.HasIndex(nameof(ScheduleItem.UserId), nameof(ScheduleItem.ParentId)).IsUnique().HasFilter($"[{nameof(ScheduleItem.Deleted)}]=0 and [{nameof(ScheduleItem.ParentId)}] is not null");
按照 DDD 的思路,业务应该发生在领域层中,事件也是从领域中触发的,整个流程的可读性比较强,下面以借鉴功能为例:
// Domain.Schedule.cs
/* 借鉴 */
public class Schedule : Entity, IAggregateRoot
{
private Schedule()
{
Items = new List<ScheduleItem>();
Children = new List<Schedule>();
}
public Schedule(string title, string description, Guid userId, bool isPrivate = false, long? parentId = null) : this()
{
Title = title;
Description = description;
UserId = userId;
IsPrivate = isPrivate;
if (parentId.HasValue)
{
ParentId = parentId;
}
AddDomainEvent(new ScheduleCreatedEvent(UUID));
}
public Schedule Subscribe(Guid userId)
{
if (userId == UserId)
{
throw new ValidationException("不能借鉴自己的计划");
}
if (ParentId > 0)
{
throw new ValidationException("很抱歉,暂时不支持借鉴来的学习计划");
}
var child = Deliver(userId);
Children.Add(child);
FollowingCount += 1;
AddDomainEvent(new NewSubscriberEvent(this.UUID, child.UUID));
return child;
}
public Schedule Deliver(Guid userId)
{
var schedule = new Schedule(Title, Description, userId, isPrivate: false, Id);
return schedule;
}
}
阅读Subscribe():首先不能借鉴自己的计划,其次不能借鉴借鉴来的计划,Deliver()生产或者说克隆一个Schedule出来,作为当前计划的孩子,然后把借鉴数+1,触发有新的借鉴者事件NewSubscriberEvent。
Application作为领域的消费者,就可以直接消费这个领域了。
// Application.ScheduleAppService.cs
public async Task<long> SubscribeAsync(long id, Guid userId)
{
var schedule = await _repository.Schedules.FirstOrDefaultAsync(s => s.Id == id);
if (schedule != null)
{
try
{
schedule.Subscribe(userId);
await _repository.UnitOfWork.SaveEntitiesAsync();
}
catch (Exception ex) when (ex.InnerException is SqlException sqlerror)
{
if (sqlerror.Number == 2601)
{
throw new ValidationException("已经借鉴过了");
}
}
}
return 0;
}
最后使用 UnitOfWork 工作单元持久化到数据库,并分发领域中产生的事件。
// Infrastructure.DbContext.cs
public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default(CancellationToken))
{
//https://stackoverflow.com/questions/45804470/the-dbcontext-of-type-cannot-be-pooled-because-it-does-not-have-a-single-public
var bus = this.GetService<ICapPublisher>();
using (var trans = Database.BeginTransaction())
{
if (await SaveChangesAsync(cancellationToken) > 0)
{
await bus.DispatchDomianEventsAsync(this);
trans.Commit();
}
else
{
trans.Rollback();
return false;
}
}
return true;
}
通过 EF Core 的上下文实现了 IUnitOfWork 接口,通过事务保证一致性。这里使用 DotNetCore.CAP 这个优秀的开源产品帮助我们分发事件消息,处理最终一致性。
public static class CapPublisherExtensions
{
public static async Task<int> DispatchDomianEventsAsync(this ICapPublisher bus, AcademyContext ctx)
{
var domainEntities = ctx.ChangeTracker
.Entries<BaseEntity>()
.Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any());
if (domainEntities == null || domainEntities.Count() < 1)
{
return 0;
}
var domainEvents = domainEntities
.SelectMany(x => x.Entity.DomainEvents)
.ToList();
domainEntities.ToList()
.ForEach(entity => entity.Entity.ClearDomainEvents());
var tasks = domainEvents
.Select(domainEvent => bus.PublishAsync(domainEvent.GetEventName(), domainEvent));
await Task.WhenAll(tasks);
return domainEvents.Count;
}
}
这里参考了eShopContainer的实现。在触发事件的时候一直都有一个疑问,我们的实体的主键是自增长类型的,只有持久化到数据库之后才知道 Id 的值是多少,但是我们在领域事件中却经常需要这个 Id作为消息的一部分。我解决这个问题的方案,给实体增加一个 GUID 类型的字段UUID,作为唯一身份标识,这样我们就不需要关心最终的Id是多少了,用UUID就可以定位到这个实体了。
事件消息分发出去后,关心这个事件消息的领域就能通过订阅去消费这个事件消息了。
当有新的借鉴者的时候,“消息中心”这个领域关心这个事件,它的MsgService通过DotNetCore.CAP订阅事件消息:
// Msg.AppService.cs
[CapSubscribe(EventConst.NewSubscriber, Group = MsgAppConst.MessageGroup)]
public async Task HandleNewSubscriberEvent(NewSubscriberEvent e)
{
// Notify schedule author
var child = await _repository.FindByUUID<Schedule>(e.ChildScheduleUuid).Include(x => x.Parent).FirstOrDefaultAsync();
if (child == null) return;
var auth = await _uCenter.GetUser(x => x.UserId, child.Parent.UserId);
if (auth == null) return;
var subscriber = await _uCenter.GetUser(x => x.UserId, child.UserId);
if (subscriber == null) return;
var msg = new Notification
{
RecipientId = auth.SpaceUserId,
Title = $"有用户借鉴了您的「{child.Parent.Title}」",
Content = $@"<p>亲爱的 {auth.DisplayName} 同学:</p>
<p>
<b>
<a href='{AppConst.DomainAddress}/schedules/u/{subscriber.Alias}/{child.Id}'>
{subscriber.DisplayName}</a>
</b>
借鉴了您的学习计划
<a href='{AppConst.DomainAddress}/schedules/u/{auth.Alias}/{child.ParentId}'>
「{child.Parent.Title}」
</a>
</p>"
};
await _msgSvc.NotifyAsync(msg);
}
“消息中心”的业务是要给作者发送通知,它负责生产出通知Notification,因为我们团队已经有了基础服务——MsgService,已经实现发送通知的功能,所以只需要调用即可,如果没有的话我们就要自己来实现通过邮件或者短信进行通知。
源代码已托管在 github 上了
DDD 实战记录——实现「借鉴学习计划」的更多相关文章
- 【Maven实战技巧】「插件使用专题」Maven-Archetype插件创建自定义maven项目骨架
技术推荐 自定义Archetype Maven骨架/以当前项目为模板创建maven骨架,可以参考http://maven.apache.org/archetype/maven-archetype-pl ...
- 【Maven实战技巧】「插件使用专题」Maven-Assembly插件实现自定义打包
前提概要 最近我们项目越来越多了,然后我就在想如何才能把基础服务的打包方式统一起来,并且可以实现按照我们的要求来生成,通过研究,我们通过使用maven的assembly插件完美的实现了该需求,爽爆了有 ...
- 「快速学习系列」我熬夜整理了Vue3.x响应性API
前言 Vue3.x正式版发布已经快半年了,相信大家也多多少少也用Vue3.x开发过项目.那么,我们今天就整理下Vue3.x中的响应性API.响应性APIreactive 作用: 创建一个响应式数据. ...
- Python(三)基础篇之「模块&面向对象编程」
[笔记]Python(三)基础篇之「模块&面向对象编程」 2016-12-07 ZOE 编程之魅 Python Notes: ★ 如果你是第一次阅读,推荐先浏览:[重要公告]文章更新. ...
- 实战java虚拟机的学习计划图(看懂java虚拟机)
啥也不说了,实战java虚拟机,好好学习,天天向上!针对自己的软肋制定学习计划. 一部分内容看完,自己做的学习笔记和感想. 学java很简单,但懂java会有难度,如果你的工资还没超过1W,那是时候深 ...
- DDD实战课--学习笔记
目录 学好了DDD,你能做什么? 领域驱动设计:微服务设计为什么要选择DDD? 领域.子域.核心域.通用域和支撑域:傻傻分不清? 限界上下文:定义领域边界的利器 实体和值对象:从领域模型的基础单元看系 ...
- [译]聊聊C#中的泛型的使用(新手勿入) Seaching TreeVIew WPF 可编辑树Ztree的使用(包括对后台数据库的增删改查) 字段和属性的区别 C# 遍历Dictionary并修改其中的Value 学习笔记——异步 程序员常说的「哈希表」是个什么鬼?
[译]聊聊C#中的泛型的使用(新手勿入) 写在前面 今天忙里偷闲在浏览外文的时候看到一篇讲C#中泛型的使用的文章,因此加上本人的理解以及四级没过的英语水平斗胆给大伙进行了翻译,当然在翻译的过程中发 ...
- 重学 Java 设计模式:实战备忘录模式「模拟互联网系统上线过程中,配置文件回滚场景」
作者:小傅哥 博客:https://bugstack.cn - 原创系列专题文章 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 实现不了是研发的借口? 实现不了,有时候是功能复杂度较高难以实 ...
- 重学 Java 设计模式:实战状态模式「模拟系统营销活动,状态流程审核发布上线场景」
作者:小傅哥 博客:https://bugstack.cn - 原创系列专题文章 沉淀.分享.成长,让自己和他人都能有所收获! @ 目录 一.前言 二.开发环境 三.状态模式介绍 四.案例场景模拟 1 ...
随机推荐
- Windows系统中下载Earthdata数据
总的来说,为四大步: 1.注册Earthdata用户. 注册时需注意的是,最好把所有需打勾的都勾上,在最后[注册]按钮前,弹出[人机验证]才能注册成功.如果注册不成功,除了检查用户名和密码是否符合要求 ...
- eclipse导出jar(java打包导出jar)
有时候需要将j2se工程导出,这样可以在别处运作,就不必拘泥于开发感觉中才能运行了.具体做法如下:方法一:(工程没有引用外部jar包时,直接导出) 选中工程---->右键,Export...-- ...
- windows下python IDE安装注意事项&Python安装及编辑器UliPad安装
python下载地址: http://www.python.org/download/releases/2.7.6/ 我自己用的是ulipad ,但是注意 ulipad和python的版本一定要配 ...
- 使用Git上传文件到github
第一次利用git连接github时往往都不会勾选Initialize this repository with a README,这样的的确确是简单了,但是如果我们需要勾选,勾选了之后应该怎么办呢?1 ...
- pycharm启动vue项目
一 移动vue项目问题 1 移植到其他windows or mac 2 重新安装依赖 基于情况2(我们把除了第一个文件都复制到一个新的文件夹) 重新依据配置文件去安装各种各样的依赖(也就是根据配置文件 ...
- 一张图讲解单机FastDFS图片服务器安装步骤(修订版)
前面已经讲 一张图秒懂微服务的网络架构,通过此文章可以了解FastDFS组件中单机安装流程. 单机版架构图 以下为单机FastDFS安装步骤 一.环境准备 CentOS 7.X libfastcomm ...
- 【搞定面试官】try中有return,finally还会执行吗?
本篇文章我们主要探讨 一下如果try {}语句中有return,这种情况下finally语句还会执行吗?其实JVM规范是对这种情况有特殊规定的,那我就先上代码吧! public class Final ...
- 关于iframe/子窗体与父窗体的交互
父子窗体交互方式 通过contentWindow交互 通过postMessage交互 通过contentWindow交互 主窗体内嵌的iframe或者是其通过js打开的新窗口都可以通过contentW ...
- 技术谈 | SDN 和 NFV 之间的爱与恨
部分开发者经常混淆 SDN 和 NFV,无法看清他们的关系.今天,小编搬出华为技术专家的一篇大稿,给大家掰扯掰扯:SDN 和 NFV 究竟是什么关系. ----文/闫长江 什么是 SDN 回到基本的概 ...
- Windows下创建Python虚拟环境的两种方法:
在实际的项目开发中,我们会根据自己的需求去下载各种相应的框架库,但是每个项目可能使用的库不一样,或者版本不一样等等等.为了避免这些因素对我们的项目造成一些不必要的影响,我们可能需要来回的切换或者装卸等 ...