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

简介:

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





本文转自田园里的蟋蟀博客园博客,原文链接:http://www.cnblogs.com/xishuai/p/ddd-design-two-entities.html,如需转载请自行联系原作者

相关文章
|
9月前
|
人工智能 算法 机器人
开源极客桌面机器人 Desk-Emoji
Desk-Emoji 是一款开源的实体 AI 桌面陪伴机器人,具备酷炫外观、流畅 Emoji 表情、双自由度云台及大模型语音聊天功能,支持手势识别和情绪反馈,适合 DIY 和二次开发,是性价比极高的桌面机器人。
1994 1
开源极客桌面机器人 Desk-Emoji
|
10月前
|
API
国家名称大全免费API接口教程
此API提供全球国家信息查询服务,包括坐标、中英文名称、简称及域名后缀。支持POST/GET请求,需提供用户ID和KEY。返回状态码、消息内容及结果集。示例URL:https://cn.apihz.cn/api/other/country.php?id=88888888&key=88888888。详情见:https://www.apihz.cn/api/country.html。
305 15
|
10月前
|
人工智能 自然语言处理 测试技术
文生图参数量升至240亿!Playground v3发布:深度融合LLM,图形设计能力超越人类
【10月更文挑战第29天】Playground v3(PGv3)是最新发布的文本到图像生成模型,其在多个测试基准上取得了最先进的性能。与传统模型不同,PGv3采用了一种全新的结构,将大型语言模型与图像生成模型深度融合,展现出卓越的文本提示遵循、复杂推理和准确的文本渲染能力。此外,PGv3还具备超越人类的图形设计能力,支持精确的RGB颜色控制和多语言理解,为设计师和创意工作者提供了强大的工具。尽管存在一些挑战,但PGv3的发布标志着文本到图像生成技术的重大突破。
186 6
|
11月前
|
人工智能 监控 IDE
利用AI进行代码生成:开发新纪元
【10月更文挑战第9天】人工智能在软件开发领域的应用日益广泛,特别是AI驱动的代码生成技术。本文介绍了AI代码生成的原理、核心优势及实施步骤,探讨了其在自动补全、代码优化和快速原型开发中的应用,并提供了实战技巧,旨在帮助开发者高效利用这一技术提升开发质量和效率。
|
安全 编译器 Go
什么是 CGO?什么时候会用到它?
【8月更文挑战第31天】
1194 0
|
算法 关系型数据库 数据库
德哥的PostgreSQL私房菜 - 史上最屌PG资料合集
看完并理解这些文章,相信你会和我一样爱上PostgreSQL,并成为PostgreSQL的布道者。 沉稳的外表无法掩饰PG炙热的内心 。 扩展阅读,用心感受PostgreSQL 内核扩展 《找对业务G点, 体验酸爽 - PostgreSQL内核扩展指南》https://yq.
58382 128
|
Kubernetes 容器
k8s集群状态全部为NotReady , 已解决
等待一会 , 执行命令kubectl get nodes 状态就好了
510 0
|
算法 C语言
C语言中的数字整除问题居然如此简单
C语言中的数字整除问题居然如此简单
|
弹性计算 负载均衡 网络协议
云平台网络架构以及相关产品的介绍(一)| 学习笔记
快速学习云平台网络架构以及相关产品的介绍。
2852 3
云平台网络架构以及相关产品的介绍(一)| 学习笔记
|
存储 安全 算法
面试官 - https 真的安全吗,可以抓包吗,如何防止抓包吗
面试官 - https 真的安全吗,可以抓包吗,如何防止抓包吗