知行合一--DDD--eis platform
目标:记录DDD 在 platform 项目中的实践
- 架构采用 六边形架构 端口与适配器
- 根据用例来设计应用程序。
- 具有对称性特征的架构风格,不同的客户通过“平等”的方式与系统交互。 添加新客户时,只需要添加一个新的适配器将客户输入转化成能被系统API 所理解的参数就行了。
- 对于特定的输出,都有一个新建的适配器负责完成相应的转化功能。
- 使用依赖注入的架构自然地具有了 端口与适配器 风格。
- 六边形架构存在两个区域:
- “外部区域”,不同的客户均可以提交输入;
- “内部区域” 系统则用于获取持久化数据,并对程序输出进行存储(比如数据库),或者在中途将输出转发到另外的地方。
- 每种类型的客户端都有自己的适配器,将客户输入转化为程序内部API 所能理解的输入,六边形每条不同的边代表不同类型的端口,端口要么处理输入,要么处理输出。可能有的请求使用 HTTP协议(浏览器,REST和 SOAP 等),或使用AMQP协议,如RabbitMQ。 无论何种方式对端口划分,当客户请求到达时,都应该有相应的适配器对输入进行转化,然后端口将调用应用程序的某个操作或者向应用程序发送一个事件,控制权由此交给内部区域。
- 以端口为HTTP ,适配器想成是 Java 的 servlet 或 JAX-RS 的 REST 请求处理类。 或者,可以为NServiceBus 或 RabbitMQ 创建消息监听器,此时,端口是消息机制,而适配器是消息监听器,因为 消息监听器 将负责从消息中提取数据, 并将数据转化为应用层 API (领域模型的客户) 所需的参数.
- 所有的适配器都使用相同的API
- 应用程序通过公共API接收客户请求。应用程序边界,即内部 六边形,也是用例边界。当应用程序通过API 接收到请求时, 它将使用领域模型来处理请求,其中便 包括对业务逻辑的执行。应用层API 通过应用服务的方式展现给外部。
- 资源库的实现看作是持久化适配器,该适配器用于访问先前存储的聚合实例,或者保存新的聚合实例。可通过不同方式实现:RDBMS, 基于文档的存储,分布式缓存和内存存储。
- 如果应用程序向外部发送领域事件消息,将使用适配器H处理,其处理消息输出。
- 六边形架构可以为 SOA架构,REST 或事件驱动架构,或者是 数据网织 或 基于网格的分布式缓存;可能采用 Map-Reduce 这种分布式并行处理方式 提供坚实的支撑基础。

