为了账号安全,请及时绑定邮箱和手机立即绑定

领域建模和设计

标签:
Java

 

领域建模和设计的重要性:

Eric Evans将其定义为领域驱动设计(Domain-Driven Design,简称DDD)  方法论 


服务模块之间过渡耦合

随着迭代的不断演化,业务逻辑变得越来越复杂,我们的系统也越来越冗杂。模块彼此关联,谁都很难说清模块的具体功能意图是啥。例如: 

订单服务模块中 具有以下接口 订单存储(数据表)

订单接口 订单字段

评价接口 评价字段

支付接口 支付相关字段

保险接口 保险相关字段等等。。。

订单服务模块中的订单接口中提供了查询、创建订单相关的接口,也提供了订单评价、支付、保险的接口。

同时我们的表也是一个订单大表,包含了非常多字段。

在我们维护代码时,牵一发而动全身,很可能只是想改下评价相关的功能,却影响到了创单核心路径。

虽然我们可以通过测试保证功能完备性,但当我们在订单领域有大量需求同时并行开发时,改动重叠、恶性循环、疲于奔命修改各种问题。

上述问题,归根到底在于系统架构不清晰,划分出来的模块内聚度低、高耦合。 解决方案:

按照演进式设计的理论:让系统的设计随着系统实现的增长而增长。我们不需要作提前设计,就让系统伴随业务成长而演进。这当然是可行的,敏捷实践中的重构、测试驱动设计及持续集成可以对付各种混乱问题。

重构——保持行为不变的代码改善清除了不协调的局部设计,测试驱动设计确保对系统的更改不会导致系统丢失或破坏现有功能,持续集成则为团队提供了同一代码库。

在这三种实践中,重构是克服演进式设计中大杂烩问题的主力,通过在单独的类及方法级别上做一系列小步重构来完成。我们可以很容易重构出一个独立的类来放某些通用的逻辑,但是你会发现你很难给它一个业务上的含义,只能给予一个技术维度描绘的含义

重构可以解决问题,但是对新同学不太友好,新同学并不总是知道对通用逻辑的改动或获取来自该类。显然,制定项目规范并不是好的idea。我们又闻到了代码即将腐败的味道。



领域模型:则表达与业务相关的事实(从业务中抽象出领域模型),将数据和行为封装在一起    贫血模型(失血模型)和充血模型介绍

 

贫血领域对象(贫血模型)

贫血领域对象(Anemic Domain Object)只是仅用作数据载体,而没有行为和动作的领域对象。 

失血模型简单来说,就是domain object只有属性的getter/setter方法的纯数据类,所有的业务逻辑完全由business object来完成,这种模型下的domain object被Martin Fowler称之为“贫血的domain object”

 

过程式代码:

习惯了三层接口开发 mvc模式(Action/Service/DAO这种分层模式),使用这种开发方式,对象只是数据的载体,没有行为。以数据为中心,以数据库ER设计作驱动。   分层架构在这种开发模式下,可以理解为是对数据移动、处理和实现的过程。

举例: 抽奖平台设计: 场景需求 奖池里配置了很多奖项,我们需要按运营预先配置的概率抽中一个奖项。 实现非常简单,生成一个随机数,匹配符合该随机数生成概率的奖项即可。

奖项

奖池

可以发现:在业务领域里非常重要的抽奖,我的业务逻辑都是写在Service中的,Award充其量只是个数据载体,没有任何行为。

简单的业务系统采用这种贫血模型和过程化设计是没有问题的,但在业务逻辑复杂的情况下,业务逻辑、状态会散落到在大量方法中,原本的代码意图会渐渐不明确,我们将这种情况称为由贫血症引起的失忆症。    由贫血症 引起的失忆症

用领域模型的开发方式,将数据和行为封装在一起,并与现实世界中的业务对象相映射。各类具备明确的职责划分,将领域逻辑分散到领域对象中。继续举我们上述抽奖的例子,使用概率选择对应的奖品就应当放到AwardPool类中。

充血模型:

系统困境与软件复杂度,为什么我们的系统会如此复杂?


解决复杂和大规模软件的武器可以被粗略地归为三类:抽象、分治和知识。


抽象: 使用抽象能够精简问题空间,而且问题越小越容易理解。举个例子,从北京到上海出差,可以先理解为使用交通工具前往,但不需要一开始就想清楚到底是高铁还是飞机,以及乘坐他们需要注意什么。


