知识树沉淀总结-领域驱动设计DDD

  • 我们经常谈领域建模,很多同学刚开始接触学习DDD的时候,也会被各种新颖的概念所吸引,觉得很高大上。

  • 然而理想是丰满的,现实确实骨感的,事实上,领域建模的几本书晦涩难懂,新概念带来的困惑太多,实际真正落地的场景、实践起来的经验很少。有些同学更是在了解一些概念后在日常开发中强行上DDD,结果反而是代码难以理解、维护和扩展,建立了“技术壁垒”。还有一个关键点,业务的需求变化速度快,功能能够开发上线完成已是万幸,哪有这么多时间去设计和思考。

  • 那么,DDD到底有没有用?该如何入门呢?

  • 其实,Martin很早就说过,领域模型并不一定是最好的工具。DDD首先是作为一种思想而存在,教我们做好软件。而我个人认为DDD更适合一些具有一定复杂度的业务,和架构一样,没有最好的架构,只有最合适的架构。

  • 本文是早之前刚接触时一些文章笔记梳理以及一些个人的理解分享出来,欢迎讨论。本文从几个方面浅谈DDD入门知识。

1 序言

  • 在讨论为什么要使用领域驱动设计,或者介绍DDD的优势和劣势之前,我们先看一下一般开发同学经常挂在嘴边的一些常见问题吧。

1.1 传统框架MVC

  • 传统的MVC模型把框架分成了三层:显示层、控制层、模型层。显示层负责显示用户界面,控制层负责处理业务逻辑、而模型则负责与数据库通信,对数据进行持久化的操作。
  • 从代码角度来看,这样的框架结构每个模块职责分离,特别适合小型的应用系统。但是随着业务复杂度的上升,会发现服务层的逻辑以及代码不断增长,变得庞大且复杂、测试成本直线上升。由于各个Service的逻辑散落在各处,后续新需求的维护的成本也非常高,导致交付效率越来越低,稳定性风险也越来越高。
  • 除了日常开发的一些问题外,由于缺乏一定的业务知识沉淀,如果文档沉淀或者更新不及时的情况下,新同学来接受一个新的小需求,面对产品描述的需求改动点,开发同学根本无从下手。一方面是新同学业务的生疏,但真正根本原因还是:开发与产品之间的语言不能保持一致,双方对于同一事物的表达和理解有很大的区别。产品描述的更多是实际的业务场景,而开发则更关注背后的具体实现逻辑,加之文档的缺失,可以说是面对一堆模型和代码两眼茫然。

1.2 CRUD工程师?

  • 开发同学特别是后端开发同学,经常自嘲CRUD工程师、高级数据订正专家等等。很多同学也很焦虑,觉得自己每天除了写CRUD和BUG,要不就是在修BUG,没有很大长进。
  • 什么是CRUD?CRUD就是数据库中的增删改查的操作。其实CRUD没什么错,数据库本身就很强大,提供了可靠、快速、大容量的存储服务以及事务管理,大部分企业的业务业务初期需求在拆分到一定粒度的开发任务时,就是CRUD,都是适用的,此时系统是清晰的。
  • 所以,很多场景形成以数据为中心的开发模式,并没有什么不好。CRUD的好处不用再多说,但是,随着迭代不断演化,业务逻辑越来越复杂、系统越来越冗杂、模块彼此关联,这时候,CRUD也逐渐暴露出一些问题。
  • 最显著的就是它是一种数据模型,与业务脱节,很多同学使用CRUD,导致对象和关系数据库累赘转换。其次,本身对业务理解是缺失或者说存在很大问题的,如果开发同学没有深入了解业务或者说在开发过程中,对业务不够熟悉,很容易发现开发做了半天得到的,并不是用户和产品想要的。再者,如果后续有新需求升级时,技术上的改动可能特别大或者推翻重做都有可能,成本非常高。
  • 如果对业务不够了解,在面向复杂的业务场景时,技术同学本身也会面临很多问题:
    • 系统的架构设计:很多项目,技术复杂度与业务复杂度相互交错纠缠不清,对复杂的业务理解不充分,无法产出缺优秀的业务架构、产品架构、技术架构顶层架构设计。
    • 系统的具体实现:由于缺少合理的产品和技术设计文档,开发同学在具体实现时,无法确定业务系统的边界,进而导致服务以及应用边界模糊不清楚。其次,技术和业务实现耦合度高,业务及功能性代码混杂,导致代码模块性差、可读性差,新的业务需求无法准确的转换为正确的代码实现。
    • 缺乏灵活扩展:对业务理解不充分,导致缺乏一定的前瞻性架构设计,加上系统的代码实现混杂,从而让整个系统缺乏扩展性。面对频繁变化的业务需求时,无法有效基于现有的模型支持业务的不断创新和扩展,从而让开发同学陷入一个死循环:无暇设计,需求紧急基于老系统打补丁,系统越来越复杂,变成一个大泥塘的遗留系统,技术壁垒越来越深。
    • 项目的协作:由于业务系统边界划分不清,各个业务系统间比较耦合,没有明确的边界以及统一的语言来进行沟通,让业务同学和技术同学理解一致。进而,一个项目一个需求提到好几个团队都能做,但是开发过程你推我我推你,需求一拖再拖,整个项目工时随着上升。
    • 价值的认定:业务系统后端工程师的价值在哪里?理解并抽象出业务逻辑,建立满足需求的业务模型,以此设计实现出可靠的系统,并有效地控制复杂性。这才是大部分业务系统后端工程师的工作重点,也是解决他们工作中遇到的问题和难点的关键。

2 为什么要DDD?

  • 那么说了那么多日常开发中的困境,DDD到底能帮助我们什么呢?
  • 用一段话总结下:DDD改变了传统软件开发工程师针对数据库进行的建模方法,将要解决的业务概念和业务规则转换为软件系统中的类型以及类型的属性与行为,通过运用面向对象的封装、继承、多态等设计要素,降低或隐藏整个系统的业务复杂性,并使得系统具有更好的扩展性,应对纷繁多变的现实业务问题。
  • DDD试图解决的是软件的复杂性问题,如果软件比较复杂,或者是预期会很复杂,或者是你不知道,那么都可以开始考虑DDD。否则,由于维系领域模型需要实现大量的封装和隔离,DDD会带来较大的成本。

2.1 使用DDD的好处?

  • 领域模型是整个软件的核心,是软件中最有价值和最具竞争力的部分,设计足够精良且符合业务需求的领域模型能够更快速的响应需求变化。
  • 通用领域模型语言:在有界的上下文中形成统一的语言,方便与产品同学或者开发同学之间沟通,减少歧义和沟通成本。* 通用领域模型语言:DDD帮助统一语言,在有界的上下文中形成通用的语言,方便与产品同学或者开发同学之间沟通,减少歧义和沟通成本。
  • 业务领域知识沉淀:领域驱动设计的核心是建立统一的领域模型,领域模型不同于数据数据模型,和任何技术实现及存储实现无关,只反映业务本身,业务通过核心稳定的领域模型,领域知识进行传递,沉淀业务知识。
  • 系统的架构设计:传统的开发设计方式,数据驱动是从数据出发,设计数据库表,编写DAO,然后进行业务实现。而领域驱动设计从领域出发,分析领域内模型及其关系,并进行领域建模,设计核心业务逻辑,完成了领域模型与数据模型分离,业务复杂度与技术复杂度分离。
  • 系统的具体实现:领域驱动设计领域建模完成后,确定了业务和应用边界,保证业务模型与代码模型的一致性,进而再进行技术细节实现。领域模型确保了我们的软件的业务逻辑都在一个模型模块中,提高软件的可维护性,业务可理解性以及可重用性。
  • 系统的扩展性:领域模型划分出的边界,沉淀的核心稳定的领域模型知识,面对新来的需求可以快速判断需求的合理性,需求的归属子域,应该在哪个模块实现,通过不断的抽象、不断的分治、拉齐团队内成员对需求的认知,应对系统复杂性,让设计更加清晰和规范。