精要:
- 构建富含领域逻辑模型,避免“贫血模型”
- 应用层的每个方法对应着一个用例流,不要在一个方法中包含多个用例流。
- Specification 逻辑的整理。
- RPC 更容易产生有损性能的时间延迟,并且有可能导致调用彻底 失败。网络和远程系统的加载过程都是RPC产生延迟的原因。要采用异步请求,或者事件处理的方式,达到更高的自治性.
- DDD 的做法是:在本地创建一些由外部模型翻译而成的领域对象,这些对象保留着本地模型所需的最小状态集。为了初始化这些对象,只需要有限的RPC调用或REST请求,但是,要与远程模型同步,最好是在远程系统中采用面向消息的通知(notification)机制。消息通知可以通过服务总线进行发布,也可以采用消息队列或者REST.
- 基于订阅源 feed 通知的 工作机制
- 消息机制,处理资源不可用的一个好办法便是将其显现出来, 由标准类型实现的 状态模式 State 模式. Enum。 此时的状态是一个值对象;使用资源可用性状态的好处(ui 显式)的好处包括技术和商业.
- 防腐层 ACL 根据需要可以是多个,第二个防腐层来处理与别的上下文的集成.
- 所有依赖于远程资源的本地聚合,采用异步组件,不管是RPC 客户端还是消息处理器-- 调用 self , attachXXX() 方法--传入的参数是远程新创建的 xxx 值对象。
- 用例驱动架构,DIP 原则和 Hexagonal 六边形架构 来改进 分层架构,对RPC 和 REST 支持,数据网织(Data Fabric ) 或 基于网格的分布式缓存 (Grid-Based Distributed Cache) 和 事件驱动风格, DDD 新架构模式-CQRS
- 分层,较低层与叫高层耦合 的情况,只局限于采用 观察者(Observer)模式或者 调停者 (Mediator)模式, 较低层是绝对不能直接访问较高层的。例如 调停者模式时,较高层可能实现了较低层定义的接口,然后将实现对象作为参数传递到较低层,当较低层调用该实现时,并不知道实现出自何处。
- 如果用户界面使用了领域模型的对象,此时的领域对象仅限于数据的渲染展现。可使用 展现模型 对用户界面和领域对象进行解耦。
- 用户可能是人,也可能是其他的系统,有时用户界面层将采用 开放主机服务 的方式对外提供 API.
- 用户界面是应用层的直接用户.
- 应用服务 不同于 领域服务, 应用服务用于控制持久化事务 和 安全认证, 或向其他系统 发送 基于事件的消息通知,还可以用于创建邮件以发送给用户。 不处理业务逻辑,却是领域模型的直接客户。用于协调对领域对象的操作,如聚合。是表达用例和用户故事 的主要手段。因此: 通常 接收来自用户界面的输入参数,再通过资源库 获取到聚合实例,然后执行相应的命令操作。
- 需要创建聚合时, 应用服务使用 工厂 或聚合的构造函数来实例化对象,然后采用资源库对其进行持久化。 应用服务还可以调用 领域服务 来完成和领域相关的任务操作。但此时的操作应该是无状态的。
- 当领域模型用于发布领域事件时,应用层可以将订阅方注册到任意数量的时间上,这样的好处是可以对事件进行存储和转发,领域模型只需要关注自己的核心逻辑,领域事件发布器也可以保持轻量化,而不用依赖消息机制的基础设施.
- DIP 依赖倒置原则, 高低都依赖抽象, 抽象不依赖细节,细节依赖抽象, 基础设施 实现所有在其他层中定义的接口。使领域层和基础设施层都只依赖与 由领域模型所定义的抽象接口。
- 应用层是领域层的直接客户,将依赖于领域层接口,并且间接地访问资源库 和 由基础设施提供的实现类 ,应用层可以采用不同方式或者这些实现:DI, 服务工厂 和 插件,
概念:
- 限界上下文主要用来封装 通用语言 和 领域对象,同时也包含了那些为 领域模型提供交互手段和辅助功能的内容。
重点:
- 一个系统的使用者并不只是人,还可能是别的计算机系统。系统有可能存在诸如 Web服务 ( Web Service ) 之类的组件。也可以使用 REST 资源来与模型交互,此时的 REST 资源即被称为 开放主机服务 (open host servce ) ,也可使用 SOAP 或 消息服务断电。 哪些面向服务的组件都应该位于上下文边界之内.
- 用户界面 和 面向服务端点 都会将操作 委派给 应用服务, 应用服务包含了不同类型的服务,如 安全 和 事务管理。
- 对于模型来说,应用服务是 门面模式, 同时 应用服务还具有 任务管理 功能, 将来自 用例流 的 请求 转换成 领域逻辑的 执行流。 应用服务位于上下文边界之内.
- 两个上下文集成式 防腐层的内部工作机制 p92
- RESTfull HTTP 服务器的关键方面
- 资源式关键的概念:暴露给外界的“东西”, 一个唯一的身份标识。例如:每一个客户,产品,产品列表,搜索结果和每次对产品目录的修改都应该分别作为一种资源。资源具备 展现 representation 和 状态 的.展现的格式可能不同:客户端通过资源的展现与服务端交互,格式可以为 XML, JSON ,HTML 或 二进制数据。
- 无状态通信,采用具有自描述功能的消息,此时,HTTP请求便包含了服务器所需的所有信息。服务器也可以用其本身的状态来辅助通信,重要的是:不能依靠请求本身来创建一个隐式上下文环境(对话)。无状态通信保证了不同请求之间的相互独立性,很大程度上提高了系统的可伸缩性.
- 资源拥有什么样的接口。可以调用的方法集合是固定的,每个对象都支持相同的接口。对象方法可以表示为 可操作资源的HTTP动词,其中最重要的有GET, PUT, POST 和 DELETE. 不同于CRUD, 不表示任何持久化实体,而是封装了某种行为,HTTP动词的应用表示行为调用。
- GET 方法表示“安全”的操作: 1 可能完成一些客户并没有要求的动作行为; 2 总是读取数据;3 可能被缓存起来;
- 有些HTTP 方法是 幂等的, 可以安全地对失败的请求进行重试,包括 GET, PUT, DELETE.
- Hypermedia ,REST 服务器的客户端可以沿着某种路径发现应用程序可能的状态变化。不同资源是相互链接在一起的.
- RESTfull HTTP 客户端的关键方面 p119
- client 可以通过两种方式在不同资源之间进行转移,一种是超媒体,一种是服务器端的重定向。 Server 和 Client 协同工作以动态地影响客户端的分布式行为。URI 包含了对地址的解引用的所有信息,包括主机名和端口,客户端可根据超媒体链接访问到不同应用程序,不同主机,甚至不同公司的资源。
- 浏览器不算自给自足,需要人为操作,但客户端程序可以。
不建议将 领域模型直接暴露给外界,会使系统变得非常脆弱,原因在于对领域模型的每次改变都会导致对系统接口的改变. 如果要结合。
- 为系统接口层单独创建一个限界上下文,在此上下文中通过适当的策略来访问实际的核心模型。经典方法。将系统接口看作一个整体,通过资源抽象将系统功能暴露给外界,而不是通过服务或远程接口。 专属系统。
- 用于需要使用标准媒体类型的时候,如果某种媒体类型并不用于支持单个系统接口,而是用于一组相似的客户端-服务器交互情景,可以创建一个领域模型来处理每一种媒体类型。 这样的领域模型甚至可以在服务器和客户端重用。本质为DDD中的共享内核或发布语言。
实践提示:
- 用户和权限 user permission 是与身份(identity) 和 访问 (Access)相关的概念,即是与安全 Security 相关的. Identity And Access Context 身份与访问上下文。将用户和全限放到支撑子域或通用子域,另外的核心域也会用到。
- 一个上下文中(用户权限上下文)中的 User 和 Role 信息被另一个限界上下文用来创建 Mederator 对象.
- 当模型驱动着数据库Schema 的设计时,此时的数据库Schema也应该位于该模型所处的上下文中,因为数据库Schema是由建模团队设计,开发并维护的。意味着数据库中的表和列的名字应该和模型的名字保持一致。


