围炉网

一行代码,一篇日志,一个梦想,一个世界

架构师系列——领域驱动设计相关资料的读书笔记

  • 领域模型的特点:

    • 细粒度的类,易于扩展,容易复用

    • 可以应对复杂的业务逻辑

    • 需要经验才能掌握

    • 复杂领域模型使用了继承、组合、设计模式等各种手段

            

  • DDD分层架构各层职能如下:

    • 展现层

      • 展现层负责向用户显示信息和解释用户指令。

    • 应用层

      • 应用层是很薄的一层,主要面向用户用例操作,协调和指挥领域对象来完成业务逻辑。应用层也是与其他系统的应用层进行交互的必要渠道。应用层服务尽量简单,它不包含业务规则或知识,只为下一层的领域对象协调任务,使它们互相协作。应用层还可进行安全认证、权限校验、分布式和持久化事务控制或向外部应用发送基于事件的消息等。

    • 领域层

      • 领域层是软件的核心所在,它实现全部业务逻辑并且通过各种校验手段保证业务正确性。它包含业务所涉及的领域对象(实体、值对象)、领域服务以及它们之间的关系。它负责表达业务概念、业务状态以及业务规则,具体表现形式就是领域模型。

    • 基础层

      • 基础层为各层提供通用的技术能力,包括:为应用层传递消息、提供API管理,为领域层提供数据库持久化机制。它还能通过技术框架来支持各层之间的交互。

  • 在领域层有什么东西

    • 实体(Entity)

      • 对象不是由属性定义的,而是标志定义的

      • 对象内容的变化不会影响标识符

      • 无论保存到硬盘,装入内存,通过网络发送,标识符都不变

      • 实体Entity具有全局唯一标识

      • 实体中的属性只能是值对象

      • 实体中不能调用第三方服务,也不能调用DAO,保证实体依赖最小,让实体处于一个好验证的状态(可在领域服务中调用第三方方法)

      • 实体的方法的入参也必须是值对象

      • 实体得提供明确的构造方法,最好不要偷懒打上@Data注解(实体的构造方法里往往有很多逻辑)

      • 实体得足够内聚,是业务的主要载体,聚合根是业务的入口

      • 不同上下文中的实体业务逻辑不要写在一个实体中

    • 值对象(Value Object)

      • 对象是根据值来确定的

      • 可以在不同的实体中使用

      • 值对象通常是不可变的

      • Color, Point, Money, Address

    • 领域服务

      • 有些领域逻辑是动词,表示了一种重要的行为很难映射为对象,无法归结到实体和值对象当中

      • 例如:转账

      • 操作是无状态的

      • 比如涉及到跨越多个实体的操作或者涉及到外部系统的交互适合放到领域服务

    • 聚合

      • 聚合表示逻辑上联系很紧密的对象,每个聚合都有一个根,有一个边界

      • 聚合根控制了对这个聚合内所有对象的访问,外部要想内部对象,必须通过聚合根才行

      • 特点

        • 根实体有全局标识(例如order id)

        • 聚合内的实体有本地标识(只在聚合内部是唯一的,例如LineItem的序号)

        • 聚合外部的对象不能直接引用除了根之外的对象

          • 可以把一个值对象的copy 传递给外界,该副本和聚合没有关联

        • 只有聚合的根才能通过数据库被加载

        • 不要为每个实体创建一个仓储。仓储最好只有一个byId()和Save(),最多可能会有一些byBizId()直接的方法。仓储只是封装聚合根、实体和持久层交互的逻辑,和DAO不一样,更细的查询、保存交给DAO

        • 删除操作会把聚合内的所有对象删除

        • 当对聚合边界内任何对象做修改时,聚合内的规则必须被满足(例如Order的总价格和各个LineItem的价格和要相等)

    •  资源库(Repository)

      • 工厂负责创建对象

      • 资源库从存储中重建对象

        • 不分配唯一的标识,标识是输入参数的一部分

      • 资源库“说”的是领域语言

  • 领域驱动设计的理念

    • 架构设计的理念是分层、分治。

    • 高内聚,低耦合

      • 高内聚是指一个软件模块是由相关性很强的代码组成,只负责一项任务,也就是常说的单一责任原则。

      • 内聚:每个模块尽可能独立完成自己的功能,不依赖于模块外部的代码。

      • 耦合:模块与模块之间接口的复杂程度,模块之间联系越复杂耦合度越高,牵一发而动全身。

    • 目的:使得模块的“可重用性”、“移植性”大大增强

    • 领域模型不是要建立一个尽可能符合“现实”的模型

    • 领域专家(业务专家)必须要参与到模型的建立中来。需要有一种通用的语言,让团队在交流时能达到一致。

  • Evic Evans在著作中将软件系统的设计分为2个部分:战略设计和战术设计。

    • 在战略设计层面提出了域、子域、限界上下文等重要概念;

    • 在战术设计层面提出了实体、值对象、领域服务、领域事件、聚合、工厂、资源库等重要概念。如图1所示:

    • 战略设计部分指导我们如何拆分一个复杂的系统,战术部分指导我们对于拆分出来的单个子系统如何进行落地,在落地过程中应该遵循哪些原则。

  • 贫血模型

    • 像 UserBo 这样,只包含数据,不包含业务逻辑的类,就叫作贫血模型(Anemic Domain Model)。同理,UserEntity、UserVo 都是基于贫血模型设计的。这种贫血模型将数据与操作分离,破坏了面向对象的封装特性,是一种典型的面向过程的编程风格。

  • 防腐层(Anti-corruption layer,简称 ACL)

    • 介于新应用和遗留应用之间,用于确保新应用的设计不受老应用的限制。是一种在不同应用间转换的机制。

    • 创建一个防腐层,以根据客户端自己的域模型为客户提供功能。该层通过其现有接口与另一个系统进行通信,几乎不需要或不需要对其进行任何修改。因此,防腐层隔离不仅是为了保护您免受混乱的代码的侵害,还在于分离不同的域并确保它们在将来保持分离。

    • 防腐层是将一个域映射到另一个域,这样使用第二个域的服务就不必被第一个域的概念“破坏”。

    • 如果新旧系统之间没有重要的语义差异,则此模式可能不适合。

  • 四色建模方法(color modeling)建立的一套领域模型。我们建模的次序和重点:

    • 首先以满足管理和运营的需要为前提,寻找需要追溯的事件。

    • 根据这些需要追溯,寻找足迹以及相应的时标性对象。

    • 寻找时标性对象周围的人、事、物。

    • 从中抽象角色。

    • 把一些信息用描述对象补足。

  • 限界纸笔建模法的3点优势

    • 划分核心领域有助于“分而治之”:一旦确定了核心领域,限界上下文也就确定了,不同的限界上下文之间通过“翻译器”来彼此沟通并屏蔽干扰,这样就避免了“大泥球”的设计,并有助于演进到微服务架构。

    • “聚集根”有助于数据完整性:每个限界上下文都有一个“聚集根”的概念,外界对其下属概念的访问都必须通过它来进行,这样既方便定位职责,也有助于增强数据的完整性。

    • 用“纸和笔”画恰好够用的概念有助于避免过度设计:每个限界上下文中要管理的概念,都是通过“倒退到没有电脑而用纸和笔的时代如何管理”来引导出来的,用纸和笔来记录,能促使人避免写过多的信息,而只写限界上下文中恰好够用的概念。

  • 在领域模型中,有些业务操作并不能自然地放在实体或值对象上,此时我们可以使用无状态的领域服务

  • 领域事件(Domain Event)表示领域模型中发生的重要事件。有多种方式可以对领域事件进行建模。在对聚合进行命令操作时,聚合本身将发布领域事件

  • 可以将限界上下文看成是整个应用程序之内的一个概念性边界。这个边界之内的每种领域术语、词组或句子——也即通用语言,都有确定的上下文含义。在边界之外,这些术语可能表示不同的意思。

  • 通用语言是团队自己创建的公用语言,团队中同时包含领域专家和软件开发人员。

  • Domain Primitive

    • 是一个在特定领域里,拥有精准定义的、可自我验证的、拥有行为的Value Object。

    • DP的第一个原则:Make Implicit Concepts Expecit将隐性的概念显性化

    • DP的第二个原则:Make Implicit Context Expecit 将隐性的上下文显性化

    • DP的第三个原则:Encapsulate Multi-Object Behavior 封装多对象行为

  • 什么情况下应该用Domain Primitive

    • 常见的DP的使用场景包括:

    • 有格式限制的String:比如Name,PhoneNumber,OrderNumber,ZipCode,Address等

    • 有限制的Integer:比如OrderId(>0),Percentage(0-100%),Quantity(>=0)等

    • 可枚举的int:比如Status(一般不用Enum因为反序列化问题)

    • Double或BigDecimal:一般用到的Double或BigDecimal都是有业务含义的,比如Temperature、Money、Amount、ExchangeRate、Rating等复杂的数据结构:比如Map<String, List<Integer>>等,尽量能把Map的所有操作包装掉,仅暴露必要行为

  • DDD代码框架

    • Types 模块