2.2 DDD的难点?

  • DDD有这么多优势,为什么大家使用的还是不够多呢?任何一个事物都有两面性,不可能是完美,DDD也有很多问题,而且有些问题可能就是致命的,让人望而却步的?
  • 要求、难度系数高:领域模型的正确构建首先需要有一个熟悉业务、建模的领域专家,其次依赖编程人员对DDD的深刻理解,对团队成员的本身素质要求较高。
  • 效率,投入产出比:正确的建模从方案讨论、设计、实践、落地往往需要花费一段时间,面对业务紧急的需求以及倒排的工期,可能满足不了上线的要求。而其他一般架构不需要这些时间,短期投入成本高,但是从长期看,领域模型收益还是很高的。
  • 团队成员之间协作:领域模型一般是整体的,领域内是相互依赖的,相互影响的,不容易分割为可并行独立解耦开发的模块,对开发同学的协作要求较高。
  • 技术上的缺陷:DDD是基于聚合来组织代码,对于高性能场景下,加载聚合中大量的无用字段会严重影响性能,实际场景中,更多的高流量查询往往脱离聚合直接对某一个数据进行查询。此外,事务被限定在限界上下文中,跨多个限界上下文的场景需要开发者额外考虑分布式事务问题。

3 DDD核心概念

  • DDD分为战略设计和战术设计。战略设计侧重于高层次、宏观上去划分和集成限界上下文,而战术设计则关注更具体使用建模工具来细化上下文。

3.1 战略设计

  • 在战略设计中,讲求的是子域和限界上下文的划分,以及各个限界上下文之间的上下游关系,更偏向于软件架构,帮助我们从一个宏观的角度观察和审视软件系统。
  • 首先,领域可以拆分为多个子领域。一个领域相当于一个问题域,领域拆分为子域的过程就是大问题拆分为小问题的过程。子域还可根据需要进一步拆分为子子域,拆到一定程度后,有些子子域的领域边界就可能变成限界上下文的边界了。每个领域模型都有它对应的限界上下文,团队在限界上下文内用通用语言交流。领域内所有限界上下文的领域模型构成整个领域的领域模型。

3.1.1 领域 Domain

  • 领域,一种专门活动或事业的范围、部类或部门。领域用来确定范围和边界,将业务上的问题限定归属在特定的边界内,领域就是这个边界内要解决的业务问题域。

3.1.2 子域 & 核心域/通用域/支撑域

  • 子域:为了降低业务理解和系统实现的复杂度,可以将领域进一步划分成更小范围的小问题,称为子域,如果需要可以对子域进一步划分形成子子域。有一个文章的例子挺好的,比如研究的对象是桃树,那么果实、根、茎叶是领域,但是如果在果实领域进一步研究,还需要研究组织甚至细胞,那么组织就是果实的子域、细胞是组织的子域。

  • 什么是核心域、通用域、支撑域?

  • 首先我们看下这三个域的定义:

    • 核心域:决定产品和系统核心竞争力的子域是核心域,它是影响产品和业务成功的主要因素,比如电商系统中关注的会员、商品、订单、交易、库存、营销等。对于业务来说, 核心域是企业核心竞争力, 也是创造利润里最关键的部分。
    • 通用域:没有太多个性化的诉求,比如统一的认证和权限管理系统、日志等,特点是可能被多个领域公用的部分。这类应用很容易买到,没有企业特点限制,不需要做太多的定制化。
    • 支撑域:系统中业务分析阶段最不重点关注的领域,也就是非核心域非通用域的领域,不包含决定产品和公司核心竞争力的功能,但也是必须的,例如电商里面的物流,仅仅是为了支撑业务的运转而存在,甚至可以去购买别人的服务。
  • 为什么需要这种划分?

    • 为什么要划分出这些新的名词呢?拿上面文章提到例子,对于桃树而言,根、茎、叶、花、果实、种子六个领域哪一个是核心域?有人说是种子,有人说是根,有人说是叶子,也有人说是茎等等,为什么会有这种情况呢?
    • 因为每个人站的角度不一样,你如果是果农,那么果实就是核心域,你的大部分操作应该都是围绕提高果实产量进行;如果你是景区管理员,那么芳菲四月桃花盛开才是你重点关注;如果比是林场工作人员,那么树干才应该是你重点关注的领域。由此可见,对于同一个领域划分的子域,每个人都有不同的理解,必须通过讨论确定核心域,确保大家认同一致。对于实际业务开发来说,参与的人员众多,势必要最开始就确定我们的核心域。
  • 总的来说,核心域、支撑域和通用域的主要目标是:通过领域划分,区分不同子域在公司内的不同功能属性和重要性,从而公司可对不同子域采取不同的资源投入和建设策略,给予不同的关注度。对于一个公司来说,预算、时间是有限的,公司战略重点和商业模式应该找到核心域,且重点关注核心域。

3.1.3 限界上下文 Bounded Context

  • 为什么要有限界上下文这个概念?日常人与人之间的沟通交流,单单从别人口中的一个单词很难理解到表达者的意思,尤其中华文化博大精深,比如说到“小米”,如果没有上下文,你很难知道表达者表达的是粮食小米,还是小米名牌小米手机等。因此,为了避免同样的词语产生歧义,一定需要结合到语言上下文中去理解其真正的语义。限界上下文就是用来细分领域,从而定义通用语言所在的边界。

  • 限界上下文包含两部分:限界(Bounded)和上下文(Context)。限界就是领域的边界, 而上下文就是语义环境, 通过限界上下文让所有交流的人讨论的范围在同一个领域边界内。限界上下文用来封装通用语言和领域对象,提供上下文环境,保证在领域之内的一些术语、业务相关对象等概念有一个确切的含义。

3.1.4 通用语言 Ubiquitous Language

  • 当我们有了限界上下文以后,就需要在该限界上下文中使用一种语言用于表达软件模型,这个语言就叫做这个限界上下文里的通用语言。

  • 为什么需要通用语言?开发同学和业务同学、产品同学、领域专家等在交流以及协作的过程中,业务、领域专家一般都是使用业务行话表达他们的这些概念,开发同学则满脑子都是类、方法、怎么实现等等,总是想将实际生活中的业务概念和程序模块进行对应,在这个痛苦的交流过程中,怎么对齐所有人讲同一种语言和概念?

  • 所以整个项目同学需要一种通用语言来进行沟通,它可以是任何计算机语言、人类语言或者图形,只要能让团队内的每个人都能看懂。通用语言的产出其实就是需求分析的过程,也是理解领域知识的过程,提炼领域知识过程的产出物。团队中各个角色就系统目标、范围与具体功能达成一致的过程。通用语言可以定义一些公共术语,减少概念混淆,达到概念和代码的统一语言,连接概念和实现。一开始的通用语言可能不尽完美,但它就像是代码一样,经常需要重构。

  • 好的通用语言应该是清晰明了的,解决沟通障碍节省时间成本,让大家更好进行协作,并且能够反映在代码中。