如果数据库 Schema 已经存在,或者另有一个专门的数据建模团队要求有别于模型的数据库Schema 设计,此时的Schema便不能和模型位于同一个限界上下文了。
实体唯一标识
实体设计早期,关注在能体现实体身份唯一性的主要属性和行为上,同时还将关注如何对实体进行查询。不要一开始便关注实体的属性和行为.
值对象可用于存放实体的唯一标识。其不变性可以保证实体身份的稳定性,且与身份标识相关的行为可以得到集中处理。可避免将身份标识相关
的行为泄露到模型的其他部分或者客户端中.
常见策略:
1. 用户提供一个或多个初始唯一值。程序要保证
2.程序内部算法。
3.依赖于持久化存储,如数据库生成唯一标识。
4.另一个限界上下文的传输。

1. 用户提供唯一标识 采用值对象更好。
系统需要采用无故障的方法来保证用户输入的的确是唯一的身份标识, 采用基于工作流的标识审批过程,对于生成具有可读性的身份标识是必要的。
此额外的阶段来保证身份标识的质量是值得的。
UUID PROJECT-P-04-10-2021-FASX323 project项目名字- 创建一个product - 日期 - UUID 截取
String rawId = "EIS-P-04-10-2021-FAX2354";
ProductId productId = new ProductId(rawId);
....
Date productCreationDate = productId.creationDate();
客户可以询问标识的细节信息,如一个Product创建时间。 信息已方便地包含在标识中,客户不需要原始格式.
此时聚合根 Product 可以通过 creationDate() 方法向外界暴漏该Product 的创建时间,而客户并不需要知道对创建时间的获取细节。

Apache Commons 项目-- Commons Id 组件, 提供了5中标识生成器。
Nosql MongoDB 用于生成唯一标识。
程序生成的标识, 创建标识的工厂对象,
聚合根的唯一标识,可采用资源库来生成:

- 如果用户界面 User Interface 被用于渲染模型,且驱动着模型的行为设计时,用用户界面也属于模型所在的上下文边界之内。但不应该在UI中对领域建模,将导致贫血领域对象。要拒绝使用智能UI反模式。
- 安全管理机制作为单独的限界上下文,可集中化和重用化。
- 每一个协作功能不必创建各自的限界上下文,每一种工具都可以被部署为一个自治的组件,实际上为每一个协作工具创建单独的JAR文件。使用Jigsaw模块化功能,为每种工具都创建了基于版本的部署单元。除了这些自然分离的JAR文件之外,还需要另一个JAR文件用于部署共享的模型对象,比如Author,...这种方式有助于建立一套统一的通用语言。同时对架构和应用程序管理来说也是有益的.
- RESTfull HTTP HATEOAS 成熟度3
项目:
在实施某个解决方案之前,需要对问题空间和解决方案空间进行评估。
1.这个战略核心域的名字是什么,目标是什么?
2.这个战略核心域中包含哪些概念?
3.这个核心域的支撑子域和通用子域是什么?
4.如何安排项目人员
5.你能组建出一只合适的团队吗?
目录:
1. repository :
可通过对象之间的关联来找到对象。但当它处于生命周期的中间时,必须要有一个起点,以便从这个起点遍历到一个 Entity 或 Value.
1. 创建对象,获得该新对象的引用。
2.遍历关联到该对象,得有一个起点。
3. 基于对象的属性,执行查询来找到对象;或者是找到对象的组成部分,然后重建它。
当客户代码直接使用数据库时,开发人员试图会绕过模型的功能,如 Aggregate,甚至是对象封装,而直接获取和操作他们所需的数据。
这将导致越来越多的 领域规则 被嵌入到查询代码中,或者干脆丢失了。
要通过Aggregate 的根来得到这些对象,避免领域逻辑进入查询和客户代码中,导致 Entity 和 Value Object 变成单纯的数据容器。
采用大多数处理数据库访问的技术复杂性会使客户代码变得混乱,将导致开发人员简化领域层,最终使模型变得无关紧要.
最重要的是:除了通过 根 来遍历查找对象这种方法意外,禁止用其他方法对 Aggregate 内部的任何对象进行访问。
持久化的Value object 一般可以通过遍历某个 Entity找到,在这里 Entity 就是把对象封装在一起的 Aggregate 的根。
所有的持久化对象中,有一小部分必须通过基于对象属性的搜索来全局访问。当很难通过遍历方式来访问某些 Aggregate 根的时候,就需要
使用 全局搜索访问数据库。
通常是 Entity, 有时是具有复杂内部结构的 Value Object, 还可能是枚举Value.其他对象不宜使用这种访问方式,会混淆它们之间的重要区别.
随意的数据库查询会破坏领域对象的封装 和 Aggregate.
技术基础设施 和 数据库访问机制的暴露会增加客户的复杂度,并妨碍模型驱动的设计.
Repository 将某种类型的所有对象都表示为一个概念集合(通常是模拟的). 它的行为类似于 集合 collection, 只是具有更复杂的查询功能。
在添加或删除相应类型的对象时,Repository的后台机制负责将对象添加到数据库,或是从中删除对象。
这个定义将一组职责集中在一起,这些职责提供了对Aggregate 根的整个生命周期的全程访问.
客户使用查询方法项Repository 请求对象,这些查询方法根据客户所指定的条件(通常时特定属性的值)来挑选对象。
Repository 检索被请求的对象,并封装数据库查询和元数据映射机制。
Repository 可以根据客户所要求的各种条件来挑选对象。也可以返回汇总信息,如有多少个示例满足查询条件。
Repository甚至能返回汇总计算,如所有匹配对象的某个数值属性的纵隔。
Repository 使客户只需要与一个简单的,易于理解的接口进行对话,并根据模型向这个接口提出它的请求。
要实现所有这些功能需要大量复杂的技术基础设施,但接口很简单,而且在概念层次上与领域模型紧密联系在一起.
为每种需要全局访问的对象类型创建一个对象,这个对象相当于该类型的所有对象在内存中的一个集合的“替身”。通过一个众所周知
的全局接口来提供访问。提供添加和删除对象的方法。用这些方法封装在数据存储中实际插入和删除的操作。
提供根据具体条件来挑选对象的方法,并返回属性值满足查询条件的对象或对象集合(所返回的对象是完全实例化的),
从而将实际的存储技术和查询技术封装起来。
只为那些缺失需要直接访问的Aggregate 根 提供 Repository. 让用户始终聚焦于模型,
而将所有对象的存储和查询技术封装起来。只为那些确实需要直接访问的Aggregate根提供 Repository。让用户始终聚焦于模型,
而将所有对象的存储和访问操作交给Repository来完成.
优点:
1.为客户提供了一个简单的模型,用来获取持久化对象并管理它们的生命周期。
2.使应用程序和领域设计 与 持久化技术 (多种数据库策略甚至是多个数据源)解耦。
3.体现了有关对象访问的设计决策。
4.很容易将他们替换为“哑实现 (dummy implementation), 以便在测试中使用(通常是内存的集合).
Repository 的查询
基于Specification ,客户使用规则来描述(指定)需要什么,而不必关心如何获得结果。