分治: 把问题空间分割为规模更小且易于处理的若干子问题。分割后的问题需要足够小,以便一个人单枪匹马就能够解决他们;

  其次,必须考虑如何将分割后的各个部分装配为整体。分割得越合理越易于理解,在装配成整体时,所需跟踪的细节也就越少。即更容易设计各部分的协作方式。评判什么是分治得好,即高内聚低耦合。


知识 顾名思义,DDD可以认为是知识的一种。自身的知识储备,可以让我们在面临问题是选择出合适的技术去解决问题


DDD提供了这样的知识手段,让我们知道如何抽象出限界上下文以及如何去分治。


DDD 的限界上下文与微服务架构相得益彰

微服务架构众所周知,此处不做赘述。我们创建微服务时,需要创建一个高内聚、低耦合的微服务。而DDD中的限界上下文则完美匹配微服务要求,可以将该限界上下文理解为一个微服务进程。


分治: 实现系统复杂度的拆分,一般有两种方式: 技术维度 业务维度

技术维度:类似于MVC

业务维度:则按照业务领域来划分系统

微服务架构和DDD同样注重业务视角

我们将架构设计活动精简为以下三个层面:

业务架构——根据业务需求设计业务模块及其关系

系统架构——设计系统和子系统的模块

技术架构——决定采用的技术及框架

DDD的核心诉求就是将业务架构映射到系统架构上,在响应业务变化调整业务架构时,也随之变化系统架构。而微服务追求业务层面的复用,设计出来的系统架构和业务一致;

在技术架构上则系统模块之间充分解耦,可以自由地选择合适的技术架构,去中心化地治理技术和数据。

DDD的专业术语:

贫血模型

充血模型

战略建模

战术设计

应用服务--》领域服务 --》通过资源库获取聚合根

   --》通过资源库持久化聚合根 

   --》发布领域事件

=================================================================================================================


为什么领域驱动设计

一种思维方式


如何领域驱动设计

战略设计 --------》(从整体到局部)----------》 战术设计


战略设计 (如何进行战略设计)

1.通用语言(统一语言):

定义:通过统一语言 ,提炼出领域知识的产出物, 体现在两个方面:统一的领域术语  领域行为描述

如何获取统一语言: 统一语言就是需求分析的过程,也就是团队中各个角色就系统目标 范围 和具体的功能达成一致性的过程

输出: 概念  英文  定义 约束(避免快速腐化)  举例

2.领域:问题 + 边界 +  知识

  领域包含了问题域和解系统。一般认为软件是对现实世界的部分模拟。在DDD中,解系统可以映射为一个个限界上下文,限界上下文就是软件对于问题域的一个特定的、有限的解决方案。

子域和限界上下文(分治,解决系统复杂性)

子域划分:确定逻辑边界

核心域

通用域

支撑域

限界上下文:(一个具有边界的,解决特定问题的解决方案)

一个由显示边界限定的特定职责。领域模型便存在于这个边界之内。在边界内,每一个模型概念,包括它的属性和操作,都具有特殊的含义。

一个给定的业务领域会包含多个限界上下文,想与一个限界上下文沟通,则需要通过显示边界进行通信。

系统通过确定的限界上下文来进行解耦,而每一个限界上下文内部紧密组织,职责明确,具有较高的内聚性。

一个很形象的隐喻:细胞质所以能够存在,是因为细胞膜限定了什么在细胞内,什么在细胞外,并且确定了什么物质可以通过细胞膜。

如何划分出一个限界上下文? 限界上下文应该从需求出发,按领域划分

显然我们不应该按技术架构或者开发任务来创建限界上下文,应该按照语义的边界来考虑。

实践方案:考虑产品所讲的通用语言,从中提取一些术语称之为概念对象,寻找对象之间的联系;   从需求中的名词 抽象出 概念对象,并试图找寻对象之间的关系

或者从需求里提取一些动词,观察动词和对象之间的关系;   从需求中动词,抽象出行为

我们将紧耦合的各自圈在一起,观察他们内在的联系,从而形成对应的界限上下文。

形成之后,我们可以尝试用语言来描述下界限上下文的职责,

看它是否清晰、准确、简洁和完整。简言之,限界上下文应该从需求出发,按领域划分

3.上下文映射图:关联各个上下文

在进行上下文划分之后,我们还需要进一步梳理上下文之间的关系。

康威(梅尔·康威)定律