3.2 战术设计-基本模型

  • 如果说战略设计更偏向于软件架构,那么战术设计便更偏向于编码实现。战术设计的目的是使得业务能够从技术中分离并突显出来,让代码直接表达业务的本身,其中包含了聚合根、应用服务、资源库、工厂等概念。

  • 领域驱动设计围绕着领域模型进行设计,通过分层架构将领域独立出来。领域模型的对象包括:实体、值对象和领域服务,领域逻辑都应该封装在这些对象中。聚合是一种边界,它可以封装一到多个实体与值对象,并维持该边界范围之内的业务完整性。在聚合中,至少包含一个实体,且只有实体才能作为聚合根。工厂和资源库都是对领域对象生命周期的管理。工厂负责领域对象的创建,用于封装复杂或者可能变化的创建逻辑。资源库负责从存放资源的持久层获取、添加、删除或者修改领域对象。

3.2.1 模型 Model

  • 领域反映到代码里就是模型,模型是对领域某个方面的抽象,并且可以用来解决相关域的问题,模型分为实体和值对象两种。

3.2.2 实体 Entities

  • 实体是一个具有唯一身份标识的对象,并且可以在相当长的一段时间内持续地变化,具有连续性。标识主要有两个特点:唯一和不可变,唯一指的是具有一个唯一键,在任何地方能通过这个唯一键找到这个实体,类似人的身份证号等。连续性是指这个实体是有生命周期的,在这个生命周期中会有不同的状态,但是有一种内在的连续性。

3.2.3 值对象 Value Object

  • 值对象用来描述领域的特定方面,只是描述一个事物的特征,并且是一个没有标识符的对象,比如颜色,地理位置等。
  • 现实世界中存在许多这种只有特征意义的对象,会被很多地方引用,这些对象不需要唯一标识,或者不具有连续性,那么应当构建为值对象。实际代码的话,如果两个对象的所有的属性的值都相同,我们会认为它们是同一个对象的话,可以把这种对象设计为值对象,比如地址信息。还有一个需要注意的是,在不同的上下文环境下,同一对象可能是实体,也有可能为值对象,具体问题具体分析。
  • 值对象与实体的区别是什么?
    • 值对象没有唯一标识和连续性,任何属性发生变化, 都可以认为是新的值对象。判断对象是否相同:值对象需要判断所有属性是否相同,而实体只需要判断唯一标识是否相同。
    • 值对象一般依附于实体而存在,是实体属性的一部分,而非独立存在。值对象属性是只读的,可以被安全的共享.

3.2.4 领域服务 Services

  • 领域中的一些概念,如果是名词,适合建模为对象的一般归类到实体对象或值对象。如果是动词,比如一些操作、一些动作,代表的是一种行为,如果是和实体或值对象,密切相关的也可以合并到某个实体或者值对象中。 但是,有些操作不属于实体或者值对象本身,这些操作或动作往往会涉及到多个领域对象,并且需要协调这些领域对象共同完成这个操作或动作,这时就需要创建领域服务来提供这些操作。
  • 领域服务有两个特征:1) 操作代表了一个领域概念,且不是实体或者值对象的一个自然的部分; 2)被执行的操作涉及领域中的其他对象;操作是无状态的。领域服务还有一个好处可以避免领域逻辑泄露到应用层。因为如果没有领域服务,那么应用层会直接调用领域对象完成本该是属于领域服务该做的操作。此外,如果实体操作过多或者过大,为了避免臃肿,也可以使用领域服务来解决。但是,不能把所有的东西都搬到领域服务里,过度使用可能会导致贫血对象的产生。
  • 领域服务和应用服务是不同的,领域服务是领域模型的一部分,用来处理业务逻辑,而应用服务不是。应用服务是领域服务的直接客户,负责处理事务、安全等操作,它将领域模型变成对外界可用的软件系统。

3.2.5 领域事件(Domain Event)

  • 一般来说,数据的一致性处理都是通过事务完成的,而DDD原则则是希望一次事务只更新一个聚合实例。然而,真实的业务场景中,确实存在同时修改多个聚合的业务用例,此时如何保障数据的一致性呢?
  • 领域事件可以用于处理上述问题,此时数据的最终一致性取代了事务一致性,通过领域事件的方式达到各个组件之间的数据一致性。领域事件定义了模型中需要关心的事件对象。当关心的状态由于模型行为而发生改变时,系统从领域模型中发布领域事件。领域事件的最终接收者既可以是本地的限界上下文,也可以是外部的限界上下文。
  • 领域事件不仅可以解决多个聚合的事务问题,还可以记录发生在软件系统中所有的重要修改,此外,在CQRS架构系统中,还用于写模型和读模型之间的数据同步等。

3.3 战术设计-生命周期

  • 连续性是指这个实体是有生命周期的,在这个生命周期中会有不同的状态,但是有一种内在的连续性。

3.3.1 聚合 Aggregate

  • 聚合就是一组相关对象的集合,即需要将领域中高度内聚的概念放到一起组成一个整体。聚合根就是聚合在一起的基础,并提供对这个聚合的操作及外界打交道。聚合除了聚合根以外,还有自己的边界,即聚合里有什么。聚合通过定义清晰的所属关系和边界,并避免错综复杂的对象关系⽹网来实现模型的内聚。在聚合边界内,对象之间可以相互引用。聚合之间,聚合根是聚合中唯一允许被外部引用的元素,比如订单ID。

  • 如何识别聚合?需要从业务的角度深入分析哪些对象的关系是内聚的,或者说可以看成是一个整体来考虑,这些对象就可以放在一个聚合内。关系内聚是指这些对象之间必须保持一个固定规则,固定规则是指在数据变化时必须保持不变的一致性规则。当我们在修改一个聚合时,我们必须在事务级别确保整个聚合内的所有对象满足这个固定规则。

  • 如何创建聚合?设计聚合时要注意哪些东西?

    • 聚合内事务一致性,聚合外最终一致性:聚合边界之内的所有内容组成了一套不变的业务规则,该规则应该总是保持一致的。因此,聚合表达了与事务一致性边界相同的意思。在一个事务中只能修改一个聚合实例。 聚合外要通过唯一标识来引用其他聚合,引用和被引用聚合不可以在同一个事务中进行修改,如果你试图在单个事务中修改多个聚合,那么此时的一致性边界是错误的,只能通过唯一标识引用。
    • 生命周期一致性:生命周期一致性是指聚合边界内的对象,和聚合根之间存在“人身依附”关系。即:如果聚合根消失,聚合内的其他元素都应该同时消失。
    • 聚合尽可能小:刚才说到聚合是持久化的一个单位,需要保证以聚合为单位的数据事务一致性。因此,聚合的设计尽量小到不可再拆分。如果聚合太大,那就会导致并发修改困难,还会导致性能问题,大聚合在查询时需要加载的数据,涉及到的实体较多,会降低查询的性能。
    • 优先使用值对象:我们应该尽量的将聚合根所包含的其它聚合建模成值对象,而不是实体。值对象,只作为数据传输对象,无状态变化,且一旦创建不再修改。