Types模块是保存可以对外暴露的Domain Primitives的地方。Domain Primitives因为是无状态的逻辑,可以对外暴露,所以经常被包含在对外的API接口中,需要单独成为模块。Types模块不依赖任何类库,纯 POJO 。

    • Domain 模块

Domain 模块是核心业务逻辑的集中地,包含有状态的Entity、领域服务Domain Service、以及各种外部依赖的接口类(如Repository、ACL、中间件等。Domain模块仅依赖Types模块,也是纯 POJO 。

    • Application模块

Application模块主要包含Application Service和一些相关的类。Application模块依赖Domain模块。还是不依赖任何框架,纯POJO。

    • Infrastructure模块

Infrastructure模块包含了Persistence、Messaging、External等模块。比如:Persistence模块包含数据库DAO的实现,包含Data Object、ORM Mapper、Entity到DO的转化类等。Persistence模块要依赖具体的ORM类库,比如MyBatis。如果需要用Spring-Mybatis提供的注解方案,则需要依赖Spring。

    • Web模块

Web模块包含Controller等相关代码。如果用SpringMVC则需要依赖Spring。

    • Start模块

Start模块是SpringBoot的启动类。

  • 六边形架构

    • 虽然现在它的名字已经变为端口与适配器,DDD社区依然使用“六边形”作为该架构的名字。也有人称这种架构为Onion架构。

    • 六边形每条不同的边代表了不同种类型的端口,端口要么处理输人,要么处理输出。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注

沪ICP备15009335号-2