架构-初识DDD
导言
继上一篇BFF的文章后,我又去网上学习了一下DDD
(范畴驱动规划),发现一篇不错的文章,参阅并写了一些自己的了解同享在这里。
DDD
是什么
范畴驱动规划(Domain Driven Design) 是一种从体系分析到软件建模的一套办法论。以范畴为中心驱动力的规划体系。
为什么运用
- 面向目标规划,数据行为绑定,离别贫血模型
- 优先考虑范畴模型,而不是切开数据和行为
- 精确传达业务规矩
- 代码即规划
以上是网上文章说的,但我在会议中了解的仍是不变性,面向范畴常识进行一系列的编程与规划,只需其时范畴内的通用常识没有产生改动,这些规划就不会进行改动。
战略规划
首要战略开端,不以战略开端,战术规划将无法有用施行。它着重的是业务战略上的要点,怎么按重要性分配作业,以及怎么进行最佳整合。
首要用限界上下文的战略规划形式来别离范畴模型。然后在清晰的限界上下文中开展一套范畴模型的通用言语。进一步深化战略规划中将会了解到用子域处理体系无鸿沟的杂乱性。还会了解怎么经过上下文映射来集成多个限界上下文。上下文映射图一同界说了两个进行集成的限界上下文之间的团队间联系及技术完结办法。
限界上下文
限界上下文是一个显式的语义和语境上的鸿沟,范畴模型便存在于鸿沟之内。鸿沟内,通用言语中的一切术语和词组都有特定的含义。
子域
代表单一的,有逻辑的范畴模型。最佳状况,限界上下文于子域一一对应。
子域有三种类型:
- 中心域:业务的中心,中心竞争力。
- 支撑域:辅佐中心域
- 通用域:被整个业务体系运用
上下文映射图
上下文映射图便是表明两个或多个限界上下文之间的映射联系。
上下文安排和集成形式:
-
合作联系(Partnership):要么一同成功,要么一同失利,此刻他们需求树立起一种合作联系。他们需求一同和谐开发方案和集成办理。
-
同享内核(SharedKernel):模型和代码的同享将产生一种严密的依靠性。常见做法便是经过二进制依靠(jar)的办法同享给一切上下文运用。
-
客户方-供应方开发(Customer-Supplier Development):客户方(D)提需求,供应方(U)配合做开发,现在用mq解耦的办法就十分相似这种。
-
尊奉者(Conformist):跟客户方-供应方开发相似,仅仅供应方没有开发功力。下流只能盲意图运用上游的模型。
-
防腐层(Anticorruption Layer):简称ACL,在集成两个上下文,假如两头都状况杰出,能够引进防腐层来作为两头的翻译,而且能够阻隔两头的范畴模型。
-
敞开主机服务(Open Host Service):简称OHS,揭露发布服务,揭露的http服务,这是常常运用的
-
发布言语(Published Language):简称PL,在两个限界上下文之间翻译模型需求一种共用的言语,发布言语一般与敞开主机服务一同运用。比方http服务,运用xml交互仍是json做数据格式
-
另谋他路(SeparateWay):声明两个限界上下文不存在任何联系,这也是一种很重的联系,彻底独立各自开发
-
大泥球(Big Ball of Mud):对已有的一个大的稠浊的体系,现已无法在内部整理清楚了。你那怎么办呢?把这整个体系当成一个大泥球,对整个体系画在一个鸿沟内,当成一个黑盒子,这样只需接口可用就行,也防止了大泥球内部的稠浊扩展到其它体系上。对前史包袱的体系,能够采纳这种做法。
示例
架构
分层
严厉分层架构:某层只能与直接坐落的基层产生耦合。
松懈分层架构:答应上层与恣意基层产生耦合
依靠倒置准则
高层模块不应该依靠于底层模块,两者都应该依靠于笼统
笼统不应该依靠于完结细节,完结细节应该依靠于接口
简略的说便是面向接口编程。
依照DIP的准则,范畴层就能够不再依靠于根底设施层,根底设施层经过注入耐久化的完结就完结了对范畴层的解耦,选用依靠注入准则的新分层架构模型就变成如下所示:
六边形架构(端口与适配器)
关于每一种外界类型,都有一个适配器与之对应。外界接口经过运用层api与内部进行交互。
关于右侧的端口与适配器,咱们能够把资源库当作耐久化的适配器。
架构中,咱们持平的看待Web、RPC、DB、MQ等外部服务,根底施行依靠圆圈内部的笼统。
当一个指令Command恳求过来时,会经过运用层的CommandService去和谐范畴层作业,而一个查询Query恳求过来时,则直接经过根底施行的完结与数据库或许外部服务交互。再次着重,咱们一切的笼统都界说在圆圈内部,完结都在根底设施。
指令和查询责任别离--CQRS
-
一个目标的一个办法修正了目标的状况,该办法便是一个指令(Command),它不应该回来数据,声明为void。
-
一个目标的一个办法假如回来了数据,该办法便是一个查询(Query),不应该经过直接或许直接的手法修正目标状况。
-
聚合只要Command办法,没有Query办法。
-
资源库只要add/save/fromId办法。
-
范畴模型一分为二,指令模型(写模型)和查询模型(读模型)。
-
客户端和查询处理器
客户端:web浏览器、桌面运用等
查询处理器:一个只知道怎么向数据库履行根本查询的简略组件,查询处理器不杂乱,能够回来DTO或其它序列化的成果集,依据体系状况自定
-
查询模型:一种非标准化的数据模型,并不反映范畴行为,只用于数据显现
-
客户端和指令处理器
聚合便是指令模型
指令模型具有规划杰出的契约和行为,将指令匹配到相应的契约是很直接的作业
-
作业订阅器更新查询模型
-
处理具有终究共同性的查询模型
作业驱动架构
- 作业驱动架构能够融入六边型架构,交融的比较好,也能够融入传统分层架构
- 管道和过滤器
- 长时处理进程
- 自动拉取状况查看:定时器和完结作业之间存在竞态条件或许形成失利
- 被迫查看,收到作业后查看状况记载是否超时。问题:假如由于某种原因,一向收不到作业就一向不过期
- 作业源
- 关于聚合的每次指令操作,都至少一个范畴作业发布出去,表明操作的履行成果
- 每一个范畴作业都将被保存到作业存储中
- 从资源库获取聚合时,将依据产生在聚合上的作业来重建聚合,作业的重放次序与其产生次序相同
- 聚合快照:将聚合的某一作业产生时的状况快照序列化存储下来。以削减重放作业时的耗时
战术规划
实体
DDD中要求实体是仅有的且可继续改动的。意思是说在实体的生命周期内,不管其怎么改动,其仍旧是同一个实体。仅有性由仅有的身份标识来决议的。可变性也正反映了实体自身的状况和行为。
实体 = 仅有身份标识 + 可变性【状况(特点) + 行为(办法或范畴作业或范畴服务)
为什么运用实体
在运用DDD时,将数据模型转化为实体模型
仅有标识
在规划实体时。咱们首要考虑实体的实质特征,特别是实体的仅有标识和对实体的查找,而不是一开端便重视实体的特点和行为。
值目标能够存储实体的仅有标识,与身份相关的行为能够封装在值目标中,防止走漏到模型的其他部份或客户端中。
创立战略
- 用户供给仅有标识
- 运用程序生成仅有标识
- 耐久化机制生成仅有标识
- 另一个限界上下文供给仅有标识
标识生成时刻
推迟生成办法
及早生成办法
派遣标识
两个标识,一个为范畴所用,一个为ORM所用。派遣标识没有业务含义,投合ORM而建。对外要躲藏,不是范畴模型的一部分。
形式,层超类型,protected类型的派遣标识字段。
标识稳定性,不应该修正实体的仅有标识。
发现实体及其实质特性
发掘实体的行为:set办法不是彻底要制止,在其契合通用言语(有语义)的时分,或许完结客户端单个恳求不必调用多个set时才有理由运用set办法。多个set办法使语义充潢歧义,使范畴作业的发送也无法应对到单个指令上
创立实体:实体保护了一个或多个不变条件(整个生命周期中都有必要坚持业务共同性的状况),聚合重视不变条件
盯梢改动:运用范畴作业盯梢范畴实体的状况改动,将范畴专家所关怀的状况改动建模成作业。
值目标
当你只关怀某个目标的特点时,该目标便可作为一个值目标。为其增加有含义的特点,并赋予它相应的行为。咱们需求将值目标当作不变目标,不要给它任何身份标识,还应该尽量防止像实体目标相同的杂乱性。
值目标=值+目标=将一个值用目标的办法进行表述,来表达一个详细的固定不变的概念。
为什么运用值目标
运用不变的值目标使得咱们做更少的责任假定
值目标的特性
- 它衡量或许描绘了领城中的一件东西
- 它能够作为不变量
- 它将不同的相关的特点组合成一个概念全体
- 当衡量和描绘改动时,能够用另一个值目标予以替换
- 它能够和其他值目标进行持平性比较
- 它不会对协作目标形成副效果
完结
值目标有两个结构
第一个:包括一切特点的结构函数,对根本特点的赋值调用私有的setter办法(自派遣性)
第二个:仿制效果的结构函数,用于将一个值目标仿制为另一个新的值目标(浅仿制即可,深仿制太杂乱,关于不变的值目标同享特点不会呈现什么问题)
无副效果办法的姓名很重要,不引荐运用java bean标准,除非其有通用言语的含义。引荐:String.endWith(),startWith(), indexOf()等。值目标的规划,办法不要遵从JavaBean标准。其setter更违反了值目标的不变性准则
耐久化值目标
耐久化机制不应该影响到值目标的建模;依据范畴模型来规划数据模型,而不是依据数据模型来规划范畴模型
ORM与单个值目标
实体和值目标1对1映射,值目标的特点作为字段存在和实体同一张表中
多个值目标序列化到单个列中
实体引证了List和Set特点的值目标调集
运用数据库实体保存多个值目标
值目标独自一个数据库实体表存储,而且带有一个派遣主键标识,这个标识不对客户端展示。范畴模型依然是一个值目标。耐久化相关的逻辑没有走漏到模型或客户端上去。
范畴服务
当范畴中的某个操作进程或转化进程不是实体或值目标的责任时,咱们便应该将该操作放在一个独自的接口中,即范畴服务。请确保该服务和通用言语时共同的。而且确保它是无状况的。
概述
- 用于完结某个范畴的使命,不适合放在聚合或值目标上时,就放在范畴服务上
- 放在聚合根的静态办法上有悖DDD
- 防止在聚合中调用资源库
能够用范畴服务的状况
- 履行一个明显的业务操作
- 对范畴目标进行转化
- 以多个范畴目标作为输入参数进行核算,成果产生一个值目标
范畴作业
范畴作业是一个范畴模型中极其重要的部分,用来表明范畴中产生的作业。疏忽不相关的范畴活动,一同清晰范畴专家要盯梢或期望被告知的作业,或与其他模型目标中的状况更改相相关
范畴作业 = 作业发布 + 作业存储 + 作业分发 + 作业处理。
建模范畴作业
- 依据限界上下文的通用言语来命名作业及其特点
- 假如作业由聚合上的指令操作产生,则应该依据操作办法的姓名来命名范畴作业
- 作业的姓名应该反映曩昔产生的作业
- 范畴作业应该都有一个产生时刻特点,一同要包括别的的特点:比哪些聚合跟此作业相关,承继一致的DomainEvent接口
- 作业所带的特点能够反映出该作业的来历。作业目标供给getter办法。作业特点应该是只读的,没有setter办法
- 是否有必要消除作业的重复提交
- 一个业务用例对应一个业务,一个业务对应一个聚合根,也即在一次业务中,只能对一个聚合根进行操作。当一个聚合依靠另一个聚合时,能够经过作业完结它们状况的终究共同性
模块
模块的规划是根据范畴模型的,要契合通用言语的表述。其次,模块的规划要契合高内聚低耦合的规划思维。
范畴模型命名标准
- 尖端模块下一层模块名定位了一个限界上下文(便是一个运用子域),如com.smudge.atum
- 示例:com.smudge.atum.domain.aggregate,该层界说模型中的聚合。
- 上述命名标准与传统的分层架构和六边形架构兼容
聚合
聚合是范畴目标的显式分组,旨在支撑范畴模型的行为和不变性,一同充任共同性和业务性鸿沟。一个聚合包括聚合根、实体和值目标。
聚合规划准则
-
在共同性鸿沟之内建模真实的不变条件
- 共同性。业务共同性、终究共同性。一个业务中只修正一个聚合,反之:不能在一个业务中一同修正多个聚合实例,真要这么做的话要考虑终究共同性
- 不变条件。指的是一个业务规矩,该规矩应该总是坚持共同的
-
规划小聚合。根实体(Root Entity)表明聚合,绝大多数根实体能够规划为聚合
-
经过仅有标识引证其它聚合
-
在鸿沟之外运用终究共同性
打破准则的理由
- 便利用户界面
- 一组聚合只要一个用户在处理它们,确保用户-聚合亲和度使咱们有理由在单个业务中修正多个聚合实例,由于这不会违反聚合不变条件
- 大局业务
- 查询功用
完结
- 创立具有仅有标识的根实体(将实体建成聚合根)
- 优先运用值目标
- 运用迪米特规律
- “告知而非问询”准则
- Version完结达观并发
- 聚合中不应该注入资源库或许范畴服务
聚合根、实体、值目标
从标识视点:聚合根是实体,具有大局的仅有标识。而实体只要在聚合内部有仅有的本地标识,值目标没有仅有标识,经过特点判别持平性,完结Equals办法。
从是否只读的视点:聚合根除了仅有标识外,其他一切状况信息都理论上可变。实体是可变的。值目标不可变,是只读的。
从生命周期视点:聚合根有独立的生命周期,实体的生命周期从属于其所属的聚合,实体彻底由其所属的聚合根担任办理保护。值目标无生命周期可言,由于仅仅一个值。
聚合根、实体、值目标目标之间怎么树立相关
聚合根到聚合根:经过ID相关;
聚合根到其内部的实体,直接目标引证;
聚合根到值目标,直接目标引证;
实体对其他目标的引证规矩:
- 能引证其所属聚合内的聚合根、实体、值目标。
- 能引证外部聚合根,但引荐以ID的办法相关,别的也能够相关某个外部聚合内的实体,但有必要是ID相关,不然就呈现同一个实体的引证被两个聚合根持有,这是不答应的,一个实体的引证只能被其所属的聚合根持有。
值目标对其他目标的引证规矩:只需确保值目标是只读的即可,引荐值目标的一切特点都尽量是值目标。
工厂
范畴模型中的工厂
- 将创立杂乱目标和聚合的责任分配给一个独自的目标,它并不承当范畴模型中的责任,可是范畴规划的一部份
- 关于聚合来说,咱们应该一次性的创立整个聚合,而且确保它的不变条件得到满意
- 工厂只承当创立模型的作业,不具有其它范畴行为
- 一个含有工厂办法的聚合根的首要责任是完结它的聚合行为
- 在聚合上运用工厂办法能更好的表达通用言语,这是运用结构函数所不能表达的
聚合根中的工厂办法
- 聚合根中的工厂办法体现出了范畴概念
- 工厂办法能够供给护卫办法
范畴服务中的工厂
- 在集成限界上下文时,范畴服务作为工厂
- 范畴服务的接口放在范畴模型内,完结放在根底设施层
资源库
是聚合的办理,仓储介于范畴模型和数据模型之间,首要用于聚合的耐久化和检索。它阻隔了范畴模型和数据模型,以便咱们重视于范畴模型而不需求考虑怎么进行耐久化。
只为聚合创立资源库
聚合和资源库存在1对1的联系
完结
- 第一步,界说资源库接口,接口中有put或save相似的办法
- 与面向调集的资源库的不同点:面向调集的资源库只要在新增时调用add即可,面向耐久化的不管是新增仍是修正都要调用save
- 完结类放在根底设施层,将范畴的概念与耐久化相关的概念相别离,依靠倒置准则。根底设施层位与一切层之上,而且单向向下引证范畴层
业务办理
- 业务的办理肯定不应放在范畴模型和范畴层中,业务放在运用层,然后为每个首要的用例创立一个门面,门面的办法是粗粒度的,每一个用例流对应一个业务办法。当用户界面层调用门面中的一个业务办法时,该办法都将开端一个业务。
- 正告:不要过度的在范畴模型上运用业务,咱们有必要稳重的规划聚合以确保事确的共同性鸿沟。
资源库VS数据拜访目标(DAO)
- DAO首要从数据库表的视点看待问题,而且供给CRUD操作。Martin Fowler将DAO相关的设施与范畴模型别离开来对待。他指出比方“表模块”,“表数据网关”和”活动记载“这样的形式应该用于业务脚本中。这些与DAO相关的形式仅仅对数据库表的一层封装。
- 资源库和"数据映射器"则更倾向于目标,一般被运用于范畴模型。
- DAO形式中所履行的CRUD操作都是能够放在聚合中完结的,要防止在范畴模型范畴模型中运用DAO形式
- 在规划资源库时咱们应该选用面向调集的办法,而不是面向数据拜访的办法,这有助于你将自己的范畴当作模型来看待,而不是CRUD操作。
集成限界上下文
范畴服务接口坐落范畴模型层(六边形内部),完结位为根底设施层(六边形外部,即端口和适配器所在位置)。
运用服务
运用服务是用来表达用例和用户故事(User Story)的首要手法。
运用层经过运用服务接口来露出体系的悉数功用。在运用服务的完结中,它担任编列和转发,它即将完结的功用托付给一个或多个范畴目标来完结,它自身只担任处理业务用例的履行次序以及成果的组装。经过这样一种办法,它躲藏了范畴层的杂乱性及其内部完结机制。
运用层相对来说是较“薄”的一层,除了界说运用服务之外,在该层咱们能够进行安全认证,权限校验,耐久化业务操控,或许向其他体系产生根据作业的音讯告知,别的还能够用于创立邮件以发送给客户等。
运用层作为展示层与范畴层的桥梁。展示层运用VO(视图模型)进行界面展示,与运用层经过DTO(数据传输目标)进行数据交互,然后到达展示层与DO(范畴目标)解耦的意图。
总结
谈谈我对 DDD 的了解,我觉得 DDD 不像一门技术,我了解的技术比方高并发、缓存、音讯行列等,DDD 更像是一项软技术,一种办法论,包括了许多规划理念。
这篇文章写于上一年,所以其时对 DDD 了解的其实还不行深化,本年做过一些 DDD 的项目,所以现在对 DDD 的了解又加深了几分。
咱们不要以为,把握了一些概念,以及 DDD 的根本思维,就把握了 DDD,然后做项目时,照葫芦画瓢,这样你会死的很惨!
只把握 DDD 外表的东西,其实是不行的,我觉得 DDD 最杂乱的当地,其实是在它的范畴规划部分,项目发动前,你一定要规划各个范畴目标,以及它们直接的交互联系。
比方咱们之前做过一个项目,由于这块没有做好,咱们一边写代码,一边还在考虑,这个范畴目标该怎么结构,严重影响开发功率,最终又不得不回退到 MVC 的形式。
不要为了炫技,啥都要搞个 DDD,两者怎么挑选:
MVC:上来就能够开干,短平快,前期用起来很香,全体开发功率也更高,所以关于紧迫,或许不那么重要的项目,我会直接用 MVC 怼,欠好的地便利是,后面会越来越杂乱,或许最终便是一坨屎山,可是许多时分,比方老板进展催的紧,我哪想到那么多今后呢?
DDD:前期需求花很多时刻规划好范畴模型,关于一些根底组件,或许一些中心服务,假如目标模型十分杂乱,主张选用 DDD,前期或许会略微苦楚一些,可是后期保护起来会十分便利。