3.3.2 聚合根 AggregateRoot

  • 聚合根也称为根实体,是业务的载体。聚合根本身也是实体,拥有实体的属性和业务行为,有着自己的业务逻辑。只不过这个实体,还用来作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则,协同完成共同的业务逻辑。聚合根必须是实体而非值对象,因为它需要整体持久化,所以一定会有标识,而聚合根里的各个元素,既可能是实体,也可能是值对象。
  • 如何识别聚合根?如果一个聚合只有一个实体,那么这个实体就是聚合根;如果有多个实体,那么我们可以思考下聚合内哪个对象有独立存在的意义并且可以和外部直接进行交互。聚合内任何对其它元素的操作都必须通过聚合根来进行。聚合根拥有自己独立的生命周期,其实体的生命周期从属于其所属的聚合,值对象因为只是值而已,并没有生命周期。

3.3.3 工厂(Factory)

  • 当创建一个对象或者创建一个聚合很复杂或者暴露了过多内部结构,可以使用工厂进行封装。工厂的作用是将创建对象的细节隐藏起来,这样可以不会让领域层的业务逻辑泄露到应用层,同时也减轻了应用层的负担,它只需要简单的调用领域工厂创建出期望的对象即可。
  • 工厂是生命周期的开始阶段,它知道先怎样实例化一个对象,然后在对这个对象做哪些初始化操作,这些知识就是创建对象的细节,如果传递进来的参数符合创建对象的业务规则,则可以顺利创建相应的对象,但是如果由于参数无效等原因不能创建出期望的对象时,应该抛出一个异常,以确保不会创建出一个错误的对象。整个创建过程可简单可复杂,有时可能直接调用构造函数即可有时则可能需要调用其他系统获取数据等。实体和值对象的工厂不太一样,因为值对象是不可变的,所以需要工厂一次性创建一个完整的值对象出来。而实体工厂则可以选择创建之后再补充一些细节。

3.3.4 库 (Repository)

  • 资源库是聚合根的家,外部世界只能通过资源库来完成对聚合的访问。仓储里面存放的对象一定是聚合,仓库以聚合的整体管理对象,不会单独对某个聚合内的子对象进行单独查询或做更新操作。
  • 资源库是生命周期的结束,它封装了基础设施以提供查询和持久化聚合的操作。这样能够让我们始终聚焦于模型,而把对象的存储和访问都委托给资源库来完成。通俗来将,资源库就是用来持久化聚合根的,但是需要注意的是,资源库并不是数据库的封装,而是领域层与基础持久层之间的桥梁。DDD关心的是领域内的模型,而并非是数据库的操作,理论上资源库对客户隐藏了内部的工作细节,委托持久层来进行存储,至于具体使用关系型数据库、NOSQL、甚至内存里读取和存储数据并不重要。

4 关于​领域模型&数据模型

关于数据及领域模型,张建飞有个文章专门介绍了下什么是领域模型?什么又是数据模型?两者可以有什么区别。

  • 领域模型关注的是领域的知识,对业务的一个抽象建模,是业务领域的核心实体,如何建模的关键是要看是否真正理解并抽象出业务逻辑,建立满足需求的业务模型,业务领域知识模型是否沉淀,性能及扩展性不是首要考虑目标。
  • 数据模型关注更多的是数据存储,而数据存储考虑更多的是CRUD,如何建模则更多取决于性能、扩展性等非功能属性,不用过多考虑业务模型,不行多加一张表总能解决。
  • 对业务部门来说,领域模型是核心价值,数据模型更多是技术细节了,但是二者还是都很重要的。按大佬的说法正确的做法应该是有意识的把这两个模型区别开来,分别设计,因为他们建模的目标会有所不同,数据模型负责的是数据存储,其要义是扩展性、灵活性、性能。而领域模型负责业务逻辑的实现,其要义是业务语义显性化的表达,以及充分利用OO的特性增加代码的业务表征能力。
  • 虽然说领域模型和数据模型各司其职,但是,在实际工作中,我们经常会因为性能、扩展性的原因故意打破,导致领域及数据模型差异不大。比如会为了查询方便,数据库的表冗余聚合根的其他业务键而不是唯一标识,比如通过数据冗余提升访问性能,还有类似通过元数据、垂直表、扩展字段提升表的扩展性等。

5 关于贫血和充血模型

回想一下我们在刚学习面向对象的概念时,会学习到对象会由属性和方法组成,也会经常拿汽车启动的例子来对比面向过程和面向对象编程的区别。例如在面向过程编程时,如果想让汽车开动,我们必须要清楚汽车启动的每个细节,然后编写该过程。而进行面向对象编程时,仅仅创建出汽车对象后,调用其启动方法即可完成汽车开动,具体的属性和汽车开动细节封装在汽车对象内部。而当我们正式实践时,往往都是汽车对象仅仅做了属性封装,是一个典型的贫血对象,汽车的开动过程往往都是封装在了对应的Service类中来完成。如果按照面向对象的角度来看,DDD中的实体可能才算是更加标准的面向对象。

造成我们传统的DO对象和DDD中实体的DO对象差异化的很大原因,和我们之前设计时是按照数据模型优先有很大关系,往往都是数据库表和DO对象一一对应。而DDD中是提倡先构建领域模型,针对实际业务场景构建实体对象和行为,再将实体对象映射到数据库持久对象。这就有可能形成一个实体对应多个数据库表的情况,例如我们常用的BUC系统中,如果将其中的权限作为一个实体来设计,实体中就有可能包含用户和角色两个持久化对象。因此,如果一开始应用DDD进行设计时,一定要跳出数据模型优先的束缚,不要让领域模型被数据模型绑架,先设计出合理的领域模型是首要任务。

  • 关于DDD的失血、贫血、充血和涨血模型,失血和胀血模型应该是不被提倡的,一般讨论最多的还是贫血模型和充血模型。从技术上来说,这两者都是可行的了。
  • 简单来说,贫血模型就是类似平常用的Bean,一般只有getter和setter方法,只作为保存状态或者传递状态,但并不包含业务逻辑,这种只有数据没有行为的对象不是真正的领域对象, DDD中的实体属于充血模型,会封装包含这个实体相关的所有业务逻辑,它不仅是多个业务属性的载体,也是操作或行为的载体。

但是我个人仍然主张使用贫血模型。其理由:
* 充血模型第三个缺点,为了事务封装,Service层要给每个domain logic提供一个过程化封装,这对于编程来说,做了多余的工作,非常烦琐。
* 如何划分domain logic和service logic的标准是不确定的
* 贫血模型的domain object确实不够rich,但是我们是做项目,不是做研究,好用就行了
1、实体类层,即Item,带有domain logic的domain object
2、DAO层,即ItemDao和ItemDaoHibernateImpl,抽象持久化操作的接口和实现类
3、业务逻辑层,即ItemManager,接受容器事务控制,向Web层提供统一的服务调用

  • 业务逻辑(business logic) 可以划分为 应用逻辑(application logic)和 领域逻辑 (domain logic);划分标准:换技术换语言换通信协议换存储这些技术手段导致要更改的逻辑,是应用逻辑,不需要更改的逻辑是领域逻辑。
    • 举个栗子,股东持有企业的股票, 股东可以买卖股票, 这些是领域逻辑;
    • 而使用手机,网络,证券集中交易系统, 来完成交易, 这是应用逻辑
    • 100年前, 没有电脑, 是使用纸合同来完成股票买卖的, 领域逻辑也没变。
  • 有了上述划分, 三种模型的特点就可以简述为:
  • 充血模型的思路就是把比较稳定的领域逻辑分离出来。
  • 贫血模型的问题就是领域逻辑泄露到应用逻辑(事务脚本)里了。
  • 涨血模型的问题是存储逻辑(应用逻辑)混到领域逻辑里了。

