驾一叶之扁舟 举匏樽以相属
寄蜉蝣于天地,渺沧海之一粟。哀吾生之须臾,羡长江之无穷。
挟飞仙以遨游,抱明月而长终。知不可乎骤得,托遗响于悲风。

从壹开始微服务 [ DDD ] 之三 ║ 简单说说:领域、子域、限界上下文

前言

哈喽大家好,DDD领域驱动设计系列又开始了,前天周二的那篇入门文章中,也收到了一定的效果(写小说的除外),同时我也是倍感鸭梨,怎么说呢,DDD领域驱动设计已经有十年历史了,甚至更久,但是包括我在内的一批技术人员还是对其不是很明白,这几天我也是日思夜想,怎样才能说的明白,怎样才能把这个高高在上的思想落在实践上,可惜的是国内栗子比较少,国外文章比较少,只能硬啃了,所以更需要大家一起来讨论,这里要说一下,是一起讨论推动,而不是内心去拒绝,而一直和多层架构做对比,这样不仅不利于学习,也无法带动我的积极性,所以,这里恳请大家,多多评论,多多交流,比较我一个人很难扛得动这个DDD的大旗。

好啦,言归正传,上次咱们说到了《[ DDD ] 之二 ║ DDD入门 & 项目结构粗搭建》,其中主要说明了为什么使用DDD,以及如何简单的搭建一个基于领域的粗略层,颗粒度还是项目级别,还没有继续往下深究,今天呢,咱们就往下慢慢走,说一说整个项目下,是如何实现领域设计的。

这里先给大家提一个问题,如果一个新的项目,比如一个小的问答系统交给你的手里,PM 刚刚和你简单的讨论了下需求,下一步你打算做些什么?

1、根据需求,立刻准备设计数据库,建表,脑中模拟场景;

2、根据需求,立刻建立实体类(也就是model层),然后CodeFirst 生成数据库;

3、找寻该领域专家(做过或者懂得类似产品的人),设计该问答领域下,有哪些子领域,制作限界上下文;

4、啥都没有,直接网上找开源项目,下载下来看看;

老张说:这里没有正确与否的比较,只是一个习惯和优劣的分析,不用太在意,如果你比较好奇,那就往下看吧。

 

零、今天要完成绿色的部分

 

一、领域 —— 就是一个独立项目

1、领域的概念

 这个概念相信很多人已经很明白了,而且也听到了无数遍,这里就再简单的说两句:

领域(Domain)其实就是一个组织所要做的整个事情,已经这个事情下所包含的一切内容。这是一个范围概念,而且是面向业务的(注意这里不是面试技术的,更不是面向数据库的持久化的),每个组织都有自己的人员、自己的工作业务范围和做事方式,当你为该组织开发软件的时候,你面对的就是这个组织的领域。

就比如之前我在一家旅游公司进行开发工作,那我所进行的开发工作就是一个旅游行业,我必须要很清晰旅游行业的其中的领域知识,而且必须能和领域专家通过通用性语言进行沟通,这样能保证我开发出来的是他们想要的,而不是我单纯的从技术上实现,在领域设计上一塌糊涂。当然我们每天也都在做这样的事情,也许你感觉很正常,那我再举个例子:

我在开发其中一个目的地(旅游景点)项目的时候,这是一个领域,后来在电商系统项目中,又是一个领域,但是在电商领域中,涉及到了景点领域的一些数据,那我如果不和领域专家沟通,有时候为了贪图技术上的方便,甚至把两个领域合并成一个,虽然都不大,合并以后大小也还可以,但是这样却完全打破了领域的这个概念,这个就是完全面向技术开发的,因为领域专家看不懂我这么写到底属于什么。

当然上边的栗子有点儿牵强,咱们再说下以后我想做的一个基于DDD的问答项目,咱们先画一个框。

就如图所示,咱们首先定义一个边界,至于里边有什么东西,咱们接着往下看,这个很简单。

2、如何定义一个领域

这个是更简单的一个问题,在领域设计中,有两个方法:战略设计和战术设计,其实我个人感觉可以定义两步走,这两个是有先后之分的,

战略设计中定义了,一个领域就是一个问题空间,我们在业务中所遇到的所有的问题与挑战;