任何组织在设计一套系统(广义概念上的系统)时,所交付的设计方案在结构上都与该组织的沟通结构保持一致。


梳理清楚上下文之间的关系,从团队内部的关系来看,有如下好处:

任务更好拆分,一个开发人员可以全身心的投入到相关的一个单独的上下文中;

沟通更加顺畅,一个上下文可以明确自己对其他上下文的依赖关系,从而使得团队内开发直接更好的对接。

从团队间的关系来看,明确的上下文关系能够带来如下帮助:


每个团队在它的上下文中能够更加明确自己领域内的概念,因为上下文是领域的解系统;

对于限界上下文之间发生交互,团队与上下文的一致性,能够保证我们明确对接的团队和依赖的上下游。

限界上下文之间的映射关系

1.合作关系(Partnership)PS:两个上下文紧密合作的关系,一荣俱荣,一损俱损。

2.共享内核(Shared Kernel)SK:两个上下文依赖部分共享的模型。

3.客户方-供应方开发(Customer-Supplier Development)CSD:上下文之间有组织的上下游依赖。

4.遵奉者(Conformist):下游上下文只能盲目依赖上游上下文。

5.防腐层(Anticorruption Layer)ACL:一个上下文通过一些适配和转换与另一个上下文交互。

6.开放主机服务(Open Host Service)OHS:定义一种协议来让其他上下文来对本上下文进行访问。

7.发布语言(Published Language)PL:通常与OHS一起使用,用于定义开放主机的协议。

8.大泥球(Big Ball of Mud):混杂在一起的上下文关系,边界不清晰。

9.另谋他路(SeparateWay):两个完全没有任何联系的上下文。


战术设计:设计过程

概念建模(面向业务 问题 业务 抽象) ----------》    领域建模(概念到代码的一个过渡)------------》    框架设计(代码设计 解决 技术 具体)

框架设计的总体思路:分层 CQRS  EDA

 

用户接口层 : web  app  api mq         

 

应用层  

 

领域层 实体 值 聚合根

  

基础设施层  base org  upm gis  

核心概念:

实体

当一个对象由其标识(而不是属性)区分时,这种对象称为实体(Entity)。

例:最简单的,公安系统的身份信息录入,对于人的模拟,即认为是实体,因为每个人是独一无二的,且其具有唯一标识(如公安系统分发的身份证号码)。

在实践上建议将属性的验证放到实体中。

值对象

当一个对象用于对事务进行描述而没有唯一标识时,它被称作值对象(Value Object)。