失血模型

  • 领域对象模型仅仅包含对象属性的定义和操作对象属性的getter/setter方法的纯数据类,所有的业务逻辑完全由业务逻辑层中的服务来完成。
  • 优点:领域对象结构简单;
  • 缺点:肿胀的业务服务代码逻辑,难于理解和维护;无法良好的应对复杂业务逻辑和场景;
eg:拍卖项目
* 实体类叫做Item;
* DAO接口类叫做ItemDao ; 持久层定义
* DAO接口实现类叫做ItemDaoImpl; 持久层实现
* 业务逻辑类叫做ItemService;实现具体的业务逻辑及事务的管理。

* Item只有getter/setter方法; 其他所有业务逻辑统统放在ItemService中实现,如placeBid,loadItemById,findAll等;

贫血模型

  • 贫血模型相对于失血模型包含了不依赖于持久化的领域逻辑,而那些依赖持久化的领域逻辑被分离到Service层。相当于在包含getter/setter方法上并包含了对象的行为(例如:就像一个完整的人,具有一些属性如姓名、性别、年龄等,还具有一些能力,如走路、吃饭、恋爱等,这样才是一个完整的对象, 但不包含依赖持久层的业务逻辑。
  • 优点:各层单向依赖,结构清楚,易于实现和维护;设计简单易行,底层模型非常稳定,一般简单业务逻辑的应用很适用;
  • 缺点:部分依赖持久化的逻辑分离到service层可能导致service层过重,不够OO,无法良好的应对非常复杂逻辑和场景;
eg:拍卖项目
* 实体类叫做Item;
* 竞标这个业务逻辑被放入到Item中来。placeBid这种透明的持久化行为。Item里面不能去调用ItemDAO,对ItemDAO产生依赖;

* DAO接口类叫做ItemDao ; 持久层定义
* DAO接口实现类叫做ItemDaoImpl; 持久层实现
* 业务逻辑类叫做ItemService;实现具体的业务逻辑及事务的管理,不包含了placeBid的逻辑,包含依赖ItemDao的方法loadItemById,findAll,这些依然在ItemService;

* placeBid业务逻辑是放在Item中实现的,而loadItemById和findAll业务逻辑是放在ItemService中实现的。但是即使placeBid业务逻辑放在Item中,你仍然需要在ItemService中简单的封装一层,以保证对placeBid业务逻辑进行事务的管理和持久化的触发。
  • 哪些逻辑应该放入领域逻辑?
    • 可重用度高的,和领域对象状态密切关联的,此外该逻辑独立于持久层框架之外,仍然可以脱离持久层进行单元测试,这个领域对象仍然是一个完备的、自包含、不依赖于外部环境的领域对象。
  • 如何区分领域逻辑层和业务逻辑层?
    • 领域逻辑层只应该和这一个领域对象的实例状态有关,而不应该和多个领域对象的状态有关,因为其他对象只应该关联唯一标识。所以,逻辑只和这个对象的状态有关,就是领域逻辑层;如果,逻辑和对个领域对象状态有关,就该放业务逻辑层。

充血模型

  • 充血模型相对于贫血模型包含了依赖于持久化的领域逻辑, 相当于在包含getter/setter方法上并包含了大多数相关的业务逻辑,也包含了依赖于持久层的业务逻辑。业务逻辑层是很薄的一层,仅仅简单封装少量业务逻辑以及控制事务、权限逻辑等,不和DAO层打交道。
  • 优点:更加符合OO的原则;业务逻辑层比较薄,不和DAO打交道。
  • 缺点:DAO和领域对象形成了双向依赖,复杂的双向依赖会导致很多潜在的问题;如何划分业务层逻辑和领域层逻辑是非常模糊;Service层的事务封装特性,Service层必须对所有的领域对象的逻辑提供相应的事务封装方法,几乎重新定义了一遍领域层服务。
eg:拍卖项目
* 实体类叫做Item;
* 包含了实体类信息,也包含了所有的业务逻辑。 所有的业务逻辑全部都在Item中。

* DAO接口类叫做ItemDao ; 持久层定义
* DAO接口实现类叫做ItemDaoImpl; 持久层实现
* 业务逻辑类叫做ItemService;实现具体的业务逻辑及事务的管理,不包含了placeBid的逻辑,包含依赖ItemDao的方法loadItemById,findAll,这些依然在ItemService;

* placeBid业务逻辑、而loadItemById和findAll业务逻辑都是是放在Item中实现的。ItemService中简单的封装一层,以保证事务的管理。

胀血模型

  • 领域对象模型包含对象属性的定义和操作对象属性的getter/setter方法并包含了所有相关的的业务逻辑,也包含了不想关的其它应用逻辑(如授权、事务等)。胀血模型取消了业务逻辑层,只剩下Domain Object和DAO两层,在Domain Object的Domain Logic上面封装事务,授权逻辑等。
  • 优点:简化了代码分层结构,也算符合面向对象设计;
  • 缺点:取消了业务逻辑层,在Domain Object的Domain Logic上面封装事务,授权等很多本不应该属于领域对象的逻辑,导致模型的不稳定;代码理解和维护性差;

4 关于DDD的写操作、读操作

DDD写操作

  • DDD的写操作,需要将领域实体完整的构建出来,然后调用聚合的服务或者领域服务完成写操作,尽量不要使用单个的原子服务暴露给外界使用,尽量地按照“应用服务 -> 聚合根 -> 资源库”的结构进行写服务实现,
  • 首先聚合根的创建,需要通过Factory完成,好处在前面Factor也说过;其次,各种写操作的业务逻辑要尽量使用聚合本身的服务,如果能够在聚合根边界内完成更好。最后,在聚合根中实在不适合放置的业务逻辑,才考虑放到DomainService。

DDD读操作

  • 在DDD的写操作中,我们需要构建领域实体之后再进行写操作。但是,如果读操作也严格采用与写操作相同的结构时,会发现有时不但得不到好处,反而使整个过程变得冗繁,查询性能非常差。在日常的工作中读操作更倾向于基于数据模型的读操作,因为在互联网企业,每个域的划分比较细查询的依赖也比较单一,其次,对性能的要求也比较高。如果有条件的话,可以使用CQRS,读写模型可以各自设计,但是需要考虑数据的延迟、一致性的关键问题。
  • 基于领域模型的读操作:这种方式不区分读模型和写模型,先通过统一的资源库获取到领域模型,然后将其转换为DTO或者VO,这种方式优点是直接明了,也不用创建新的数据读取机制,直接使用资源库读取数据即可。然而缺点也很明显:一是读操作完全束缚于聚合根的边界划分,为了加载查询所需的数据,经常讲整个聚合根或者相关的数据加载到内存中再做转换,这种方式既繁琐又低效吗,关键性能还差;二是在读操作中,通常需要基于不同的查询条件返回数据,比如如果都通过资源库,导致仓库上处理了太多的查询逻辑,变得越来越复杂,也逐渐偏离了Repository本应该承担的职责。
  • 基于数据模型的读操作:这种方式绕开了资源库和聚合,直接从数据库中读取客户端所需要的数据,此时写操作和读操作共享的只是数据库。这种方式的优点是读操作的过程不用囿于领域模型,而是基于读操作本身的需求直接获取需要的数据即可,一方面简化了整个流程,另一方面大大提升了性能。但是,由于读操作和写操作共享了数据库,而此时的数据库主要是对应于聚合根的结构创建的,因此读操作依然会受到写操作的数据模型的牵制。
  • CQRS:CQRS(Command Query Responsibility Segregation),即命令查询职责分离,这里的命令可以理解为写操作,而查询可以理解为读操作。与“基于数据模型的读操作”不同的是,在CQRS中写操作和读操作使用了不同的数据库,数据从写模型数据库同步到读模型数据库,通常通过领域事件的形式同步变更信息。这样一来,读操作便可以根据自身所需独立设计数据结构,而不用受写模型数据结构的牵制。CQRS本身是一个很大的话题,已经超出了本文的范围,读者可以自行研究。

关于领域建模常用方法

通用方法论

  • 领域建模通用方法论:
    • 了解业务是建模的必要条件;
    • 任何业务都存在一条稳定的业务流程, 找到业务流程节点就成功了一半;
    • 业务流程中的每个节点都会有相应的产物出现,每个节点的产物就是业务骨架;
  • 首先回顾下,领域建模的基本概念:领域是什么,模是什么以及如何建。
    • 领域是什么?领域建模首先要有限定范围,你建模的限定词,比如商品领域建模、优惠券领域建模等。
    • 模是什么?模是业务场景的映射,想要建模首先就要了解业务。
    • 怎么建模?领域建模方法论有很多:用例建模、四色建模、事件风暴等。
  • 领域建模的基本步骤:
    • 找出业务主流程;比如优惠券业务,业务流程就是建券、发券、用券。业务主流程每个流程节点都会有一个产物就是业务的骨架,在这个业务下,它的产物是券批次、优惠券实例。
    • 细分业务主流程;主流程基础上继续分析子流程,主流程能让我们知道整体的业务流程,但还有些细节流程是在子流程中,比如建券是一个大流程。
    • 进行一定的抽象:领域建模源于业务,又服务于业务。
  • 领域驱动设计的四重边界
    • 确定项目的愿景与目标,确定问题空间,确定核心子领域、通用子领域(多个子领域可以复用)、支撑子领域(额外功能,如数据统计、导出报表);
    • 解决方案空间里的限界上下文就是一道进程隔离层面的物理边界;
    • 每个限界上下文内,使用分层架构划分为:接口层、领域层、应用层、基础设施层之间的最小隔离;
    • 领域层里为了保证各个领域的完整性和一致性,引入聚合的设计作为隔离领域模型的最小单元 ;

领域驱动设计过程

图片转自:http://zhangyi.xyz/overview-of-ddd/

面对客户的业务需求,由领域专家与开发团队展开充分的交流,经过需求分析与知识提炼,获得清晰的问题域。通过对问题域进行分析和建模,识别限界上下文,利用它划分相对独立的领域,再通过上下文映射建立它们之间的关系,辅以分层架构与六边形架构划分系统的逻辑边界与物理边界,界定领域与技术之间的界限。之后,进入战术设计阶段,深入到限界上下文内对领域进行建模,并以领域模型指导程序设计与编码实现。若在实现过程中,发现领域模型存在重复、错位或缺失时,再进而对已有模型进行重构,甚至重新划分限界上下文。

用例分析法

  • 用例分析是比较通用的领域建模方法,在传统需求调研过程中结合领域模型的设计思路进行,核心是通过通过需求、场景、规则、流程等梳理用例,进而规划领域模型。
  • 用例分析法是领域建模最简单可行的方式。大致可以分为获取用例、收集实体、添加关联、添加属性、模型精化几个步骤。
  • 在进行用例分析法进行建模之前,关键要梳理业务,产出用例:
    • 梳理领域概念:梳理出领域内我们关注的概念、用例的关系,并统一交流词汇,形成统一语言;
    • 梳理业务规则:梳理出领域内我们关注的各种业务规则,业务规则通常具有不变性;
    • 梳理业务场景:梳理出领域内的核心业务场景;
    • 梳理业务流程:梳理出领域内的关键业务流程,比如订单处理流程;
    • 梳理业务用例:梳理出业务用例,收集用例进而进行建模;
  • 用例分析法是领域建模最简单可行的方式。大致可以分为获取用例、收集实体、添加关联、添加属性、模型精化几个步骤:
    • 收集用例:提取领域规则描述,从领域、规则、场景、流程中进行提取,收集相应的名词、动词、形容词;
    • 收集实体:从名词中定位出主要实体,比如商品、SKU、品类等
    • 添加关联:动词添加实体和实体之间的关联,比如商品“包含”SKU,卖家“开设”店铺等
    • 添加属性:从形容词中添加实体属性,比如颜色、价格等
    • 模型精化:识别出初步模型,验证并迭代模型,同时补充用例验证模型、业务流程验证模型

四色建模法

  • 四色建模法是一种模型的分析和设计方法,通过把所有模型分为四种类型,帮助模型做到清晰、可追溯。简单说,四色法关注的是,某个人(Party)的角色(PartyRole)在某个地点(Place)的角色(PlaceRole)用某个东西(Thing)的角色(ThingRole)做了某件事情(MomentInterval)。
    • 时间(Moment-interval):表示在某个时刻或某一段时间内发生的某个活动,具有可追溯性的记录运营或管理数据的时刻或时段对象,用粉红色表示;
    • 角色(Role):角色就是我们平时所理解的“身份”,往往由人或者物来承担,会有相应的责任和权利,用黄色表示。一般,一个Moment-interval对象会关联多个Role。例如,一次下单涉及两个Role,分别是客户(Customer)和商品(Product);为什么会有角色这个概念?因为有些活动,只允许具有特定角色(身份)的PPT(参与者)才能参与该活动。比如一个人只有具有教师的角色才能上课(一种活动)。
    • 人-事-物(Party-Place-Thing Archetype):表示参与某个活动的人或物,地点则是活动的发生地,也叫PPT,代表参与到流程中的参与方/地点/物,用绿色表示;
    • 描述(Description):表示对PPT的本质描述,对PPT 对象的一种补充描述,用蓝色表示, 它不是PPT的分类。
    • 举个例子,有一个人叫张三,如果某个外星人问你张三是什么?你会怎么说?可能会说,张三是个人,但是外星人不知道“人”是什么。然后你会怎么办?你就会说:张三是个由一个头、两只手、两只脚,以及一个身体组成的客观存在。虽然这时外星人仍然不知道人是什么,但我已经可以借用这个例子向大家说明什么是“Description”了。在这个例子中,张三就是一个PPT,而“由一个头、两只手、两只脚,以及一个身体组成的客观存在”就是对张三的Description,头、手、脚、身体则是人的本质的不变的共性的属性的集合。但我们人类比较聪明,很会抽象总结和命名,已经把这个Description用一个字来代替了,那就是“人”。

事件风暴法

  • 事件风暴法类似头脑风暴,简单来说就是谁在何时基于什么做了什么,产生了什么,影响了什么事情。事件风暴是快速的设计技术,类似头脑风暴,可以快速分析复杂业务领域,完成领域建模的目标。

  • 事件风暴关注如下元素:简单理解就是谁(实体)在何时(时间)基于什么(输入)做了什么(命令、动作)产生了什么(输出)影响了什么(事件)。

    • 事件 -> 某个动作的结果
    • 属性-> 事件的输入、输出
    • 命令-> 某个动作
    • 实体-> 命令的触发者
  • 事件风暴要先做如下准备:

    • 正确的人(业务⼈员,领域专家,技术⼈员,架构师,测试⼈员等关键⻆色都);
    • 开放空间(有足够的空间可以将事件流可视化,让⼈们可以交互讨论);
    • 即时贴(至少三种颜色),关联的人充分讨论,集体决策,从价值角度来审视业务流程的合理性,领域事件容易创建业务人员和非业务人员的共识。
  • 事件风暴的基本步骤:

    • 1、在便利贴上写领域事件,梳理出业务流程,一般是橘色。写好的便利贴按照时间顺序放到建模平面上,从左往右逐步发生。
    • 2、创建导致领域事件发生的命令,命令应该是指令式的。创建领域事件的便利贴是浅蓝色的,按照时间顺序,将命令和事件的关系处理好
    • 3、把命令和领域事件通过实体、聚合联系起来。由于建模没完,因此没有真正的实体和聚合,而是领域专家思想里的业务概念和概念群。用淡黄色的便利贴来表示聚合,其左下角是命令,右下角是事件。聚合的名字应该是名词。
    • 4、在建模平面上画出边界和事件流动的箭头。
    • 5、识别用户执行操作所需的各种视图,以及客户不同用户的关键角色。

关于DDD分层架构

  • 由于现实世界的复杂性,分层可以提供一个相对高层的视角来 分解和简化我们的问题,此外分层也可带来 可测试性、 可维护性、 灵活性、 可扩展性等方面的好处。

  • 关于分层架构的好处是显而易见的:

    • 简化复杂性,关注点分离(设计中每个部分都得到单独的关注,修改的影响也限定在某个范围内),结构清晰;
    • 降低耦合度,隔离层次,降低依赖(上层无需关注下层具体实现),利于分工、测试和维护(可维护性);
    • 提高灵活性,可以灵活替换某层的实现;
    • 提高扩展性,方便实现分布式部署;
  • 分层其实是 把一系列相同或相似的对象进行分类并放在同一层,然后 根据他们之间的依赖关系再确定上下层次关系。可以看出,分层的核心在于 分类 和 关联(事物的两面性: 区别和联系)。

    • 分类:确定 划分标准,将相关对象进行 抽象(分离变与不变,提取共同点)并归类,对层之间进行 隔离,并保持层内元素是高度 内聚;
    • 关联:层与层以及外部是存在联系的,需要关注不同层间的 依赖与通信,保持层间的 松散耦合;
  • 我们总是希望设计出 高内聚、低耦合、易维护、可扩展 软件,分层就是达到高内聚低耦合的手段之一。

    • 高内聚(同一层): 单一职责
    • 低耦合(层与层之间): 每层只能依赖于同层以及其下层,每层以接口方式供上层调用;原则其实可以看做一种 高层指导规范,可以帮助我们避免一些常见的坑或者帮助我们更好的实施以得到更好的效果。
  • SOLID基本原则

    • 单一职责(Single Responsibility Principle): 每个类应该只有一个独一无二的职责,这条针对类的原则同样适用于每一层;
    • 开-闭原则(Open Close Principle): 对象或实体应该对扩展开放,对修改封闭;
    • 里氏替换原则(Liskov Substitution Principle): 应用程序应该依赖于抽象(基类或者接口),而不是具体实现
    • 接口分离原则(Interface Segregation Principle): 接口的职责也应该是单一的,接口中应该包含哪些方法,需要进行严格的评估
    • 依赖倒置(Dependency Inversion Principle): 抽象不能依赖于具体,而具体则应该依赖于抽象。类之间的直接依赖应该用抽象来取代

四层架构

  • 用户界面层,负责向用户显示信息和解释用户命令,完成前端界面逻辑。展示层的组件实现用户与应用交互的功能,处理Controller的VO层展示、Restful消息处理、配置文件解析等;
    • Controller层为什么需要参数校验?校验应该取决于校验的内容,一般推荐尽早校验,不过这里主要是进行一些简单的、不涉及业务规则的校验。具体的业务规则的校验放在领域层。
  • 应用层,主要组件是Service,因为主要职责是协调各组件工作,所以通常会与多个组件交互,如其他Service,领域对象,Repostitory等。核心负责展现层与领域层之间的协调,也是与其它系统应用层进行交互的必要渠道,例如事务、执行单位操作、调用应用程序的任务。应用层要尽量简单,不包含业务规则或者知识,不保留业务对象的状态,只保留有应用任务的进度状态,更注重流程性的东西。它只为领域层中的领域对象协调任务,分配工作,使它们互相协作。
  • 领域层,负责表达业务概念,业务状态信息以及业务规则。领域层是业务软件的核心,领域模型位于这一层。包含了前面提到的核心概念,如领域对象(实体、值对象)、领域服务以及它们之间的关系,负责表达业务概念、业务状态信息以及业务规则,具体表现形式就是领域模型。领域驱动设计提倡充血模型,即尽量将业务逻辑归属到领域对象上。
  • 基础实施层,向其他层提供通用的技术能力:为应用层传递消息,为领域层提供持久化机制(如数据库资源、中间件交互、缓存、MQ消息)等,屏蔽技术底座能力(如底层服务的健康度检查、配置参数等)。

六边形架构

  • 依赖倒置原则:高层模块不应该依赖于底层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。根据该定义,DDD分层架构中的低层组件应该依赖于高层组件提供的接口,即无论高层还是低层都依赖于抽象,整个分层架构好像被推平了。如果我们把分层架构推平,再向其中加入一些对称性,就会出现一种具有对称性特征的架构风格,即六边形架构。
  • 主动适配:指来⾃于UI、命令⾏等输⼊型命令, controller就是⼀种端⼝,端⼝的具体实现就是应⽤逻辑⾃身。因此端⼝和具体实现都在应⽤系统的内部。
  • 被动适配:指访问存储设备,外部服务等。每种访问就是⼀种端⼝,具体实现是各个具体的中间件。因此端⼝在整个应⽤系统的⾥部,具体实现在系统的外部。
  • 每⼀种输⼊和输出都是⼀个端⼝,每个端⼝都有具体的实现逻辑,因此整个应⽤系统的架构就是⼀些列的端⼝+适配逻辑组成,架构图就是⼀个多边形形状。有⼏个端⼝需要根据应⽤系统的具体情况⽽定,只是六个端⼝⽐较形象⽽得名为六边形架构。
  • 特点:1. 外层依赖内层使得依赖更合理。端⼝就是接⼝,依赖接⼝编程。借此保证了应⽤和实现细节之间的隔离。2. 可测试更好

  • 六边形每条不同的边代表了不同类型的端口,端口要么处理输入,要么处理输出。对于每种外界类型,都有一个适配器与之对应,外界通过应用层API与内部进行交互。无论采用哪种方式对端口进行划分,当客户请求到达时,都应该有相应的适配器对输入进行转化,然后端口将调用应用程序的某个操作或者向应用程序发送一个事件,控制权由此交给内部区域。应用程序通过公共API接收客户请求,使用领域模型来处理请求。我们可以将DDD战术设计的建模元素Repository的实现看作是持久化适配器,该适配器用于访问先前存储的聚合实例或者保存新的聚合实例。正如图中的适配器E、F和G所展示的,我们可以通过不同的方式实现资源库,比如关系型数据库、基于文档的存储、分布式缓存或内存存储等。如果应用程序向外界发送领域事件消息,我们将使用适配器H进行处理。该适配器处理消息输出,而上面提到的处理AMQP消息的适配器则是处理消息输入的,因此应该使用不同的端口。

洋葱架构

  • 洋葱架构针对六边形架构更进⼀步把内层的业务逻辑分为了DDD概念的应⽤服务层、领域服务层和领域模型层。
  • 特点:
    • 围绕独⽴的领域模型构建应⽤
    • 内层定义接⼝,外层实现接⼝
    • 依赖的⽅向指向圆⼼(注意:洋葱架构提倡不破坏耦合⽅向的依赖都是合理的,外层可以依赖直接内层,也可以依赖更⾥⾯的层)
    • 所有的应⽤代码可以独⽴于基础设施编译和运⾏

Event Sourcing

  • Event Sourcing 就是我们不记录数据的最终状态,我们记录对数据的每一次改变(Event),而读取的时候我们把这些改变从头再来一遍来取得数据状态,比如你有100块钱,现在剩下10块了,我们记录的不是money.total=10, 而是记录你每一次取钱的记录,然后从100块开始一步步重放你取钱的过程,来得到10.

  • 一开始,我们写的过程中,时常回想起数据驱动的好,(每次开始一个新东西的时候,是不是很熟悉的感觉?),觉得用Event Sourcing各种麻烦,直到后来随着系统的复杂性不断增加,我们才感觉到带来了非常大的好处, 这个随后单独来说。

  • Event Sourcing 的优点:

    • 溯源事件与重现操作:特别是在业务复杂的系统中,一个事务包含多个操作,它们有的是并行有的串行,如果需要了解操作的执行就需要对每个事件了如指掌。Event Sourcing 恰恰提供了事件的历史信息,方便查找任何时间点发生的事情。
    • 追踪和修复Bug:可以通过事件分析业务的执行过程,帮助发现Bug,例如重方Bug产生时的事件序列,从而定位Bug所处位置。发现Bug并且修复以后,可以通过重新聚合业务数据,重放执行的事件序列验证修复结果,同时将Bug造成的损失进行挽回。
    • 提高性能:Event Sourcing模式下,由于是记录事件执行的序列,因此都是新增操作,没有更新操作,相对于需要更新操作的系统而言记录数据的性能是提高了。如果使用视图的方式将实体的最终状态可以传递给其他的应用,而不用写入数据库以后再读取,这种做法也提高了效率。
  • Event Sourcing 的缺点:

    • 转变思路:Event Sourcing的落地需要在设计时就用领域驱动的方式开展,需要有基于事件的响应式编程思维。这种方式需要以领域模型设计优先,而不是传统的数据库设计优先。
    • 变更事件结构:随着业务流程的变化需要不断调整事件结构,对事件添加或者修改一些数据。这种行为会影响到“历史重现”,需要考虑兼容之前的事件结构。
    • 处理幂等事件:如果对应的事务在执行过程中被中断,需要通过事件回放的方式达到事务的最终一致性问题。此时需要对事件的幂等性提出要求,也就是同一个事件运行多次得到的结果不变。需要在事件处理时丢弃重复事件。
    • 查询事件数据库(event store):由于数据库中存放的一个个事件,如果针对实体状态的查询会相对困难。需要将这些事件重放,获取最新的实体状态的信息。这也是为什么需要通过CQRS的方式将读写进行分离,Command端使用Event Sourcing 而Query端使用Event Sourcing 发出Event 的最终状态进行查询的原因。

CQRS

  • CQRS让查询和写入分开,把界面需要查询的数据进行原样写入,原样的意思就是界面显示什么样的,就提前保存成什么样的,类似于原来的缓存,没有任何join操作,这样查询是非常高效的。
  • CQRS(Command-Query Responsibility Segregation) 是一种读写分离的模式,从字面意思上理解Command是命令的意思,其代表写入操作;Query是查询的意思,代表的查询操作,这种模式的主要思想是将数据的写入操作和查询操作分开。

  • CQRS与Event Sourcing的完美结合,这里通过一个通过几个图展示结合的过程:

图片转自: https://mp.weixin.qq.com/s/or3R5KMJ1pKQznguJCUmQw

COLA框架

  • 之前阿里张建飞整了一个COLA框架,旨在希望这个应用框架成为应用架构的最佳实践。本质上,其实发现不管是六边形架构、洋葱圈架构、整洁架构、还是COLA架构,都提倡以业务为核心,解耦外部依赖,分离业务复杂度和技术复杂度。
  • 接口层,应用层,领域层,基础设施层。其中最主要的属于领域层,其中引入了DDD领域模型的概念,领域模型中不再是按照分层去组织代码,而是按照领域模型去组织代码

实际工作的架构

  • 在提出的工作中,也搭了不少应用,架构的分层有一定的经验分享下:
  • 对外核心服务层:
    • Api包:通常是Controller的服务、VO的展示、HTTP之类的相关服务;
    • Client包: RPC之类的相关对外标准服务;如果读写应用没分离,也可以适当的进行read、write拆分;
  • 核心服务层:
    • Service包:核心业务逻辑服务层,因为主要职责是协调各组件工作, 核心负责展现层与领域层之间的协调,也是与其它系统应用层进行交互的必要渠道,例如事务、执行单位操作、调用应用程序的任务。
  • 领域层:
    • Domain包:核心领域层,里面包含几个小模块,factor、repository、serialer等;
  • 持久层:
    • Tunnel包:持久层服务抽象,目前一般使用mybatis,所以会依赖dao层,如果有其他的也可以替换实现;
  • 持久层实现:
    • Dao包:mybatis层服务;
  • 通用层:
    • Common包:通用的utils、常量、算法工具等;
  • 外部适配
    • Adapter包:主要用于处理依赖外部的服务封装及实现;

参考文档:

部分文章内容参考:
https://www.oschina.net/question/54100_10400
https://www.raychase.net/238?spm=ata.13261165.0.0.7eaa68e7JYNtNC
https://www.infoq.cn/article/6hpBsMXQNGx_EAPKUuWS
https://mp.weixin.qq.com/s/qA7a2c2grHxCIoRspmbNuQ
https://mp.weixin.qq.com/s/Z74JCNSdQ-jEkHFpR9Gukg
http://zhangyi.xyz/overview-of-ddd/
https://blog.csdn.net/u011537073/article/details/114267739
http://t.zoukankan.com/ilovejaney-p-13570373.html

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值