在战术设计中,一个领域就算一个解决问题空间,用来解决在问题空间的所有问题;

所以,其实一个领域就是一个我们建立的一个解决方案,一个项目,在我们的问答项目中,整个解决方案就是一个问答领域。

 

二、子领域 —— 具体的项目实现

1、子域 / 核心子领域 / 通用子领域

什么是子域(SubDomain)呢?这个很好理解,就是在整个领域中,我们如何对其进行拆分,然后满足我们的业务逻辑。一个子域可能是一个 dll ,一个命名空间的形式存在。

我们定义好领域,并且划分好限界后,就开始考虑如何进行实现,这里大家想一想如何设计与划分,这里就说说我自己的之前的想法:

在我们的问答领域设计中,我们的思路一定是有客户来 =》验证是否有发问题的权限 =》 然后发布一个问题 =》

这仅仅是一个发布问题的流程,也仅仅是一个顾客认证的过程,很简单,我们一般会怎么分子领域呢,可能会这么分,这个就是 消息发布子领域,里边有我们的发布模型,用户模型,讨论模型,日历模型等等,大概就是这个样子

因为我们会这么想:“用户和权限这两个模型,和我们的消息子领域有何紧密的关系,你看,发布+回复+讨论+日历(指自己新建一个日历功能,具体待定),这些模型肯定都需要用户登陆认证吧,甚至有些是需要授权的,分在一个子领域有什么不对么?”,这样的代码逻辑应该是这样的

 

如果是你看到这里,首先明白了什么是子领域了吧,也知道如何划分了,但是你感觉这个划分对么? 如果你感觉很正常,那就请往下看吧。

 

2、核心子领域 / 通用子领域 / 支撑子领域

我们再来分析一下,我们的问答领域中的有哪些内容,首先:肯定有消息发布子领域,这个也是上边说到的,这个毋庸置疑,一个问答系统,消息发布是肯定的(这里说明下:发布问题,回答问题,讨论问题等都属于一个消息的发布,这个应该理解),而且这个子领域是缺少它不可的,这个就是我们的核心子领域

再来看看,还有一些其他的,比如日志记录,数据操作痕迹记录(哪个管理员修改了哪些数据),这些子领域贯穿着我们真个领域系统,被其他领域共用,我们称之为 通用子领域

当然,我们还有一些站内的即时消息,wiki百科,通知提醒,活动跟踪,等等,这些都不是我们的核心子域,因为没有这些,我们依然可以进行问答,但是这些确是支撑着我们核心子域的相关功能,我们就把这些命名为 支撑子领域,这个时候你会问,这些支持子领域要不要再拆开,我个人表示没有很大的必要。

最后我们再来看看我们上边的用户认证授权问题,在上边我们把他们柔和到了消息核心子域里,但是这里要说明,这两者是没有关系的:

为什么没有关系呢?诚然,我们的软件是必须有用户参与的,但是我们应该将不同的用户种类区别对待,因为在不同的上下文(下边会说到)中,他们的作用和任务是不一样的,在消息核心子域中,我们关注的是角色,不管他是谁或者有什么权限,如果我们有一天把权限模型修改了,那我们的问答模型也一定要修改,你想想是不是,因为两者业务逻辑已经耦合了!

这个时候我们应该明白,发布信息和“谁可以发,在什么条件下发”其实没有太大的关系,我的问答,只关心的是“有一个顾客发布了一个问题”这样就可以了,我们关心的是发布消息这个过程,而不能把用户权限涉及进来,这个时候我们应该把用户权限单拿出来一个子领域,就叫安全子领域。

 

3、隔离内核

其实上边说的可能有点儿朦胧,但是我们应该都已经用到了,如果你看了我的上一个系列教程,你应该知道有一个JWT权限验证那一章节,很多人就是不很理解,是如何进行授权验证的,其实采用的就是隔离内容,以前我们写逻辑,就算直接在控制器里,判断当前用户权限,但是现在我们是通过一个中间件,判断 Token 所包含的用户Role 是否有这个权限,再进行下一步,只不过在DDD中,把这一块单拿出来形成了一个安全子领域了,这个时候你应该明白了吧。

        /// <summary>
        /// 删除一个顾客信息
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        [HttpPost, ActionName("Delete")]
        [Authorize(Policy = "CanRemoveCustomerData")]
        [Route("customer-management/remove-customer/{id:guid}")]
        [ValidateAntiForgeryToken]
        public IActionResult DeleteConfirmed(Guid id)
        {
            _customerAppService.Remove(id);

            if (!IsValidOperation()) return View(_customerAppService.GetById(id));

            ViewBag.Sucesso = "Customer Removed!";
            return RedirectToAction("Index");
        }

 