Repository 设计采用灵活的查询方式,也应该允许添加专门的硬编码查询,作为便捷的方法,封装常用查询或不返回对象
(如返回的是选中对象的汇总计算)的查询,
根据所使用的持久化技术和基础设施不同. Repository 的实现也将有很大的变化,
理想的实现是向客户隐藏所有内部工作细节。不管数据是存储在对象数据库,关系型数据库,或是内存,客户代码都相同。
将 存储,检索,和查询 机制封装起来。 Hibernate Criteria, Specification
流程
client -> repository : 客户根据模型来请求它所需的对象
1. trackingId("t123");
tradeOrderRepository -> DataBaseInterface :
2. search(an SQL Query String) "select * from trade_order where trracking_id = "t123";
databaseInterface -> tradeOrderRepository : return an SQL ResultSet
tradeRepository -> SQL TradeOrderFactory
3. reconstitute ( an SQL ResultSet )
SQLTraderOrder Factory 3.1 create 实例化对象
注意事项:
1. 对类型进行抽象。 Repository "含有“ 特定类型的所有实例,并不意味着每个类都需要有一个 repository 。
类型可以是一个层次结构中的 抽象超类, (TradeOrder 可以是 BuyOrder 或 SellOrder )。 类型可以是一个接口--接口的实现并没有层次结构上的关联,
也可以是一个具体类。由于数据库缺乏这样的多态性质,将面临很多约束。
2. 充分利用与客户解耦的优点。如果客户直接调用底层机制,很难修改。 利用解耦来优化性能。可以使用不同的查询技术,或在内存中缓存对象。可随时自由切换。方面测试。
3. 将事务的控制权留给客户。
虽然保存数据后紧接着就提交事务似乎很自然,但只有客户才有上下文。从而能够正确地初始化和提交单元。repositoy 不插手事务控制。
为关系数据库设计对象
如果 database schema 是专门为对象存储设计的,可以接受模型的一些限制。
在更新数据时更安全地保证聚合的完整性。使数据更新更加高效。
关系表的设计不必反映出领域模型。映射工具已经很完善,可消除二者之间的巨大差别.
并不需要一个对象/一个表的映射。
依靠ORM ,可以实现一些聚合或对象的组合。注意 映射要透明。
对象系统外部的过程不要访问这样的对象存储,会锁定数据模型。难以修改。
简单的对应关系: 表中的一行应该包含对象,也可能还包含 Aggregate 的一些附属项。
表中的外键应该转换为 对另一个 Entity 对象的引用.
有时不得不违背这种简单的对应关系,但不要全盘放弃简单映射原则。
IDEA , 一个限界上下文通常就是一个工程项目。
项目的源代码可以只包含领域模型,也可以包含一些周边的层或六边形区域.
顶层包名通常表示限界上下文中顶层模块的名字。例如, 对于 最优选 限界上下文
com.mycompany.optimalpurchasing
根据架构职责做进一步分解,下面是二级包名:
com.mycompany.optimalpurchasing.presentation
com.mycompany.optimalpurchasing.application
com.mycompany.optimalpurchasing.domain.model
com.mycompany.optimalpurchasing.infrastructure
即使是这样模块化的拆分,团队依然只工作在一个限界上下文中.
至少存在4个以DDD的 二级模块表示的模块。
可能从技术层面上将一个限界上下文放在一个JAR / WAR / EAR 文件。
松耦合的领域模型应该放在不同的JAR文件中,这样我们可以按照版本号对领域模型进行单独部署。对于大型
模型来说,这种做法很有用,将单个大模型分成多个JAR文件也有助于版本管理,(OSGI ,Jigsaw模块)
不同的高层模块,包括它们的版本和依赖都可以通过捆包/模块 bundles / modules 进行管理。
实践指引 之 身份与访问上下文
采用标准的DDD集成技术,该上下文可以被其他限界上下文所使用,对于消费方来说,身份与访问上下文是一个通用子域
每一个租户及其拥有的每一个对象资产都有唯一的身份标识,这在逻辑上将不同的租户分离开来。
系统的使用者只有在收到邀请时自行向系统注册.
系统通过认证服务来保证安全访问,而密码通常是被高度加密过的。
用户群和嵌套群可以在整个组织范围之内完成复杂的身份管理。
通过基于角色的权限机制来管理对系统资源的获取,这种方式是简单的,优雅的,同时又是功能强大的.
当有我们关心的状态由于模型行为而发生改变时,系统将发布 领域事件。 领域事件采用 “名词+动词” 形式命名,
动词是过去分词形式, UserPassswordChanged , PersonNameChanged.

迪米特法则
任何对象的任何方法只能调用以下对象中的方法
1. 该对象自身
2. 所传入的参数对象
3. 它所创建的对象
4. 自身所包含的其他对象,且对那些对象有直接访问权
告诉而非询问 原则
客户端对象不应该 首先询问服务对象,
然后根据询问的结果 调用对象中的方法,
而是应该通过调用服务对象的公共接口的方式来“告诉” 服务对象所要执行的操作。


浙公网安备 33010602011771号