在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题:
谈谈你的DDD落地经验?
谈谈你对DDD的理解?
请用DDD把一个模块设计出来
就在前天,一个35岁的小伙伴技术2面试,遇到一个DDD建模面试难题:
并且是上机实操,不是讲理论:
尼恩给他做了 30分钟51秒的紧急语音指导
小伙伴顺利通过面试
由于尼恩的《DDDD面试圣经》以及配套视频,还在写, 咱们没有发布。
这里,借助腾讯架构师faryrong 的文章《万字长文助你上手软件领域驱动设计 DDD》,给大家做一下系统化、体系化的DDD梳理,使得大家可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”。
也一并把这个题目以及参考答案,收入咱们的 《尼恩Java面试宝典PDF》V147版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到文末公号【技术自由圈】获取
DDD如此之香,那么多大厂对DDD如此痴迷, 背后 有深层次、根本性的原因
具体原因,参见尼恩在《DDD学习圣经》为大家深度总结的、下面的6点:
DDD现在非常火爆,是有其巨大生产价值,经济价值的, 绝不仅仅是一套概念那么简单。
DDD的绝大价值,具体请参见以下视频:
从腾讯视频DDD重构案例,看看DDD极大价值
DDD未来大势所趋,是大家 明年3月面试,所需要必须掌握的 核心经验、 重点经验。
尼恩会结合一个工业级的DDD实操项目,在第34章视频《DDD的学习圣经》中,给大家彻底介绍一下DDD的实操、COLA 框架、DDD的面试题。
作者:faryrong,腾讯 CSIG 后台开发工程师
领域驱动设计(Domain-Driven Design,简称DDD)是由Eric Evans提出的,并在其著作《领域驱动设计:软件核心复杂性应对之道》发布后,在软件行业内产生了巨大影响。DDD提供了一种独特的思维方式,它倡导开发者应该用业务方案来解决业务问题,而不是单纯依靠技术方案。这种理念仿佛是魔法对抗魔法。尽管DDD的理念在当时看起来有些违反直觉,但并没有立即流行开来。
随着微服务架构的兴起,人们惊讶地发现DDD可以作为划分微服务边界的有力工具,并且DDD的许多设计理念与微服务架构高度契合。因此,DDD逐渐被开发者们接受并流行起来。现在,了解和学习DDD已经成为软件行业从业者的一门必修课。
但是,DDD的学习曲线相对较陡峭。我阅读过许多相关书籍、KM文章和分享,但始终感觉难以掌握其精髓。原因有两点:一是DDD涉及的概念众多,且抽象层次不同,如子域和限界上下文,它们在问题归类和收敛方面的区别是什么?二是缺乏过程指导,难以将概念有序地串联起来。虽然DDD提供了设计思想、核心原则和常用工具,但缺乏具体的方法步骤,使得实践变得困难。
幸运的是,最近看了一本书《解构-领域驱动设计》。这本书提出了领域驱动设计统一过程(DDDRUP),它指明了实践 DDD 的具体步骤,并很好地串联了各种概念、模式和思想。因此,我对书本内容做了梳理、简化,融入自己的理解,并结合之前阅读的书籍以及实践经验,最终形成这篇文章。希望通过这篇文章,能够帮助大家理清DDD的概念、模式和思想,降低学习DDD的门槛。
在未来的软件开发过程中,领域驱动设计将发挥越来越重要的作用。掌握DDD的理念和方法,将有助于我们更好地应对软件行业的挑战。
经典必读书籍《领域驱动设计:软件核心复杂性应对之道》的书名包含了两个关键词**:领域驱动和复杂性,**分别代表了 DDD 的核心原则以及解决的问题。
系统的复杂性通常源于领域本身、用户行为或业务流程,并非仅仅是技术问题。若在设计阶段未能妥善处理这些领域性难题,即便技术设想再精妙,也难以奏效。而系统的复杂度体现在三个方面**:规模**、结构和变化。
规模:规模涉及系统支持的功能点及其相互关系。利用领域驱动设计(DDD)中的子领域、界限上下文和聚合等概念,我们能够对问题进行细分和归类,从而缩小问题范围,确保聚合边界内的问题解决更加集中和可控。
结构:结构关注的是系统架构,包括是否分层、各层职责是否分明以及基本管理单元的性质。这些因素影响架构演化的复杂度。DDD推崇分层架构,将领域层独立出来,并为每一层分配明确职责。聚合作为基本管理单元,是独立和自包含的,服务拆分时,可 以聚合为单位进行分割。
变化:变化指的是系统对需求变更的适应能力。分离不变和易变逻辑是有效管理变化的关键。领域层通过分层架构成为不变逻辑的独立部分,它包含了领域知识,提供的领域服务反映了经验和远见,是对稳定领域规则的表达。领域层之外的应用层和基础设施层则包含易变逻辑,确保核心稳定的同时,通过调整这两层来迅速适应需求变化。
实践中,我深刻体会到DDD理念和方法在应对软件复杂性方面的重要性。应用DDD的设计模式和原则,我能更有效地分解和归类问题,构建出更清晰、易于维护和扩展的系统架构。同时,我也意识到领域驱动设计不只是技术手段,更是一种思考模式,它促使我们深入理解业务领域,从业务视角出发思考问题,用业务语言沟通,从而更有效地解决业务挑战。
领域导向设计(Domain-Driven Design, DDD)是一种以业务领域为核心的软件设计方法。它强调在满足业务需求时,首先应抽象出领域概念,并基于这些概念构建领域模型来刻画业务问题。在这个过程中,应尽量推迟技术细节的介入。编码阶段则是对领域模型的逐行实现,代码应能够清晰地表达领域概念,让人见码明义。
根据实践经验,以下是本人对“领域驱动”的一些见解:
思维模式转变
在实践 DDD 以前,我最常使用的是数据驱动设计。它的核心思路针对业务需求进行数据建模:从业务需求中提炼出类,然后通过 ORM 将类映射为表结构,并根据读写性能要求使用范式优化表与表之间的关联关系。数据驱动设计是从技术角度解决业务问题,得出的数据模型是对业务需求的直接翻译,并没有蕴含稳定的领域知识/规则。一旦业务需求发生变化,数据模型就必须调整,数据库设计也随之改变。这种设计思维导致业务变化直接影响到数据层,缺乏一个稳定且不易变的中层来缓冲变化,从而影响了系统对变化的响应能力。
总结来说,领域导向设计不仅仅是一种技术上的转变,它更是一种思维模式上的革新。它要求我们深入理解业务领域的本质,从业务的角度出发思考问题,用业务的语言进行沟通。通过这种方式,我们能够构建出更加稳定、更能适应业务变化的软件系统。领域导向设计的核心在于建立一套能够反映业务领域知识和规则的模型,这套模型是稳定的,能够在业务需求变化时提供不变的基础,从而使得技术实现能够更加灵活地适应这些变化。
协同方式转变
过去,由产品同学提出业务需求,研发同学根据业务需求的 tapd 进行技术方案设计,并编程实现。
这种协同方式的弊端在于**:无法形成能够消除认知差异的模型**。产品经理从业务视角出发提出用户需求,这些需求可能频繁变化且高度定制化。与此同时,研发人员在缺乏行业经验的情况下,往往倾向于直接将需求转换为数据模型。研发团队从技术实现的角度出发设计技术方案,涉及众多技术细节,而产品经理难以判断这些方案是否与自己的业务诉求和产品规划一致,从而导致认知差异。随着迭代的进行,这种差异会不断加剧,最终导致系统变得复杂难以维护。
注意:请点击图像以查看清晰的视图!
DDD 通过解锁新角色**”领域专家"以及模型驱动设计**,有效地减少了产品和研发之间的认知差异。领域专家拥有丰富的行业经验和深厚的领域知识,他们能够从变化多端和定制化的需求中提炼出清晰的边界,稳定且可复用的领域概念和业务规则,并与产品和研发团队合作,共同构建领域模型。领域模型是对业务需求的知识表达,它不涉及具体技术细节(但能指导研发人员进行编程实现),从而消除了产品和研发在需求理解上的分歧。模型驱动设计要求领域模型与业务需求和编码实现紧密关联,模型的任何变更都意味着需求变更和代码变更,协作的核心围绕着模型展开。
总结来说,DDD的实施不仅仅是一种技术上的改进,它更是一种协作模式的创新。通过领域专家的介入和模型驱动设计的方法,我们能够构建出一个更加清晰、更能适应业务变化的软件系统。这种模式要求我们深入理解业务领域的本质,从业务的角度出发思考问题,用业务的语言进行沟通。通过这种方式,我们能够建立起一个稳定、能够灵活适应业务变化的软件系统。领域模型成为了协作的中心,它不仅仅是一个技术工具,更是一个沟通平台,确保了产品和研发的紧密合作,共同应对业务挑战。
注意:请点击图像以查看清晰的视图!
精炼循环
精炼迭代是指在统一语言、提炼领域概念、界定边界、构建模型、实现绑定这一系列过程中,各环节相互作用与反馈,通过不断的试错、调整,最终形成一个稳定且深层次的领域模型。例如,在领域概念提炼过程中,若发现统一语言定义存在不合理或歧义,将调整统一语言定义,并重新进行领域概念的提炼。通过精炼迭代,我们逐步构建出稳定的领域模型。在DDD中,领域专家主导概念提炼、边界划分等宏观设计,这是因为领域专家的经验和行业洞察力来源于过去无数次的精炼迭代,因此,这些宏观设计推导出的领域模型通常非常稳定。
精炼迭代的关键在于循环,它确保知识在各个方向上流动,防止因环节上的认知差异导致模型在产品、领域专家和研发之间无法达成一致,以及模型与实现之间的割裂。
实践中,我深刻认识到领域驱动设计方法在解决软件复杂性方面的重要性。运用领域驱动设计的原则和模式,我能更有效地拆分和归类问题,设计出更清晰、易于维护和扩展的系统架构。同时,我也意识到领域驱动设计不只是一种技术方法,更是一种思维方式,它要求我们深入理解业务领域,从业务角度思考问题,用业务语言沟通,从而更好地解决业务问题。
总结来说,精炼迭代是一种在不断反馈和调整中逐步完善领域模型的过程。它强调领域专家的参与和主导,以确保模型稳定且具有行业洞察力。在实践中,这种方法帮助我们设计出更符合业务需求、更易于维护和扩展的系统架构。同时,它也促使我们以业务为中心,深入理解业务领域,用业务语言沟通,从而更好地解决业务问题。精炼迭代不仅是一种技术手段,更是一种思维方式,它引导我们以更全面、更深入的视角看待软件复杂性,从而找到更有效的解决方案。
我早期实践 DDD 的时候,认为代码分层遵循四层架构就是 DDD,抑或分离接口和实现,实现下沉至基础设施层就是 DDD,实则不然。结合上述内容,目前个人认为只要满足以下条件即为实践 DDD:
问题空间和解空间并非 DDD 特有的概念,而是人们为了区分真实世界和理念世界而提出的概念。问题空间表示的是真实世界,是具体的问题和用户的诉求,而解空间则是针对问题空间求解后构建的理念世界,其中包括了解决方案、模型等。
DDD 提出的战略设计覆盖了问题空间和解空间,而战术设计则聚焦在解空间上。明确 DDD 中的概念是作用于问题空间还是解空间,更有助于我们理解它们。
学生管理系统(Student Management System,下文简称 SMS)作为 DDDRUP 的讲解示例,以下为其问题空间的描述。
学校需要构建一个学生管理系统(Student Management System, SMS)。
通过这个管理系统,学生可以进行选课,查询成绩,查询绩点。
而老师则可以通过这个系统录入授课课程的成绩。录入的分数会由系统自动换算为绩点,规则如下:若分数>= 90,绩点为4.0;90>= 分数> 80,绩点为3.0;80 >= 分数 > 70,绩点为2.0;70 >= 分数 >= 60,绩点为1.0;成绩< 60,则没有绩点,并邮件通知教务员,由教务员联系学生商榷重修事宜。
成绩录入后的一周内,若出现录入成绩错误的情况,老师可提交修改申请,由教务员审核后即可完成修改。审核完成后系统会通过邮件告知老师审核结果。一周后成绩将锁定,不予修改。成绩锁定后,次日系统会自动计算各年级、各班的学生的总绩点(总绩点由各门课程的学分与其绩点进行加权平均后所得)。
而教务员则可以通过该系统发布可以选修的课程。同时,教务员能够查看到各年级,各班的学生的总绩点排名。
在我看来,领域驱动设计是一种思维方式,它强调从业务领域出发,以领域知识为核心,构建出稳定、可复用的模型。这种设计方法不仅有助于提高系统的可维护性和可扩展性,还能有效地降低产品、领域专家和研发人员之间的认知差异,促进协作。
尽管领域驱动设计将设计过程分为战略设计和战术设计,并提供了一系列模式和工具,但一直没有一个统一的过程来规范这两个阶段需要执行的活动、交付的成果以及阶段性的里程碑。此外,这两个阶段之间的衔接以及执行的工作流程也缺乏明确的定义。
《解构-领域驱动设计》一书提出的 DDDRUP 为我们提供了更详细的步骤、各步骤之间的衔接,以及明确的阶段里程碑。最重要的是,DDDRUP 能够将 DDD 的所有概念和模式串联起来,非常便于初学者做知识梳理和上手实践。下文我会依照 DDDRUP 的步骤流程进行讲述,而非战略设计+战术设计的思路。(DDDRUP 各步骤与战略&战术设计的关系见下表)。
总的来说,领域驱动设计统一过程(DDDRUP)为我们提供了一种规范化、结构化的领域驱动设计方法,有助于我们更好地理解和应用领域驱动设计,从而提高软件系统的质量和可维护性。
全局分析阶段对问题空间进行的梳理和分析,形成统一语言(ubiquitous language), 获取问题空间的价值需求以及业务需求。
统一语言:蕴含领域知识的、团队内统一的领域术语。由于产品、领域专家和开发人员掌握的领域知识有所差异,往往会导致对同一事物使用不同的术语。例如,商品的价格(Price)和商品的金额(Amount),它们本质上是相同的,但却有不同的术语表示。
统一语言将贯穿 DDDRUP 的整个流程,并在精炼循环过程中不断进行调整,以更好地反映更合适、更深层次的领域知识。
根据业务需求形成的统一语言有助于团队成员对事物的认知达成一致。统一语言可以通过词汇表的形式展示,其中词汇表最好还要包含术语对应的英文描述,以便研发人员在代码层面表达统一语言。示例-SMS 的统一语言词汇表如下。
术语 | 英文描述 | 术语 | 英文描述 |
---|---|---|---|
学生 | student | 分数 | score |
老师 | teacher | 学分 | credit |
教务员 | senator | 排名 | rank |
课程 | course | 年级 | grade |
成绩 | result | 班级 | class |
绩点 | gpa | 学年 | school year |
总绩点 | total gpa | 学期 | semester |
申请单 | application receipt | 修改成绩 | modify result |
查询成绩 | queny result | 提交申请单 | submit application receipt |
统计总绩点 | compute total gpa | 发布课程 | publish course |
查询总绩点 | query total gpa | 查询课程列表 | query course list |
查询排名 | query rank | 查询课程信息 | query course info |
同意申请单 | agree application receipt | 选择课程 | choose course |
拒绝申请单 | reject application receipt | 录入成绩 | enter result |
邮件通知 | notify by mail |
全局分析阶段是领域驱动设计过程中的重要一环,它有助于我们深入理解业务领域,挖掘业务需求,并为后续的设计和实现奠定基础。通过形成统一语言,我们可以确保团队成员在沟通和协作过程中减少歧义,提高工作效率。
在全局分析阶段,我们还需要对业务领域进行细分,划分出不同的子领域,以便在后续的设计过程中更好地组织和管理领域知识。此外,我们还需要识别出领域中的关键概念和关系,为构建领域模型做好准备。
价值需求分析主要做的三个工作是:
并不是所有系统都适合采用 DDD,DDD 的核心是解决领域复杂性。如果系统逻辑简单,功能有限,引入 DDD 可能会带来不必要的成本。通过进行价值需求分析,我们可以判断是否需要通过 DDD 来驱动系统设计。
在软件开发过程中,价值需求分析是一个非常重要的环节。通过对价值需求的分析,我们可以更好地理解目标系统的功能和目标,从而为后续的系统设计和实现提供指导。同时,价值需求分析也有助于我们识别出系统的关键利益相关者,确保系统的设计和实现能够满足他们的需求和期望。
使用业务流程、业务场景、业务服务和业务规则来表示业务需求。
业务流程:表示的是一个完整的、端对端的服务过程。
业务场景:将业务流程根据阶段性的业务目标进行划分,就可以得到业务场景。在示例-SMS 中,老师修改成绩可以分为老师“提交申请单”和教务员“同意申请单”两个场景。
业务服务:角色主动向目标系统发起服务请求,完成一次完整的功能交互,以实现业务目标。角色可以用户、策略(定时任务)或者其他系统,完整则强调的是业务服务的执行序列的所有步骤都应该是连续且不可中断的。业务服务是业务需求分析最核心,也是最基础的单元,而业务流程和业务场景是为了更好地分析出业务服务。在示例-SMS 中的“同意申请单”场景中包含了两个业务服务:教务员“同意申请单”和系统“邮件通知”教务员。
业务规则:它是对业务服务约束的描述,用于控制业务服务的对外行为。业务规则是业务服务正确性的基础。常见的业务规则有:
注意:请点击图像以查看清晰的视图!
业务需求分析是软件开发过程中的重要一环,它帮助我们深入理解业务领域,明确业务需求,并为后续的系统设计和实现提供指导。通过分析业务流程、业务场景、业务服务和业务规则,我们可以更好地理解业务领域的运作机制,为构建高质量的软件系统奠定基础。
通过对业务流程、业务场景和业务服务的整理,我们可以分析出业务需求所需的业务服务。然而,由于业务服务过于细致,而问题空间又较大,我们需要找到一个更粗粒度的业务单元,以便对业务服务进行分类。这样做一方面可以降低管理大量细粒度业务服务所带来的额外复杂度,另一方面可以帮助领域专家和开发团队在分析问题和设计方案时避免陷入业务细节。这个更粗粒度的业务单元就是子领域。
子领域的作用:
子领域的分类:
子领域的功能分类策略:问题空间应该分为哪些子领域,需要团队对目标系统整体进行探索,并根据功能分类策略进行分解。
划分子领域的过程存在很多经验因素,一个对该行业领域知识了如指掌的领域专家,可以在完成价值需求分析后,结合自身的领域经验,能够选择合适的聚类策略并给出稳定的子领域列表。但,没有领域经验也没有关系!因为根据知识消化循环思路,再经历多个迭代后收敛出来的子领域划分也会逐渐合理,逼急领域专家凭经验得出的子领域划分,只是可能需要的时间要长一些。
子领域的概念和划分方法是为了帮助我们在面对复杂问题空间时,能够更好地对业务服务进行管理和分析。通过将问题空间划分为不同的子领域,我们可以更有针对性地解决问题,提高系统的质量和可用性。在实际应用中,领域专家和开发团队需要根据目标系统的特点和业务需求,灵活运用子领域的划分方法,以实现高质量的系统设计和实现。
在架构映射阶段,我们需要识别限界上下文,并通过上下文映射表示限界上下文之间的协作关系。
限界上下文是语义和语境的边界。在问题空间,统一语言构成了团队对领域概念的统一表达,子领域形成了领域概念之间的边界。而在解空间,限界上下文可以看作是统一语言+子领域的结合体,统一语言在限界上下文内才具有明确的业务含义。
以电商购物场景为例。在进行商品下单后,系统会生成一个订单;在用户付款完成后,系统也会生成一个订单;到了物流派送流程,系统还会生成一个订单。虽然这三个步骤中的领域概念都叫订单,但是他们的关注点/职责却不同:商品订单关注的是商品详情,支付订单关注的是支付金额和分润情况,物流订单关注的是收货地址。也就是说,商品、支付和物流分别为三个限界上下文,而订单作为统一语言需要在特定的限界上下文内,我们才能够明确其关注点/负责的职责。
最小完备:限界上下文在履行属于自己的业务能力时,拥有的领域知识是完整的,无须针对自己的信息去求助别的限界上下文。
自我履行:限界上下文能够根据自己拥有的知识来完成业务能力。自我履行体现了限界上下文纵向切分业务能力的特征。
这里需要强调一下**业务模块(横向切分)和限界上下文(纵向切分)**的区别。业务模块不具备完整、独立的业务能力,它没有按照同一个业务变化的方向进行。而限界上下文是对目标系统架构的纵向切分,切分的依据是从业务进行考虑的领域维度。为了提供完整的业务能力,在根据领域维度进行划分时,还需要考虑支撑业务能力的基础设施实现,如与该业务相关的数据访问逻辑,以及将领域知识持久化的数据库模型,形成纵向的逻辑边界,即限界上下文边界。
注意:请点击图像以查看清晰的视图!
稳定空间:限界上下文必须防止和减少外部变化带来的影响。
独立进化:指减少限界上下文内部变化对外界产生的影响。
上述的四个特征可以帮助我们验证识别出来的限界上下文。限界上下文划分是否合理、职责分配是否合理(最小完备 & 自我履行),是否合理运用上下文映射的手段隔离外部变化的影响(稳定空间)、是否有合理的封装,对外提供的接口是否稳定(独立进化)?
限界上下文是软件架构设计的重要概念,它帮助我们更好地理解业务领域,明确业务边界,并为系统设计提供指导。通过识别和划分限界上下文,我们可以确保系统的高内聚和低耦合,提高系统的可维护性和可扩展性。在实际应用中,开发团队需要根据业务需求和领域特点,灵活运用限界上下文的概念和特征,以实现高质量的系统设计和实现。
1. 归类
按照业务相关性对业务服务进行归类,业务相关性体现为:
2. 归纳
归纳是对归类后的限界上下文进行命名。给限界上下文命名的过程,实际上也是对归类是否合理的再一次复查。限界上下文的命名同样需要遵循单一职责原则,它只能代表唯一的最能体现其特征的领域概念。倘若归类不合理,命名就会变得困难,这时候我们就需要反思(遵循知识消化循环)归类是否合理,并重新设计归类。
3. 边界梳理
归类和归纳之后,限界上下文的边界基本已经确定,边界梳理则是根据限界上下文特征(最小完备、自我履行、稳定空间和独立进化)以及子领域进行微调(当然也不排除大调)。
为什么需要根据子领域进行限界上下文边界的调整?限界上下文和子领域的关系是什么?
理想的限界上下文与子领域的关系是一一对应的。上文提到,子领域是领域专家根据领域经验选择合适的功能分类策略进行划分,这个过程不会牵扯对业务服务的分析,体现的是领域专家对行业的洞见和深刻认识,可见获取子领域是一个自顶向下的过程。而限界上下文则是对业务服务进行归类、归纳、梳理和调整,最终形成一个个的边界,这是一个自下而上的过程。理想情况下,两者应该是双向奔赴的,自顶向下得到的子领域和自下而上得到的限界上下文能够完美契合!但是,现实哪有这么理想呢!所以一般情况下都需要我们进行调整,力求这两者能够一一对应。
这里就再cue一下知识消化循环。优秀的领域专家划分出来的子领域,往往能够实现与限界上下文的一一对应。这就是经验的力量!那经验是怎么来的呢?我认为是领域专家经历了无数个知识消化循环之后沉淀下来的。领域专家一开始也是小白,划分出来的子领域在映射为限界上下文之后发现不同限界之间可能存在语义重叠,角色在不同限界上下文之中履行的职责可能很相似,于是他们通过知识消化循环,不断调整限界上下文的边界,然后又通过限界上下文调整子领域。慢慢地,稳定、可复用的子领域就被沉淀下来了。因此,识别限界上下文不是一个单向的过程,而是一个根据子领域调整限界上下文,然后又根据限界上下文调整子领域的循环的过程。
正交原则
正交性:如果两个或更多事物中的一个发生变化,不会影响其他事物,这些事物就是正交的。要破坏变化的传递性,就要保证每个限界上下文对外提供的业务服务不能出现雷同。
奥卡姆剃刀原理
“如无必要,勿增实体”。这是避免过度设计的良方,同样也是我们识别限界上下文的原则。如果对识别出来的限界上下文的准确性依然心存疑虑,比较务实的做法是保证限界上下文具备一定的粗粒度。遵循该原则,意味着当我们没有寻找到必须切分限界上下文的必要证据时,就不要增加新的限界上下文。
总结来说,限界上下文的识别是软件架构设计的重要环节,它有助于我们更好地理解业务领域,明确业务边界,并为系统设计提供指导。通过识别和划分限界上下文,我们可以确保系统的高内聚和低耦合,提高系统的可维护性和可扩展性。在实际应用中,开发团队需要根据业务需求和领域特点,灵活运用限界上下文的概念和特征,以实现高质量的系统设计和实现。
限界上下文封装了独立的业务能力,上下文映射则建立了限界上下文之间的关系。上下文映射提供了各种模式(防腐层、开放主机服务、发布语言、共享内核、合作者、客户方/供应方、分离方式、遵奉者、大泥球),本质是在控制变化在限界上下文之间传递所产生的影响。
下文将提供服务的限界上下文称为“上游”上下文(U 表示),消费服务的限界上下文称为“下游”上下文(D 表示)。
引入防腐层的目的是为了隔离耦合。防腐层往往位于下游,通过它隔离上游上下文发生的变化。
注意:请点击图像以查看清晰的视图!
开放主机服务定义公开服务的协议(亦称为“服务契约”),包括通信方式、传递消息的格式(协议),使得限界上下文能够被视作一系列服务接口来使用。公开主机服务也可看作一种保证,承诺开放的服务将保持稳定,不易做出变化。
对于进程内的开放主机服务,称为本地服务(对应 DDD 中的应用服务)。
对于进程间的开放主机服务,成为远程服务。根据所选择的分布式通信技术的差异,可以进一步定义出不同类型的远程服务:
注意:请点击图像以查看清晰的视图!
总结来说,公开主机服务是一种服务规范,确保服务的稳定性,无论是进程内部还是进程之间,都可以根据不同的技术选择定义不同类型的远程服务,包括提供者、资源、订阅者和控制器。
发布语言是一种公共语言,,用于在两个限界上下文之间进行模型转换。防腐层和开放主机服务都是在访问领域模型时设置的一层封装,前者针对发起调用的下游(通过基础设施层表现),后者针对响应请求的上游(通过应用层+远程服务),以防止上下游之间的通信集成各自的领域模型,导致彼此之间的强耦合。因此,防腐层和开放主机服务操作的对象都不应该是各自的领域模型,这正是引入发布语言的原因。(对于熟悉云 API 的小伙伴就会发现,其实云 API 根据我们定义的接口生成对应的 Request 对象和 Response 对象,并集成在云 API 的 SDK 中,这些对象就是发布语言)。
通常情况下,发布语言根据开放主机服务的服务契约进行定义。
说到这里,我们惊讶地发现防腐层,开放主机服务和发布语言可以完美联动!
注意:请点击图像以查看清晰的视图!
总结来说,发布语言是一种在两个限界上下文之间进行模型转换的通用语言。防腐层和开放主机服务都是在访问领域模型时设置的一层封装,以防止上下游之间的通信集成各自的领域模型,导致彼此之间的紧密耦合。因此,引入了颁布语言,用于操作对象而不是领域模型。这些概念可以相互配合,实现更好的模型转换和通信。
共享内核指将限界上下文中的领域模型直接暴露给其他限界上下文使用。注意,这会削弱了限界上下文边界的控制力。上面我们讲述的防腐层、开放主机服务以及发布语言无不传达一种思想,限界上下文不能直接暴露自己的领域模型或直接访问其他限界上下文的领域模型,一定要有隔离层!
但是,在特定的场景下,共享内核不见得不是一种合理的方式。**任何软件设计决策都要考量成本与收益,只有收益高于成本,决策才是合理的。**一般对于一些领域通用的值对象是相对稳定的,这些类型通常属于通用子领域,会被系统中几乎所有的限界上下文复用,那么这些领域模型就适合使用共享内核的方式。共享内核的收益不言而喻,而面临的风险则是共享的领域模型可能产生的变化。
注意:请点击图像以查看清晰的视图!
总结来说,共享核心是指将限界上下文中的领域模型直接展示给其他限界上下文使用。虽然这会削弱限界上下文边界的控制力,但在特定情境下,这可能是合理的方法。任何软件设计决策都需要权衡成本和收益,只有在收益大于成本的情况下,决策才是合理的。对于一些领域通用的值对象,它们通常相对稳定,适合使用共享核心的方式。然而,共享核心也面临着共享领域模型可能产生的变化的风险。
合作关系指的是协作的限界上下文由不同的团队负责,且这些团队之间具有要么一起成功,要么一起失败的强耦合关系。合作者模式要求参与的团队一起做计划、一起提交代码、一起开发和部署,采用持续集成的方式保证两个限界上下文的集成度与一致性,避免因为其中一个团队的修改影响集成点的失败。
当一个限界上下文单向地为另一个限界上下文提供服务时,它们对应的团队就形成了客户方/供应方模式。这是最为常见的团队协作模式,客户方作为下游团队,供应方作为上游团队,二者协作的主要内容包括:
分离方式的团队协作模式是指两个限界上下文之间没有一丁点关系。如果此时双方使用到了相似/相同的领域模型,则可以通过拷贝的方式解决,保证限界上下文之间的物理隔离!
当上游的限界上下文处于强势地位,且上游团队响应不积极时,我们可以采用遵奉者模式。即下游严格遵从上游团队的模型,以消除复杂的转换逻辑。
当下游团队选择“遵奉”于上游团队设计的模型时,意味着:
一定要避免制造大泥球!大泥球的特点:
示例-SMS 的限界上下文可划分为:
上下文映射图如下所示。
注意:请点击图像以查看清晰的视图!
领域建模阶段由领域分析建模,领域设计建模和领域实现建模组成。在正式讲解建模活动前,先了解一下什么是模型驱动设计。
模型是知识表达的一种方式,它通过筛选和有目的的结构化处理信息,有效应对信息过载的挑战。这种方法使人们能够抓住信息的关键含义,并集中精力于核心问题。
建模过程一般由分析活动、设计活动和实现活动组成。每一次建模活动都是一次对知识的提炼和转换,并产生相应的模型,即分析模型、设计模型和实现模型。
建模过程并非是分析、设计和实现单向的前后串行过程,而是相互影响,不断切换和递进的关系。模型驱动设计的建模过程是:分析中蕴含了设计,设计中夹带了实现,甚至实现后还要回溯到设计和分析的一种迭代的、螺旋上升的演进过程。
根据分解问题的视角不同,我们日常建立的模型可以大致分为以下三类:
总结而言,模型驱动的设计是一个复杂的迭代过程,涉及分析、设计和实施的不断循环和深入。通过这种方式,我们可以更好地理解和解决问题,将知识转化为有效的模型,并在实际应用中发挥其价值。
一个出色的领域模型应该具备以下的特征(或者说,具备这些属性的就是领域模型):
领域建模阶段目的便是建立领域模型。领域模型由领域分析模型、领域设计模型以及领域实现模型共同组成,它们也分别是领域分析建模、领域设计建模和领域实现建模三个建模活动的产物。
需要强调的是,领域模型不是开发团队独立工作的成果,而是产品经理、领域专家和开发团队共同协作的产物。领域专家利用领域模型来评估系统对领域能力的支持,并根据模型来组织上层的业务能力;开发团队则依据领域模型来构建基础的代码框架(包括确定架构分层,定义每层的接口,接口的命名等)。同样,领域模型的任何调整都意味着领域知识或业务规则的变化,这也预示着系统支持的业务能力和代码实现必须相应地进行更新。
总结来说,领域模型在驱动设计中扮演着至关重要的角色。它不仅作为产品、领域专家和开发团队之间沟通的桥梁,还确保了系统设计和实现与业务需求的一致性。通过领域模型,我们能够确保设计的系统既符合业务的实际需求,又能够在技术层面上实现高效的功能实现。这种模型驱动的 approach 提供了一种结构化和迭代的方法,使得团队能够在面对复杂和不断变化的业务环境时,保持清晰的方向和高效的协作。
领域分析建模:在限界上下文内,以“领域”为中心,提炼业务服务中的领域概念,确定领域概念之间的关系,最终形成领域分析模型。领域分析模型描述了各个限界上下文中的领域概念,以及领域概念之间的关系。
下面讲述如何通过“快速建模法”来构建领域分析模型。
找到业务服务中的名词,在统一语言指导下将其映射为领域概念。
识别动词并不是为领域模型对象分配职责、定义方法,而是将识别出来的动词当做一个领域行为,然后看它是否产生了影响管理、法律或财务的过程数据。如果存在影响,则将这些过程数据纳入领域分析模型作为领域概念。需要注意的是,此处的过程数据是指那些对企业运营和管理产生实质性影响的数据,比如示例-SMS 系统中老师提交修改申请,就会产生申请单这个过程数据,而请求流水记录和任务执行记录并不属于过程数据。动词建模通过分析领域行为是否产生过程数据来揭示潜在的领域概念,从而补充了名词建模的局限。
特别地,对于会产生领域事件的动词,一般可以抽象出一个已完成该动作的状态。
动词建模是领域驱动设计中的一个关键环节,它要求我们超越传统的名词中心视角,转而关注行为和动作。在这个过程中,我们不是简单地为模型对象分配职责或定义方法,而是将动词视为领域行为的表达,探查这些行为是否对企业运营的各个方面产生了实际影响。当这些行为确实引发了诸如申请单这样的过程数据时,我们就将这些数据作为领域概念纳入模型中,从而丰富了我们的领域分析。
除了“名词”和“动词”,概念中其他重要的类别也可以在模型中显式地表现出来,主要包括**:约束和规格**。
约束
约束通常是对领域概念的限定。我们可以将这些约束条件提取到独立的方法中,并通过方法名明确地表达这些约束的含义。比如示例-SMS 中关于 GPA 运算的约束。
有些时候,约束条件无法用单独一个方法来轻松表达,抑或约束条件中会使用到与对象职责无关的信息,那么我们就可以将其提取到一个显式的对象中。
规格(SPECIFICATION)
在很多情况下,业务规则不适合作为实体或值对象的职责,而且这些规则的变化和组合可能会掩盖领域对象的含义。如果将规则从领域层移除,那么领域代码就无法表达这些模型。此时,我们可以定义规格(谓词形式的显式值对象),它用于确定对象是否满足特定的标准。规格将规则保留在领域层,由于规格是一个完整对象,因此这种设计也能更清晰地反映模型。
规格一般有如下三种用法:
规格源自“谓词”概念,因此我们可以使用“AND”,“OR”和“NOT”等运算符对规格进行组合和修改。例如,在SMS系统中,教务员需要查询流程完成的申请单,我们可以通过使用“AND”来组合不同的规格来实现这一目标。
在领域驱动设计中,提取隐式概念是一个关键步骤,它确保了模型能够全面地反映领域的复杂性。通过将约束和规格这些通常隐含在业务逻辑中的概念显式化,我们不仅提高了模型的可读性和可维护性,而且保持了领域规则的一致性和完整性。约束通常是对领域概念的限制,而规格则是用于定义对象是否满足特定标准的规则集。规格的使用可以灵活地应用于验证、选择和创建对象的过程中,而且可以通过逻辑运算符进行组合和修改,以满足复杂的业务需求。通过这种方式,领域模型不仅变得更加精确,而且能够更好地适应业务需求的变化,从而提高了模型的实用性和价值。总之,提取隐式概念是确保领域模型全面性和准确性的重要环节,它使我们能够从更广泛的视角理解和分析企业领域的复杂性。
对于有定语修饰的名词,要注意分辨它们是类型的差异,还是值的差异。如配送地址和家庭地址,订单状态和商品状态。如果是值的差异,类型相同,应归并为一个领域概念(如,配送地址和家庭地址);而类型不同,则不能合并(如,订单状态和商品状态)。
特别是,当名词的修饰语代表不同的限界上下文,且名词本身相同(即名称相同但含义不同的领域概念)时,我们应尽可能调整命名,以确保不同含义的领域概念有不同的名称,这样可以减少不必要的歧义和沟通误解。例如,在特定的限界上下文中,商品的订单和库存的订单都可以称为“order”,但如果我们把库存的订单改为“库存的配送单”(delivery),则可以更清晰地表达其含义。
归纳抽象是领域驱动设计中的一个重要环节,它要求我们仔细分辨名词修饰语所表示的是类型差异还是值差异。通过这种方式,我们可以准确地归纳和抽象领域概念,避免概念混淆和误解。同时,当修饰语表示不同限界上下文时,我们应调整命名以区分不同含义的领域概念,这有助于提高模型的可理解性和可维护性。总之,归纳抽象是确保领域模型清晰和准确的重要步骤,它有助于我们从更广泛的视角理解和分析企业领域的复杂性。
在领域驱动设计中,我们需要根据业务需求和领域知识来判断领域概念之间是否相互关联。特别是对于1:N、N:1、M:N这三种关联关系,我们需要考虑是否可以定义一个新的类型来表征这些关系。例如,如果作品与读者之间存在1:N的关系,我们可以创建“订阅”这一概念来描述这种关联。
在确认关系时,应尽量避免在对象间建立双向关系,即对象A与对象B相关联,同时对象B也与对象A相关联。当两个对象存在双向关系时,会为管理他们的生命周期带来额外的复杂度。我们应该规定一个遍历方向,来表明一个方向的关联比另一个方向的关联更有意义且更重要,例如,在示例SMS系统中,成绩与课程之间存在关联(成绩实例包含课程ID),但课程不会与成绩关联。当然,如果双向关系本身是领域的一个核心概念,那么我们应当保留这种关系。
在领域驱动设计中,确认关系是一个关键步骤,它帮助我们根据业务需求和领域知识来确定领域概念之间的相互关联。对于不同类型的关联关系,我们需要判断是否可以定义新的类型来表征这些关系,以便更准确地描述领域中的交互。同时,我们需要避免在对象间建立双向关系,以减少生命周期管理的复杂性。通过定义明确的遍历方向,我们可以更好地理解和管理对象之间的关联。总之,确认关系是确保领域模型准确和全面的重要环节,它有助于我们从更广泛的视角理解和分析企业领域的复杂性。
通过名词建模,动词建模和归纳抽象后,可提炼出以下领域对象:成绩(Result)、绩点(gpa)、总成绩(total result)、总绩点(total gpa)、学年(school year)、学期(semester)、课程(course)、学分(credit)、申请单(application receipt),邮件(mail),排名(rank),申请单状态(application receipt status)
这些领域对象之间的关系如下图所示。
注意:请点击图像以查看清晰的视图!
领域设计建模的核心工作就是设计聚合和设计服务,在这之前我们需要先了解一下设计要素(实体、值对象、聚合、工厂、资源库、领域服务、领域事件)。
领域驱动设计强调以“领域”为核心驱动力。在构建领域模型时,应尽量规避技术实现的细节限制。然而,在许多情况下,我们仍需思考一些与技术无关的问题:
为了解答上述的四个问题,DDD 提供了很多的设计要素,它们能够帮助我们在不陷入到具体技术细节的情况下进行领域模型的设计。
领域驱动设计(DDD)是一种以领域为中心的设计方法,旨在帮助我们在构建领域模型时,避免陷入技术实现的细节限制。通过理解并运用DDD提供的设计元素,如实体、值对象、聚合、工厂、资源库、领域服务以及领域事件,我们可以在不关注具体技术实现的情况下,更好地设计领域模型。这种方法使我们能够更加专注于领域的本质,从而提高模型的灵活性和可维护性。
7.3.1.1 实体
实体的核心三要素:身份标识、属性和领域行为。
身份标识:身份标识的主要目的是管理实体的生命周期。身份标识可分为:通用类型和领域类型。通用类型 ID 没有业务含义;而领域类型 ID 则组装了业务逻辑,建议使用值对象作为领域类型 ID。
属性:实体的属性描述了主体的静态特征,并持有数据和状态。属性分为:原子属性和组合属性。组合属性可以是实体,也可以是值对象,这取决于该属性是否需要身份标识。我们应尽可能将实体的属性定义为组合属性,以便在实体内部形成各自的抽象层次。
领域行为:展示了实体的动态特征。实体具有的领域行为通常可以分为:
实体是领域模型中的基本构建块,由标识、属性和领域行为三个核心要素组成。标识用于管理实体的生命周期,属性描述实体的静态特征,而领域行为展示实体的动态特征。通过合理地定义实体的属性和领域行为,我们可以更好地描述和处理领域中的问题和场景。这种方法有助于提高领域模型的灵活性和可维护性,从而更好地支持业务的发展
7.3.1.2 值对象
在决定一个领域概念应该使用值对象还是实体类型时,判断依据:
值对象具有不变性。一旦值对象被创建,其属性和状态就不应该再改变,如果需要更新值对象,就通过创建新的值对象来进行替换。
由于值对象的属性是在其创建的时候就完成传入的,那么值对象所具有的领域行为大部分情况下都是“自给自足的领域行为”,即入参为空。这些领域行为一般提供以下的能力。
在进行领域设计建模时,应善于使用值对象而非内建类型来表达细粒度的领域概念。与内建类型相比,值对象的优势包括:
值对象在领域驱动设计中扮演着重要的角色,它们用于表示那些不应该随着时间改变的状态,并且可以提供自我验证、组合和运算的能力。值对象的不变性确保了领域模型的一致性和可预测性,同时它们可以封装领域逻辑,使得领域模型更加丰富和表达性强。在设计领域模型时,我们应该优先考虑值对象,因为它们能够更好地反映出业务领域的本质特征,并且在类型层面提供了对领域概念的直接表达。
7.3.1.3 聚合
聚合的基本特征:
聚合是领域驱动设计中的一个关键概念,它通过实体和值对象的集合来定义一个界限,确保领域概念的完整性和一致性。聚合内的实体作为树状结构的根节点,而外部对象只能通过聚合根来访问,这样维护了聚合内部对象之间的协作和外部对象与聚合之间的清晰界限。
7.3.1.4 工厂
在聚合中,工厂的概念指的是任何封装了聚合对象创建逻辑的类或方法。工厂的具体表现形式有:
注意!这里工厂创建的基本单元是聚合,而非实体,注意与实体中的创建行为区分。
工厂在领域驱动设计中扮演着至关重要的角色,它负责创建和组装聚合对象,确保了聚合的实例化和完整性。无论是通过引入专门的工厂类,还是让聚合自身、服务契约对象或装配器来承担工厂的角色,甚至是使用构建者模式,目的都是将外部请求转换为内部的实体和值对象,从而构建出一个符合领域模型要求的聚合。在这个过程中,聚合被视作创建的基本单元,与实体的创建行为有着本质的区别,这有助于维持领域模型的一致性和清晰性。
7.3.1.5 资源库
资源库是对数据访问的一种业务抽象,用于解耦领域层与外部环境,使领域层变得更为纯粹。资源库可以代表任何可以获取资源的仓库,例如网络或其他硬件环境,而不局限于数据库。
一个聚合对应一个资源库。领域驱动设计引入资源库,主要目的是管理聚合的生命周期。资源库负责聚合记录的查询与状态变更,即“增删改查”操作。资源库分离了聚合的领域行为和持久化行为,保证了领域模型对象的业务纯粹性。
需要注意的是,资源库的操作单元是聚合。在定义资源库的接口时,接口的参数应该是聚合的根实体。如果需要访问聚合内的非根实体,也只能通过资源库获取整个聚合,然后以根实体作为入口,在内存中访问聚合边界内的非根实体对象。
资源库与数据访问对象(DAO)的区别:
根本区别在于,数据访问对象在访问数据时,并无聚合的概念,也就是没有定义聚合的边界约束领域模型对象,使得数据访问对象的操作粒度可以针对领域层的任何模型对象。数据访问对象(DAO)可以自由地操作实体和值对象。没有聚合边界控制的数据访问,会在不经意间破坏领域概念的完整性,突破聚合不变量的约束,也无法保证聚合对象的独立访问与内部数据的一致性。
其次,资源库是基于领域模型对存储系统进行的抽象,因此资源库中的方法命名可以表达领域概念;而数据访问对象(DAO)是存储系统对外暴露的抽象,其方法命名更贴合数据库本身的操作。
资源库在领域驱动设计中起到了桥梁的作用,它连接了领域层和外部环境,确保了领域层的独立性和纯粹性。通过将聚合的生命周期管理和持久化操作从领域行为中分离出来,资源库维护了领域模型的一致性和完整性。与之相比,数据访问对象(DAO)则是一个更为底层的数据操作抽象,它不依赖于聚合的概念,操作粒度更细,但这也使得它在保护领域模型完整性方面不如资源库。因此,在选择数据访问策略时,应当根据领域模型的复杂性和保护需求来决定是使用资源库还是数据访问对象。
7.3.1.6 领域服务
聚合通过聚合根的领域行为对外提供服务,而领域服务则是对聚合根的领域行为的补充。因此,我们应该尽量优先通过聚合根的领域行为来满足业务服务。
那什么场景下我们会需要用到领域服务呢?有如下两个:
领域服务在领域驱动设计中扮演着重要的角色,它不仅补充了聚合根的领域行为,而且在特定情况下,如生命周期管理和依赖外部资源时,它还提供了必要的封装和组合功能。这有助于保持领域模型的一致性和稳定性,同时防止领域知识的泄露。通过领域服务,我们能够确保聚合根实体不依赖于资源库接口和防腐层接口,从而使得聚合更加纯粹和专注于业务逻辑。领域服务的使用是在特定场景下的权衡选择,它提供了一种平衡业务需求和技术约束的方法。
7.3.1.7 领域事件
领域事件属于领域层的领域模型对象,由限界上下文中的聚合发布,感兴趣的聚合(同一限界上下文/不同限界上下文)可以进行消费。而当一个事件由应用层发布,则该事件为应用事件。
领域事件的引入主要是为了更有效地追踪实体状态的改变,并且在状态改变时,通过事件消息的传递来实现领域模型对象之间的协同工作。
领域事件的特征:
领域事件的用途:
领域事件应该包含:
领域事件在领域驱动设计中扮演着至关重要的角色,它不仅允许实体状态的变更得到有效追踪,还通过事件驱动的消息传递机制,促进了领域模型对象之间的解耦和协同。领域事件的特征和用途共同确保了业务逻辑的一致性和可追溯性,同时也为异步处理和分布式系统提供了坚实的基础。通过精心设计领域事件,我们可以在不违反封装原则的前提下,实现领域内部状态变更的透明通知和外部系统的响应。这种设计使得领域模型更加健壮,能够适应复杂业务流程的变化和挑战。
在领域设计模型中,聚合是最小的设计单元。
7.3.2.1 设计的经验法则
这里有四条经验法则:
下面展开讲述法则 1 和法则 3。
法则 1 在聚合边界内保护业务规则不变性。
法则 1 包含了两个关键点:
a) 参与维护业务规则不变性的领域概念应该置于同一个聚合内;
b) 在任何情况下都要保护业务规则不变性。
比如,在 sms 系统中分数和绩点具有转换关系,这是业务规则的不变性,因此这两个概念被放在了同一个聚合边界内;当出现老师修改分数的场景时,需要保证绩点的换算同时被执行。由于这里绩点对象是值对象,不需要关心其生命周期管理的问题。当业务规则涉及到多个实体时,就需要通过本地事务来保证规则不变性(即实体间基于业务规则的数据一致性)。
法则 3 通过身份标识符关联其他聚合。
注意这里强调了关联关系,关联关系会涉及聚合 A 对聚合 B 的生命周期管理的问题,对于这种聚合间的关联关系,我们通过身份标识建立关联。而当聚合 A 引用聚合 B,但不需要对聚合 B 进行生命周期管理时,我们认为这是一种依赖关系(比如方法中的入参,而非类中的属性),对于聚合间的依赖关系,我们可以通过对象引用(聚合根实体的引用)的方式建立依赖。(PS:假设设计之初难以判断聚合之间到底是关联关系,还是依赖关系,我们就统一使用身份标识符作为关系引用即可)
聚合间的依赖关系通常分为两种方式
总结来说,聚合是领域模型中的最小单元,需要遵循一些设计原则以保证其有效性和可维护性。其中包括保护业务规则的不变性,设计简洁的聚合,通过身份标识符关联其他聚合,以及使用最终一致性来更新其他聚合。同时,聚合之间的关系可能是关联关系或依赖关系,需要根据实际情况进行判断和处理。
7.3.2.2 设计步骤
1. 理顺对象图
分析对象是实体还是值对象。
2. 分解关系薄弱处
聚合本质是一个高内聚的边界,因此我们可以根据领域对象之间关系的强弱来定义出聚合的边界。对象间的关系由强到弱可以分为:泛化关系,关联关系和依赖关系。其中关联关系和依赖关系在 7.3.2.1 小节已讲述,而泛化关系可以理解为是继承关系(即父子关系)。
泛化关系
虽然泛化关系是强耦合关系,但是根据对业务理解的视角不同,会产生不同的设计:
关联关系
上述提到过,聚合间的关联关系会涉及聚合 A 对聚合 B 的生命周期管理,这其实是一个比较宽松的约束。那聚合内实体的关联关系应该是怎么样的呢?**生命周期一致的、共存亡的,当主实体被销毁时,从实体也随之会被销毁。**例如商品实体和商品明细实体。在示例-SMS 中,成绩和总成绩会被定义为两个聚合,原因是总成绩在成绩锁定后被统计,随后将不再发生改变,可见两者不存在上述的共存亡的关联关系。
PS: 实际上根据关联关系来区分边界的方法同样适用于限界上下文的边界划分。例如示例-SMS 中的课程和成绩生命周期不同,先有课程,后有成绩;而且成绩锁定后,课程被撤销也不会对成绩有影响,因此就可以定义出课程上下文和成绩上下文。
依赖关系
依赖关系主要体现的是实体间的职责委派和创建行为,可以分到不同的聚合边界。
3. 调整聚合边界
根据业务规则调整聚合边界。为了维护业务规则的不变性,相关的实体应该置于同一个聚合边界内。
领域模型的设计需要通过整理对象图,确定聚合边界,并根据业务规则调整聚合边界。在确定聚合边界时,需要考虑对象间的关系强度,包括泛化关系、关联关系和依赖关系。泛化关系可以是整体视角或独立视角,关联关系要求生命周期一致且共存亡,依赖关系主要体现在职责委派和创建行为。通过这些步骤,我们可以设计出合理且有效的领域模型。
这里的服务是对应用服务、领域服务、领域行为(实体提供的方法)和端口(资源库接口、防腐层接口)的统称。
7.3.3.1 分解任务
业务服务包含若干个组合服务,组合服务包含若干个原子服务。领域行为和端口都可以认为是原子服务。
7.3.3.2 分配职责
应用服务:匹配业务服务,提供满足业务需求的服务接口。应用服务本身不包含任何领域逻辑,其作用是协调领域模型对象,通过它们的领域能力实现完整的应用目标。
领域服务:匹配组合服务,负责执行业务功能。若原子任务为无状态行为或独立变化的行为,也可由领域服务承担。它控制多个聚合与端口之间的协作,并负责组合任务的执行。
领域行为:匹配原子服务,提供业务功能的业务实现。强调无状态和独立变化,由实体提供。
端口:匹配原子服务,抽象对外资源的访问,主要的端口包括资源库接口和防腐层接口。
虽然上述给出了应用服务、领域服务、领域行为和端口与业务服务、组合服务和原子服务的匹配关系,但是对于应用服务、领域服务、领域行为和端口之间的关联关系却还不清晰,这里结合书中内容和个人实践给出一个参考。
应用服务:核心职责是编排聚合间的领域服务。
- 领域服务
- 防腐层接口:当多聚合间领域服务进行协作后需要访问外部资源,此时相关的防腐层逻辑应该至于应用层。(防腐层是上下文映射的方式,并非领域模型特有)
- 工厂:特指服务契约对象或装配器担任工厂,即将DTO转换为实体的工厂。
- 领域行为:在上述工厂创建实体后,若只需要调用实体的领域行为,而不需要涉及生命周期管理,可直接在应用服务中进行调用。
领域服务:细粒度的领域对象可能会将领域层的知识泄露到应用层,导致应用层需要处理复杂且细致的交互,从而使领域知识扩散到应用层或用户界面代码中,而领域层则可能丢失这些知识。明智地引入领域层服务有助于在应用层和领域层之间保持明确的界限,因此应用层通常不会直接引用聚合的领域行为。
- 工厂
- 领域行为
- 防腐层接口:聚合内需要依赖外部资源,则将防腐逻辑收拢在领域服务中。
- 资源库接口
领域行为:不要关联资源库和防腐层接口。
总结而言,服务设计是领域模型中的一个关键环节,涉及应用服务、领域服务、领域行为和端口的概念。应用服务作为协调者,领域服务负责执行,领域行为提供业务实现,而端口则是外部资源的抽象。这些服务的正确分配和关联对于保持领域模型的清晰和高效至关重要。
聚合设计:
注意:请点击图像以查看清晰的视图!
服务设计:
下面只罗列非查询类的服务设计。
成绩上下文:
领域实现建模关注的并非是如何进行代码实现,而是如何验证代码实现的正确性,保证实现的高质量。
领域模型中的服务包括了应用服务、领域服务、领域行为和端口。其中通过 Provider(面向服务行为)、Resource(面向服务资源)、Subscriber(面向事件)、Controller(面向视图模型)对外进行暴露的,我们称为远程服务。
领域模型中的服务与测试金字塔的关系如下图所示。
注意:请点击图像以查看清晰的视图!
领域实现建模提倡的是测试驱动开发的编程思想,即要求开发者在进行逻辑实现前,优先进行测试用例的编写,站在调用者角度而非实现者角度去思考接口。
在上述测试金字塔中,开发者需要关注的是单元测试(不依赖任何外部资源的测试就是单元测试)。在领域设计建模阶段,我们对业务服务/应用服务进行分解,定义出了领域行为和领域服务。对于领域行为,由于其不依赖外部资源,因此我们可以直接编写单元测试;而对于领域服务,其可能会通过端口访问外部资源,此时我们需要对端口进行 mock,以隔离外部资源对领域逻辑验证的干扰。特别地,单元测试一定要覆盖所有对业务规则的验证,这是保证领域行为和领域服务正确性的基础。
单元测试编码规范:
总结来说,测试驱动开发是领域实现建模中的一种重要编程思想,它要求开发者首先编写测试用例,从调用者的角度思考接口,并确保单元测试覆盖所有业务规则的验证。这种开发模式有助于提高代码质量,减少bug,并确保领域行为和服务的正确性。
注意:请点击图像以查看清晰的视图!
代码架构分层是经典 DDD 四层**:用户接口层**,应用层,领域层和基础设施层。
需要注意的的地方是:
总结而言,分层架构是领域驱动设计中的核心概念,它通过将代码分为用户接口层、应用层、领域层和基础设施层,帮助我们清晰地定义各个层次的责任和依赖关系。这种架构风格不仅提高了代码的可维护性和可扩展性,而且通过依赖倒置和基础设施层的独立,保证了领域层的纯净和服务的灵活拆分。这种设计模式有助于团队更好地管理和维护复杂的业务系统。
用户接口层的核心职能:协议转换和适配、鉴权、参数校验和异常处理。
├── controller //面向视图模型&资源
│ ├── ResultController.java
│ ├── assembler // 装配器,将VO转换为DTO
│ │ └── ResultAssembler.java
│ └── vo // VO(View Object)对象
│ ├── EnterResultRequest.java
│ └── ResponseVO.java
├── provider // 面向服务行为
├── subscriber // 面向事件
└── task // 面向策略
└── TotalResultTask.java
应用层的核心职能:编排领域服务、事务管理、发布应用事件。
├── assembler // 装配器,将DTO转换为DO
│ ├── ResultAssembler.java
│ └── TotalResultAssembler.java
├── dto // DTO(Data Transfer Object)对象
│ ├── cmd // 命令相关的DTO对象
│ │ ├── ComputeTotalResultCmd.java
│ │ ├── EnterResultCmd.java
│ │ └── ModifyResultCmd.java
│ ├── event // 应用事件相关的DTO对象, subscriber负责接收
│ └── qry // 查询相关的DTO对象
└── service // 应用服务
├── ResultApplicationService.java
├── event // 应用事件,用于发布
└── adapter // 防腐层适配器接口
代码组织以聚合为基本单元。
├── result // 成绩聚合
│ ├── entity // 成绩聚合内的实体
│ │ └── Result.java
│ ├── service // 领域服务
│ │ ├── ResultDomainService.java
│ │ ├── event // 领域事件
│ │ ├── adapter // 防腐层适配器接口
│ │ ├── factory // 工厂
│ │ └── repository // 资源库
│ │ └── ResultRepository.java
│ └── valueobject // 成绩聚合的值对象
│ ├── GPA.java
│ ├── ResultUK.java
│ ├── SchoolYear.java
│ └── Semester.java
└── totalresult // 总成绩聚合
├── ... 这段有点长,其代码结构与成绩聚合一致,因此省略 ...
该层主要提供领域层接口(资源库、防腐层接口)和应用层接口(防腐层接口)的实现。
代码组织基本以聚合为基本单元。对于应用层的防腐层接口,则直接以 application 作为包名组织。
├── application // 应用层相关实现
│ └── adapter // 防腐层适配器接口实现
│ ├── facade // 外观接口
│ └── translator // 转换器,DO -> DTO
├── result // 成绩聚合相关实现
│ ├── adapter
│ │ ├── facade
│ │ └── translator
│ └── repository // 成绩聚合资源库接口实现
│ └── ResultRepositoryImpl.java
└── totalresult // 总成绩聚合相关实现
├── adapter
│ ├── CourseAdapterImpl.java
│ ├── facade
│ └── translator
└── repository
└── TotalResultRepositoryImpl.java
微服务拆解指的是将一个整体服务分割成多个细粒度的服务。这里的“细粒度”并没有一个客观的标准,它依赖于主观判断。尽管如此,我们对“微”这个词还是有一些基本要求的:服务应当足够内聚,足够独立,足够完备,这样的微服务才能使得拆分的收益大于成本。如果一个微服务提供的业务功能需要与其他众多微服务协作,那么这样的微服务就失去了意义。
而上述我们对微服务的基本要求,实际上与限界上下文的特征(最小完备,自我履行,稳定空间,独立进化)不谋而合,因此,我们可以把限界上下文映射为微服务。在日常工作实践中,我通常会将限界上下文和微服务一一对应,但这并不是绝对的规则。限界上下文是从领域角度定义的逻辑边界,而微服务的设计除了考虑逻辑边界外,还需要考虑物理边界以及实际的质量要求(如性能、可用性、安全性等)。例如,在使用CQRS架构时,领域模型会被分为命令模型和查询模型,尽管它们属于同一个限界上下文,但它们通常是物理隔离的。因此,限界上下文可以作为微服务拆分的指导原则,但在拆分过程中还需要考虑质量要求、架构设计等技术因素。
总结来说,微服务拆分是一种将整体服务细分为多个独立、内聚和完备的小服务的架构风格。它与领域驱动设计中的限界上下文概念相吻合,但在实际应用中,微服务的设计还需考虑物理边界和质量要求。微服务的目标是提高系统的可维护性和可扩展性,同时保持服务的独立性和内聚性,以便更好地应对复杂的业务需求。
注意:请点击图像以查看清晰的视图!
上文在提及限界上下文识别和聚合设计的时候其实都提到需要考虑事务属性,即需要通过本地事务来保证业务规则的不变性/一致性。这里我们会疑惑的是**:谁来承担管理事务的职责?事务管理的边界是什么?**
应用层承担管理事务的职责
事务本质是一种技术手段,而领域模型本身与技术无关,因此事务应该由应用层负责管理。
事务管理的边界是聚合,有时限界上下文也可以
资源库操作的基本单元是聚合,因此事务管理的边界是聚合便是自然而然得出的结论。这里需要考虑的是当需要保证事务属性的不仅仅只有资源库操作,还包括发布领域事件时(即保证聚合落库和事件发布的原子性),我们可能需要采用可靠事件模式,即通过将领域事件落库到事件表来表示事件的发布。这样,应用层在管理事务时就无需承担太多的心智负担。当然,采用可靠事件模式实际上是限制了领域模型的实现,这可以看作是技术对领域模型的一种入侵。但相比于解放应用层而言,这种入侵应该是利大于弊。
我们也知道,应用层的核心职责是负责编排和协调不同聚合的领域服务,而应用层又负责事务管理,因此我们可以推断事务管理的范围是多个聚合(即限界上下文)。但这里有两个关注点:
总结来说,本地事务在领域驱动设计中扮演着重要角色,它们用于确保业务规则的一致性和不变性。应用层负责管理事务,而事务管理的范围通常是一个聚合,但在某些情况下,也可以是一个限界上下文。这种设计允许应用层专注于业务逻辑,而无需担心底层技术细节。同时,通过可靠事件模式,我们可以确保领域事件的发布与聚合的落库具有原子性。虽然这种模式可能会对领域模型的实现造成一定的限制,但它为应用层提供了更大的便利,使得事务管理变得更加简单和高效。
为了避免耦合,DDD 主张通过柔性事务来保证跨聚合、跨限界上下文的最终一致性。而目前业界比较主流的应用是 Saga 模式**:通过使用异步消息来协调一系列本地事务,从而维度多个服务之间的数据一致性**。而另一个非常著名的柔性事务方案 TCC 为啥没有 Saga 契合呢?
TCC 共分为三个阶段:
Try 阶段:准备阶段,对资源进行锁定或预留;
Confirm 阶段:提交阶段,执行实际的操作;
Cancel 阶段:这是补偿阶段,如果在前面阶段发生错误,需要进行补偿,即释放在 Try 阶段预留的资源。
可以看到 TCC 实际对领域模型的侵入是比较大的:
Saga 模式并不要求其对资源进行锁定/预留,而其补偿操作也是通过执行操作的逆操作来完成(比如支付的逆操作是退款)。在大多数情况下,完整的领域模型都会提供操作及其逆操作。
总结而言, Saga 事务是一种在领域驱动设计中用于保证跨服务数据一致性的柔性事务模式。它通过异步消息和本地事务的协调,避免了服务的直接耦合,同时也不需要对资源进行复杂的锁定或预留。相比之下,TCC 模式虽然也是一种柔性事务方案,但它对领域模型的设计有较大的影响,要求模型提供特定的服务接口和属性以支持事务的各个阶段。Saga 模式因其对领域模型的较小侵入和操作的直观性,在某些场景下可能更受欢迎。然而,选择哪种事务模式应该基于具体的应用场景和业务需求。
《解耦-领域驱动设计》
《领域驱动设计:软件核心复杂性应对之道》
《实现领域驱动设计》
《微服务架构设计模式》
极客时间《DDD 实战课》
极客时间《如何落地业务建模》
《领域驱动设计精粹》
DDD架构如何落地,是非常常见的面试题。
以上的内容,如果大家能对答如流,如数家珍,基本上 面试官会被你 震惊到、吸引到。
在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,并且在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。
最终,让面试官爱到 “不能自已、口水直流”。offer, 也就来了。
当然,关于DDD,尼恩即将给大家发布一波视频 《第34章:DDD的学习圣经》, 帮助大家彻底穿透DDD。
……完整版尼恩技术圣经PDF集群,请找尼恩领取
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