这个时候,可能还不是很明白,为什么好好的程序要拆分,这么做的目的又是为了什么,直接在需要用到的权限的地方写业务逻辑不就行了么,这个往下看,咱们说说限界上下文。

 

三、限界上下文 —— 领域模型的边界

 1、限界上下文是显示的,有语义的

限界上下文(Bounded Context)定义了每个模型的应用范围,在每个Bounded Context中确保领域模型的一致性。不同的限界上下文中,领域模型可以不用保证一致性。通常我们根据团队的组织、软件系统的每个部分的用法及物理表现(如组件划分,数据库模式)来设置模型的边界。

概念还是有点儿朦胧,那就举例来说:

在电商系统中,销售子域是核心域,商品子域和物流子域为支撑子域。在这三个子域中,都要和商品打交道。如果把商品抽象为Product对象的话,按我们一般的常规思路(抛开子域的划分)来说,不管是商品销售还是发货,我们都可以共用同一个Product对象。
但在DDD中,在商品子域和销售子域中,可以共享这个Product对象,但在物流子域,就有点大材小用。为什么呢?因为毕竟物流子域关注的是商品的发货处理和物流跟踪。针对发货流程而言,我只关心商品的数量、大小、重量等规格,而不必了解商品的价格等其他信息。所以说物流子域应该关注的是货物的发货处理而不是商品。
那为什么我们之前的开发思路会共用同一个Product对象呢?
答案很简单,没有进行领域的划分。把整个项目一概而论,统一建模导致的结果。
在DDD的思想下,当划分子域之后,每个子域都对应有各自的上下文。在销售子域和商品子域所在的上下文语境中,商品就是商品,无二义性。在物流子域的上下文语境中,我们也可以说商品的发货处理,但这时的商品就特指货物了。确定了真实面目之后,我想我们也会不由自主的抽象一个新的Cargo对象来处理物流相关的业务。这也是DDD带来的好处,让我们更清晰的建模。

 

2、定义限界上下文

 在我们上边的子域定义中,我们出现了三个子域,我这里同时也定义了三个限界上下文(这里说下,两者不是一对一的关系),总体来说,我们不应该按技术架构或者开发任务来创建限界上下文,应该按照语义的边界来考虑。

我们的实践是,考虑产品所讲的通用语言,从中提取一些术语称之为概念对象,寻找对象之间的联系;或者从需求里提取一些动词,观察动词和对象之间的关系;我们将紧耦合的各自圈在一起,观察他们内在的联系,从而形成对应的界限上下文。形成之后,我们可以尝试用语言来描述下界限上下文的职责,看它是否清晰、准确、简洁和完整。简言之,限界上下文应该从需求出发,按领域划分。

 

 

3、上下文都包含哪些内容

一个限界上下文不是只有领域模型,当然这个是必不可少的,它总体来说是一个系统,一个应用程序,或者一个业务服务,它里边会有实体,值对象,领域事件(一个个方法事件组成,比如用户注册,修改密码,验证信息等等都是该上下文中的领域事件),在我们的身份和访问上下文中,是这样定义的

 

感觉写到这里还是没有写的很透彻,因为我们还没有涉及到代码,可能通过代码的设计会比较好。

 

四、结语

 本文主要是通过DDD领域设计的思想,来说明如何对一个项目进行细分的过程,这个再想想文章开头提出的问题,是不是稍微有些感触,只不过在没有代码的讲解下,一起总是很空洞,下次咱们直接通过基础设施层中的上下文定义,来进一步了解领域设计的思想吧。 

 

posted @ 2018-10-25 15:40 老张的哲学 阅读(...) 评论(...) 编辑 收藏
作者:老张的哲学
好好学习,天天向上