例:比如颜色信息,我们只需要知道{“name”:“黑色”,”css”:“#000000”}这样的值信息就能够满足要求了,这避免了我们对标识追踪带来的系统复杂性。

值对象很重要,在习惯了使用数据库的数据建模后,很容易将所有对象看作实体。使用值对象,可以更好地做系统优化、精简设计。

它具有不变性、相等性和可替换性。

在实践中,需要保证值对象创建后就不能被修改,即不允许外部再修改其属性。在不同上下文集成时,会出现模型概念的公用,

如商品模型会存在于电商的各个上下文中。在订单上下文中如果你只关注下单时商品信息快照,那么将商品对象视为值对象是很好的选择。

聚合根

Aggregate(聚合)是一组相关对象的集合,作为一个整体被外界访问,聚合根(Aggregate Root)是这个聚合的根节点。

聚合是一个非常重要的概念,核心领域往往都需要用聚合来表达。其次,聚合在技术上有非常高的价值,可以指导详细设计。

聚合

聚合由根实体,值对象和实体组成。

资源库

领域对象需要资源存储,存储的手段可以是多样化的,常见的无非是数据库,分布式缓存,本地缓存等。资源库(Repository)的作用,就是对领域的存储和访问进行统一管理的对象。

生命周期:

聚合(Aggregate)

聚合就是一组应该呆在一起的对象,聚合根(Aggregate Root)就是聚合在一起的基础,并提供对这个聚合的操作。聚合除了聚合根以外,还有自己的边界(boundary),即聚合里有什么。

例如:一个订单可以有多个订单明细,订单明细不可能脱离订单而存在,而订单也不可能没有订单明细。

这种情况下,订单和订单明细就是一个聚合,而订单就是这个聚合的聚合根,订单和订单明细就处于这个聚合的边界之内。

如果要变更订单明细,我们需要通过操作聚合根订单来实现,如order.changeItemCount(),而非订单明细自身。

另外一个例子:一名客户可以有多个订单,订单不可能脱离客户而存在,而客户却可以没有订单。

这种情况下,客户和订单就是不同的两个聚合,一个聚合以客户为聚合根,另一个聚合以订单为聚合根,引用客户的标识。

客户里并不引用订单的标识,这样将关联减至最少有助于简化对象的关系网。但是带来的一个麻烦就是如果要查找某位客户的所有订单,就不得不从所有的订单里查,而不能从客户这个聚合里直接获得。

最后再举一个多对多的例子:一个班级可以有多名学生,学生可以脱离这个班级而存在,而班级不能没有学生,学生也不能不在班级里

这种情况下,班级和学生也是不同的两个聚合,一个聚合以班级为聚合根,引用学生的标识;另一个聚合以学生为聚合根,引用班级的标识,将多对多转换成两个一对多。


聚合是持久化的一个单位,我们需要保证以聚合为单位的数据一致性。如果聚合太大,那就会导致并发修改困难,多人并发修改同一个聚合里的不同项目,

结果就是只有第一个提交的人成功修改,其它人不得不重新刷新聚合才能再次修改。大聚合还会导致性能问题,因为操作实体时会将整个大聚合同时加载进内存。珍爱生命,拒绝大聚合。


聚合根必须是实体而非值对象,因为它需要整体持久化,所以一定会有标识。

而聚合根里的各个元素,既可能是实体,也可能是值对象。例如:一个订单(聚合根)一般会有订单明细(实体)和送货地址(值对象)。

这些元素里可以有对聚合根的引用,但是不能相互引用。任何对其它元素的操作都必须通过聚合根来进行。聚合根里的标识是全局的,

聚合根里的实体标识是聚合里唯一的本地标识,因为对它的访问都是通过聚合根来操作的。聚合根拥有自己独立的生命周期,其实体的生命周期从属于其所属的聚合,值对象因为只是值而已,并没有生命周期。


工厂(Factory)

工厂是生命周期的开始阶段,它可以用来创建复杂的对象或是一整个聚合。复杂对象的创建是领域层的职责,但它并不属于被创建的对象自身的职责。实体和值对象的工厂不太一样,因为值对象是不可变的,

所以需要工厂一次性创建一个完整的值对象出来。而实体工厂则可以选择创建之后再补充一些细节。


资源库(Repository)

资源库是生命周期的结束,它封装了基础设施以提供查询和持久化聚合的操作。

这样能够让我们始终聚焦于模型,而把对象的存储和访问都委托给资源库来完成。

以订单和订单明细的聚合为例,因为一定是通过订单这个聚合根来获取订单明细,所以可以有订单的资源库,但是不能有订单明细的资源库。

也就是说,只有聚合才拥有资源库。需要注意的是,资源库并不是数据库的封装,而是领域层与基础设施之间的桥梁。

DDD关心的是领域内的模型,而并非是数据库的操作。理想的资源库对客户(而非开发者)隐藏了内部的工作细节,委托基础设施层来干那些脏活,到关系型数据库、NOSQL、甚至内存里读取和存储数据。


什么是复杂? 如何定义复杂?

从理解力 和 预测能力 来个维度来分析 复杂系统理论

抽象    分解    层次结构

如何划分 领域服务  和 应用服务

战略层语境:

领域服务通常指 相对聚焦的底层支撑域、通用域服务

应用服务通常指面向业务场景的负责功能组装的服务

战术层语境:

领域服务指 领域建模工具中所指的 领域服务

应用服务指面向场景的技术实现组装

DDD对 clean  code的再定义

统一语言 与统一语言英文保持一致性的代码命名

Domain层: domain层 仅包含领域模型定义的对象,且用 plain object

不依赖spring ICO 和APO等第三方包

拒绝对getset以及构造方法进行注解

拒绝setter update modify save delete 等无法明确业务含义的方法

值对象不用加上标识技术语言的Enun

应用层: application层拒绝xxxxHandle   xxxxProcessor  xxxxContext

区分命令和查询  命令推荐xxxxCommandService  查询推荐xxxxQueryService

infrastructure层 基础支撑层

资源库repository的 入参和出擦 除了原始数据类型,只能包含领域对象

Repository 对象交互拒绝DTO PO

对外接口访问的防腐层,统一命名为xxxAdaptor

禁止外部接口对象直接上层透传

事件

事件命名为 事件+Event  且事件命名为动词过去式



点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消