阿里一面:谈一下你对DDD的理解?2W字,帮你实现DDD自由

Posted 40岁资深老架构师尼恩

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了阿里一面:谈一下你对DDD的理解?2W字,帮你实现DDD自由相关的知识,希望对你有一定的参考价值。

说在前面

在微服务的应用开发中,DDD 用得越来越普及。

在40岁老架构师 尼恩的读者交流群(50+)中,DDD是一个非常、非常高频的交流话题。

最近,有小伙伴面试阿里时,遇到一个面试题:

谈谈你对DDD的理解?

小伙伴没有用过DDD,一点概念都没有。当然,面试也就挂了

这里尼恩给大家做一下系统化、体系化的 DDD 梳理,使得大家可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”

也一并把这个题目以及参考答案,收入咱们的 《尼恩Java面试宝典》V47版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

注:本文以 PDF 持续更新,最新尼恩 架构笔记、面试题 的PDF文件,请从这里获取:码云


DDD(Domain Driven Design)理解

领域驱动设计(简称 DDD )历史悠久。

2004年著名建模专家eric evans(埃里克埃文斯)发表的他最具影响力的书籍:

《domain-driven design –tackling complexity in the heart of software》(中文译名:领域驱动设计—软件核心复杂性应对之道)一书。标志着 DDD 这种 设计和架构方法的诞生。

我们在日常开发中,经常针对一些功能点争论“这个功能不应该我改,应该是你那边改”,最终被妥协改了之后都改不明白为什么这个功能要在自己这边改。

区别于传统的数据驱动架构(Data Driven Design)设计,领域驱动设计(DDD)也许在这个时候能帮助你做到清晰的划分。

什么是DDD

领域驱动设计最初由Eric Evans提出,但是多年以来一直停留在理念阶段,

然后,真正能实现并且落地的项目和公司少之又少,

近来,包括阿里在内很多大厂,都在大力推行DDD的设计方法,

它主要可以帮助我们解决传统单体式集中架构难以快速响应业务需求落地的问题,并且针对中台和微服务盛行的场景做出指导。

DDD为我们提供的是架构设计的方法论,既面向技术也面向业务,从业务的角度来把握设计方案。

DDD的作用

统一思想:统一项目各方业务、产品、开发对问题的认知,而不是开发和产品统一,业务又和产品统一从而产生分歧。

明确分工:域模型需要明确定义来解决方方面面的问题,而针对这些问题则形成了团队分钟的理解。

反映变化:需求是不断变化的,因此我们的模型也是在不断的变化的。领域模型则可以真实的反映这些变化。

边界分离:领域模型与数据模型分离,用领域模型来界定哪些需求在什么地方实现,保持结构清晰。

DDD的基本概念

领域驱动设计围绕着领域模型进行设计,通过分层架构将领域独立出来。

这里有两个关键词:

  • 领域模型
  • 分层架构

领域模型的对象包括:实体、值对象和领域服务,领域逻辑都应该封装在这些对象中。

领域模型 Domain Model

领域反映到代码里就是模型,模型是对领域某个方面的抽象,并且可以用来解决相关域的问题,

模型分为实体和值对象两种。

实体对象 Entities

有唯一标志的核心领域对象,且这个标志在整个软件生命周期中都不会发生变化。

这个概念和我们平时软件模型中和数据库打交道的Entity实体比较接近,

不同的是DDD中这些实体会包含与该实体相关的业务逻辑,它是操作行为的载体。

实体 = 唯一身份标识 + 可变性【状态 + 行为】

DDD 中要求实体是唯一的且可持续变化的。

意思是说在实体的生命周期内,无论其如何变化,其仍旧是同一个实体。

唯一性由唯一的身份标识来决定的。

可变性也正反映了实体本身的状态和行为。

实体以 DO(领域对象)的形式存在,每个实体对象都有唯一的 ID。

我们可以对一个实体对象进行多次修改,修改后的数据和原来的数据可能会大不相同。

但是,由于它们拥有相同的 ID,它们依然是同一个实体。

比如商品是商品上下文的一个实体,通过唯一的商品 ID 来标识,不管这个商品的数据如何变化,商品的 ID 一直保持不变,它始终是同一个商品。

值对象 Value Object

依附于实体存在,通过对象属性来识别的对象,它将一些相关的实体属性打包在一起处理,形成一个新的对象。

这些对象是用来表示临时的事物,或者可以认为值对象是实体的属性,这些属性没有特性标识但同时表达了领域中某类含义的概念。

通常值对象不具有唯一id,由对象的属性描述,可以用来传递参数或对实体进行补充描述。

举个栗子:

比如用户实体,包含用户名、密码、年龄、地址,地址又包含省市区等属性,而将省市区这些属性打包成一个属性集合就是值对象。

值对象与实体的区别是什么?

  • 值对象没有唯一标识和连续性,任何属性发生变化, 都可以认为是新的值对象。判断对象是否相同:值对象需要判断所有属性是否相同,而实体只需要判断唯一标识是否相同。
  • 值对象一般依附于实体而存在,是实体属性的一部分,而非独立存在。值对象属性是只读的,可以被安全的共享.

值对象 = 将一个值用对象的方式进行表述,来表达一个具体的固定不变的概念。

还是举个订单的例子,订单是一个实体,里面包含地址,这个地址可以只通过属性嵌入的方式形成的订单实体对象,也可以将地址通过 json 序列化一个 string 类型的数据,存到 DB 的一个字段中,那么这个 Json 串就是一个值对象,是不是很好理解?

当你只关心某个对象的属性时,该对象便可作为一个值对象。

我们需要将值对象看成不变对象,不要给它任何身份标识,注意,**不要给它任何身份标识,该对象便可作为一个值对象。 **

注意:应该尽量避免像实体对象一样的复杂性。

聚合

实体和值对象表现的是个体的能力,而我们的业务逻辑往往很复杂,依赖个体是无法完成的,这时候就需要多个实体和值对象一起协同工作,而这个协同的组织就是聚合。

聚合是数据修改和持久化的基本单元,同一个聚合内要保证事务的一致性,所以在设计的时候要保证聚合的设计拆分到最小化以保证效率和性能。

聚合根

也叫做根实体,一个特殊的实体,它是聚合的管理者,代表聚合的入口,抓住聚合根可以抓住整个聚合。

领域服务

有些领域的操作是一些动词,并不能简单的把他们归类到某个实体或者值对象中。

领域的动作,从领域中识别出来之后,应该将它声明成一个服务,它的作用仅仅是为领域提供相应的功能。

简单理解: 就是业务方法

领域事件

在特定的领域由用户动作触发,表示发生在过去的事件,或者领域状态的变化。

比如:

  • 充值成功
  • 充值失败的事件。

DDD四种模式

接下来,看看DDD四种模式

失血模型

模型中只有简单的get set方法,是对一个实体最简单的封装,其他所有的业务行为由服务类来完成。

pojo里边光秃秃的,get和set方法都没有

@Data
@ToString
public class User 
    private Long id;
    private String username;
    private String password;
    private Integer status;
    private Date createdAt;
    private Date updatedAt;
    private Integer isDeleted;

而且,其他所有的业务行为由服务类来完成,

public class UserService
    public boolean isActive(User user)
        return user.getStatus().equals(StatusEnum.ACTIVE.getCode());
    

贫血模型

贫血模型是指领域对象里只有get和set方法(POJO),所有的业务逻辑都不包含在内而是放在Business Logic层。

@Data
@ToString
public class User 
    private Long id;
    private String username;
    private String password;
    private Integer status;
    private Date createdAt;
    private Date updatedAt;
    private Integer isDeleted;
    
    public boolean isActive(User user)
        return user.getStatus().equals(StatusEnum.ACTIVE.getCode());
    
    
    public void setUsername(String username)
        return username.trim();
    

贫血模型在失血模型基础之上,领域对象的包含一些状态变化,但是停留在内存层面,不关心数据持久化。

贫血模型所有的业务逻辑都不包含在内而是放在Business Logic层。

贫血模型优点是系统的层次结构清楚,各层之间单向依赖,Client->(Business Facade)->Business Logic->Data Access Object。

可见,领域对象几乎只作传输介质之用,不会影响到层次的划分。

这就是 传统的 数据驱动的开发。

在使用Spring的时候,通常暗示着你使用了贫血模型,我们把Domain类用来单纯地存储数据,Spring管不着这些类的注入和管理,Spring关心的逻辑层(比如单例的被池化了的Business Logic层)可以被设计成singleton的bean。

假使我们这里逆天而行,硬要在Domain类中提供业务逻辑方法,那么我们在使用Spring构造这样的数据bean的时候就遇到许多麻烦,比如:bean之间的引用,可能引起大范围的bean之间的嵌套构造器的调用。

充血模型

在贫血模型基础上,负责数据的持久化。

@Data
@ToString
public class User 
    private Long id;
    private String username;
    private String password;
    private Integer status;
    private Date createdAt;
    private Date updatedAt;
    private Integer isDeleted;
    
    private UserRepository userRepository;
    
    public boolean isActive(User user)
        return user.getStatus().equals(StatusEnum.ACTIVE.getCode());
    
    
    public void setUsername(String username)
        this.username = username.trim();
        userRepository.update(user);
    

充血模型层次结构和上面的差不多,不过大多业务逻辑放在Domain Object里面,Business Logic只是简单封装部分业务逻辑以及控制事务、权限等,这样层次结构就变成Client->(Business Facade)->Business Logic->Domain Object->Data Access Object。

它的优点是面向对象,Business Logic符合单一职责,不像在贫血模型里面那样包含所有的业务逻辑太过沉重。

胀血模型

service都不需要,所有的业务逻辑、数据存储都放到一个类中。

对于DDD来说,失血和胀血都是不合适的,

失血太轻量没有聚合,胀血那是初学者才这样写代码。

那么充血模型和贫血模型该怎么选择?

充血模型依赖repository接口,与数据存储紧密相关,有破坏程序稳定性的风险。

DDD建模方法

  • 用例分析法
  • 四色建模法
  • 事件风暴法

用例分析法

用例分析法是领域建模最简单可行的方式。

大致可以分为获取用例、收集实体、添加关联、添加属性、模型精化几个步骤。

  1. 获取用例:提取领域规则描述
  2. 收集实体:定位实体,
  3. 添加关联:两个实体间用动词关联起来
  4. 添加属性:获取实体属性
  5. 模型精化:可选的步骤,可以用UML的泛华和组合来表达模型间的关系,同时可以做子领域的划分

四色建模法

四色建模法源于《Java Modeling In Color With UML》,它是一种模型的分析和设计方法,

通过把所有模型分为四种类型,帮助模型做到清晰、可追溯。

简单来说,四色关注的是某个人的角色在某个地点的角色用某个东西的角色做了某件事情。

Peter Coad和Mark Mayfield奠定了4种架构型(一种形式,所有的东西都或多或少地遵守)的早期工作。

4种架构型 Archetype:

  1. MomentInterval 时刻时段 类型的 Archetype
    一个时刻或一段时间,粉红色表示。
    • 一次销售是在一个时刻完成的——这次销售的日期和时间。
    • 一次租赁发生在一个时间段——从登记入住到归还。
  2. Role 角色 类型的 Archetype
    角色是一种参与方式,
    角色由 PartyPlaceThing 类型模型 来承担,黄色表示。
  3. Description 描述 类型的 Archetype
    Description 是类似 “分类目录条目似的” 描述,
    Description 是一组反复应用的值,
    Description 也为所有对应到某个描述的东西提供行为,蓝色表示。
  4. PartyPlaceThing 参与方-地点-物品 类型的 Archetype
    PartyPlaceThing 包括 参与方(意味着人或组织机构)、地点或物品
    PartyPlaceThing 是扮演不同角色的人或物,
    PartyPlaceThing 绿色表示。

这4种颜色中, 每一种都对应一种架构型的特征(属性、链接、方法、插入点和交互),每一种颜色对应的类,或多或少都包含了这些特性。

如何确定一个类的颜色类型?

  1. 它是某个时刻或时段,是出于某个原因(业务原因或法律原因),是系统需要追踪的东西吗?
    如果是这样,那么它是粉红色的、时刻时段 MomentInterval 类型的 Archetype。
  2. 否则,它是一个角色吗?
    如果是这样,那么它是黄色类型的、Role 角色 类型的 Archetype
  3. 否则,它是一个类似分类目录似的描述,包含了一组可以反复应用的值吗?
    如果是这样,那么它是蓝色的、Description 描述 类型的 Archetype
  4. 否则,它就是绿色的、PartyPlaceThing 参与方-地点-物品 类型的 Archetype。

事件风暴法

事件风暴法类似头脑风暴,简单来说就是谁在何时基于什么做了什么,产生了什么,影响了什么事情。

DDD分层架构

DDD的概念

领域驱动设计围绕着领域模型进行设计,通过分层架构将领域独立出来。

这里有两个关键词:

  • 领域模型
  • 分层架构

领域模型的对象包括:实体、值对象和领域服务,领域逻辑都应该封装在这些对象中。

聚合是一种边界,它可以封装一到多个实体与值对象,并维持该边界范围之内的业务完整性。在聚合中,至少包含一个实体,且只有实体才能作为聚合根。工厂和资源库都是对领域对象生命周期的管理。工厂负责领域对象的创建,用于封装复杂或者可能变化的创建逻辑。资源库负责从存放资源的持久层获取、添加、删除或者修改领域对象。

传统的三层架构

传统的MVC模型把框架分成了三层:显示层、控制层、模型层。

显示层负责显示用户界面,控制层负责处理业务逻辑、而模型则负责与数据库通信,对数据进行持久化的操作。

从代码角度来看,这样的框架结构每个模块职责分离,特别适合小型的应用系统。

最简单的分层方式自然就是“表现层、业务逻辑层和数据访问层”,如下图:

其中包含一些 帮助类/工具类,比如 SQLHelper、StringUtility之类,归纳到 基础结构层。

在传统的三层架构中,表现层只能跟业务逻辑层打交道,而业务逻辑层 只能跟 数据访问层 打交道。

这也是一般的编程规范都要求: 表现层不能访问 DAO层,表现层对数据访问层的内容一无所知

从领域驱动的角度看,这种分层的方式有一定的弊端。

主要有两点:

  • 首先, 基础结构层作为 公共组件,为各个层面提供服务,职责职责比较紊乱。

基础结构层 既可以是纯粹的技术框架,又可以包含或处理一定的业务逻辑,这样一来,业务逻辑层与“基础结构层”之间就会存在依赖关系;

  • 其次,传统的三层架构数据表驱动编程,或者可以说是面向数据表编程,总之,这种结构过分地突出了“数据访问”的地位。

这种面向数据表模式中,“数据访问”甚至超过了 “业务逻辑”地位。一旦在数据访问弱化的场景中,甚至不存在库表的场景中,很多软件人员一上来就问:“我没有表,难道还要三层?不用三层,该怎么办? 这就问题来了。

另外,随着业务复杂度的上升,会发现服务层的逻辑以及代码不断增长,变得庞大且复杂、测试成本直线上升。由于各个Service的逻辑散落在各处,后续新需求的维护的成本也非常高,导致交付效率越来越低,稳定性风险也越来越高。

除了日常开发的一些问题外,由于缺乏一定的业务知识沉淀,如果文档沉淀或者更新不及时的情况下,新同学来接受一个新的小需求,面对产品描述的需求改动点,开发同学根本无从下手。一方面是新同学业务的生疏,但真正根本原因还是:开发与产品之间的语言不能保持一致,双方对于同一事物的表达和理解有很大的区别。产品描述的更多是实际的业务场景,而开发则更关注背后的具体实现逻辑,加之文档的缺失,可以说是面对一堆模型和代码两眼茫然。

怎么办?DDD来解决这摊子问题。

DDD领域驱动设计的分层架构

DDD将软件系统分为四层:基础结构层、领域层、应用层和表现层。

在**《领域驱动设计——软件核心复杂性的应对之道》**一书中,DDD 范式的创始人 Evans 提出下图所示的这样一种分层架构:

整个系统划分为:

  • 基础设施层(Infrastructure)
  • 领域层(Domain)
  • 应用层(Application)
  • 表示层(也称为 用户接口层 User Interface)

与上述的三层相比,**数据访问层(DAO层)**已经不在了,它被移到基础结构层了。

基础结构层 (Infrastructure Layer):

该层专为其它各层提供技术框架支持。

基础结构层的基本原则:不会涉及任何业务知识,与业务无关,不涉及业务逻辑。

为啥数据访问的内容移动到了基础结构层 ?因为 数据的CRUD,本质上不涉及业务逻辑,或者说数据的读写是业务无关的。所以,数据访问的内容,也被放在了该层当中。

Infrastructure Layer 一些例子:

  • 领域层需要持久化服务,在DDD中,领域层通过仓储(Repository)接口定义持久化需求,基础设施层通过采用JDBC、JPA、Hibernate、NoSQL等技术之一实现领域层的仓储接口,为领域层提供持久化服务。
  • 领域层需要消息通知服务,在领域层中定义了一个NotificationService领域服务接口,基础设施层通过采用手机短信、电子邮件、Jabber等技术实现NotificationService领域服务接口,为领域层提供消息通知服务。
  • 用户接口层需要一个对象序列化服务,将任何JavaBean序列化为JSON字符串,可以在用户接口层定义一个ObjectSerializer服务接口,基础设施层通过采用Gson实现这一接口,为用户接口层提供对象序列化服务。

领域层 (Domain Layer):

包含了业务所涉及的领域对象(实体、值对象)、领域服务以及它们之间的关系。

这部分内容的具体表现形式是: 领域模型(Domain Model)。

DDD提倡富领域模型,即尽量将业务逻辑归属到领域对象上,实在无法归属的部分,则以领域服务的形式进行定义。

什么是业务逻辑?

业务逻辑就是存在于问题域即业务领域中的实体、概念、规则和策略等,业务逻辑与软件实现无关,主要包含下面的内容:

  • 业务实体(领域对象)。例如银行储蓄领域中的账户、信用卡等等业务实体。
  • 业务规则。例如借记卡取款数额不得超过账户余额,信用卡支付不得超过授信金额,转账时转出账户余额减少的数量等于转入账户余额增加的数量,取款、存款和转账必须留下记录,等等。
  • 业务策略。例如机票预订的超订策略(卖出的票的数量稍微超过航班座位的数量,以防有些旅客临时取消登机导致座位空置)等。
  • 完整性约束。例如账户的账号不得为空,借记卡余额不得为负数等等。本质上,完整性约束是业务规则的一部分。
  • 业务流程。例如,“在线订购”是一个业务流程,它包括“用户登录-选择商品-结算-下订单-付款-确认收货”这一系列流程。

对领域层的进一步说明如下:

  • 领域层实现映射到领域模型,是设计维度的领域模型(Domain Model)在软件中的具体实现。
  • 包含实体、值对象和领域服务等领域对象,通常这些领域对象和问题域中的概念实体一一对应,具有相同或相似的属性和行为。
  • 在实体、值对象和领域服务等领域对象的方法中,封装实现业务规则和保证完整性约束(这一点是与CRUD模式相比最明显的差别,CRUD中的领域对象没有行为)。
  • 领域对象在实现业务逻辑上具备坚不可摧的完整性,意味着不管外界代码如何操作,都不可能创建不合法的领域对象(例如没有账户号码或余额为负数的借记卡对象),亦不可能打破任何业务规则(例如在多次转账之后,钱凭空丢失或凭空产生)。
  • 领域对象的功能是高度内聚的,具有单一的职责,任何不涉及业务逻辑的复杂的组合操作都不在领域层而在应用层中实现。
  • 领域层中的全部领域对象的总和在功能上是完备的,意味着系统的所有行为都可以由领域层中的领域对象组合实现。

应用层/工作流层 (Application Layer):

应用层是领域驱动中最有争议的一个层次,也会有很多人对其职责感到模糊不清。

应用层不包含任何领域逻辑,但它会对任务进行协调,并可以维护应用程序的状态,

因此,应用层更注重流程性的东西。在某些领域驱动设计的实践中,也会将其称为“工作流层”。

既然不包含领域逻辑,那应用层又如何协调工作任务呢?

它通过排列组合领域层的领域对象来实现用例,它的职责可表示为“编排和转发”,

具体来说,Application Layer 将要实现的功能委托给一个或多个领域对象来实现,Application Layer 只负责安排工作顺序和拼装操作结果。

如果一定要进行类比的话 ,Application Layer 的职责 类似于微服务领域的 SpringCloud gateway,或者服务总线Service Bus。

表现层/用户接口层(User Interface):

这个好理解,跟三层架构里的表现层(Controller)意思差不多,

表现层依赖于应用层,但是表现层与应用层之间是通过数据传输对象(DTO)进行交互的,

DTO数据传输对象是没有行为的POCO(C#中的概念)对象,DTO的目的只是为了对领域对象(Domain Object)中的数据进行封装,剥离了Domain Object中的行为,实现层与层之间的数据传递。

为何不能直接将 Domain Object 用于数据传递?两个原因:

(1) 因为Domain Object 更注重领域,而DTO更注重数据。

(2) 由于“富领域模型”的特点,这样做会直接将Domain Object 的行为暴露给表现层。

表现层为外部用户访问底层系统提供交互界面和数据表示。

表现层在底层系统之上封装了一层可访问外壳,为特定类型的外部用户(人或计算机程序)访问底层系统提供访问入口,并将底层系统的状态数据以该类型客户需要的形式呈现给它们。

表现层有两个任务:

(1)从用户处接收命令操作,改变底层系统状态;

(2)从用户处接收查询操作,将底层系统状态以合适的形式呈现给用户。

表现层说明:

o 典型的用户是人类用户,但是也可能是别的计算机系统。例如如果 ERP 系统要访问我们的系统获取信息,它也是一种用户。

o 不同类型的用户需要不同形式的用户接口,例如为人类用户提供 Web 界面和手机 App,为 ERP 软件用户提供 REST 服务接口。所以,REST 服务接口 也算是表现层。

o 不同类型的用户需要不同形式的数据表示,包括表现形式的不同(XML、JSON、html)和内容的不同(例如手机 App 中呈现的数据内容往往比 Web 页面中呈现的少)。

o 表现层对应用层进行封装,表现层的操作与应用层上定义的操作通常是一一对应的关系。表现层从外部用户处接受输入,转换成应用层方法的参数形式,调用应用层方法将任务交由底层系统执行,并将返回结果转换成合适的形式返回给外部用户。

表现层的典型任务是下面三个:

  • 校验——校验外部客户输入的数据是否合法;
  • 转换——将外部客户的输入转换成对底层系统的方法调用参数,以及将底层系统的调用结果转换成外部客户需要的形式;
  • 转发——将外部客户的请求转发给底层系统。

表现层也被称为用户界面层或用户接口层。

有时候,为了某些需要,我们可以从表现层中分离出一个亚层,可命名为门面层(Facade)。位于真正的表现层和应用层之间。

门面层(Faced Layer)

门面层隔离前台和后台系统,定义特定于表现层的数据结构,从后台获取数据内容并转化为表现层的数据形式。

从表现层中分离出专门的门面层,具有下面的优势:

  • 使得表现层能够独立于后台系统,与后台系统并行开发。

表现层通过门面层接口达到和应用层、领域层解耦,意味着表现层可以独立开发,不必等待后台系统的完成,亦不受后台系统重构的影响,在需求调研阶段系统原型出来并得到用户确认之后,就可以开始表现层的开发了

可以根据界面原型定义表现层需要的数据结构,该数据结构与底层数据结构解耦,不需要知道底层数据类型和数据之间的关联关系。将应用层DTO数据和界面数据连接起来,并相互转换是门面层实现类的职责,这方面工作可以等待前后台系统分别完成之后进行。

  • 使得分布式部署成为可能。

如果没有门面层的隔离,表现层只能直接使用领域层的领域对象作为自己的数据展现结构。

这样我们就不能将系统进行分布式部署,将表现层和后台系统(领域层、应用层等)分别部署到不同的服务器上。

因为在JPA和Hibernate等技术实现中,领域实体绑定到当前服务器的持久化上下文中,必须脱管之后才能够跨越JVM进行传输。

  • 避免Hibernate中“会话已关闭”的问题,消除成本巨大的“Open Session in View”模式的需要。

在采用JPA或Hibernate作为持久化手段的系统中,存在臭名昭著的“会话已关闭”问题,对付这一问题的主要手段是使用Open Session in View方案,但是这个方案的性能很低。

  • 把事务作用范围控制在后端,缩短事务的跨度,提升性能和系统的吞吐量。

更大的问题是事务问题,事务要跨越服务器的边界,复杂性增加,性能严重下降。门面层的存在使得实体和事务都限制在后台系统,不需要扩展到前台服务器。

如果不采用门面层隔离后台数据结构,在前端展现数据需要访问实体的延迟初始化属性时,就会遇到“会话已关闭”问题,而采用Open Session in View模式去解决的话,就意味着事务不是在后端独立完成,这样事务就扩展到前端表现层,在大流量、高吞吐的网站上,把事务扩展到前端界面做造成事务时间跨度极度拉长,从而带来严重的严重的性能问题,大大降低吞吐量。

采用门面模式的话,有关联关系的数据在后台拼装完毕再一次性返回给前端,事务局限在后端范围,不再有“会话已关闭”和性能问题。

门面层说明:

  • 门面层特定于表现层,由表现层定义和控制(包括操作和数据的形式和内容),这意味着需要为不同类型的表现层开发专门的门面层。
  • 查询结果通常以数据传输对象(DTO)的形式表示。DTO的结构由表现层而不是后端决定,代表前端需要的数据形式,与底层数据结构脱耦。
  • 通过门面层实现类访问后端的应用层。实现类将后端数据拼装为DTO并返回给前端,它可以将数据装配职责委托给专门的Assembler工具类去执行。
  • 在分布式系统中,可以在前端和后端分别部署门面层。前后端的门面层接口相同,但后端的门面层实现类负责数据装配和发布,前端的门面层实现类负责通过某种通信机制(Web Service等)与后端门面层通讯,获取后者装配好的数据。传输过程中DTO可能序列化为JSON或XML等形式。

分层架构的优点

分层架构的目的是通过关注点分离来降低系统的复杂度,同时满足单一职责、高内聚、低耦合、提高可复用性和降低维护成本。

  • 单一职责:每一层只负责一个职责,职责边界清晰,如持久层只负责数据查询和存储,领域层只负责处理业务逻辑。
  • 高内聚:分层是把相同的职责放在同一个层中,所有业务逻辑内聚在领域层。
    这样做有什么好处呢?试想一下假如业务逻辑分散在每一层,修改功能需要去各层修改,测试业务逻辑需要测试所有层的代码,这样增加了整个软件的复杂度和测试难度。
  • 低耦合:依赖关系非常简单,上层只能依赖于下层,没有循环依赖。
  • 可复用:某项能力可以复用给多个业务流程。
    比如持久层提供按照还款状态查询信用卡的服务,既可以给申请信用卡做判断使用,也可以给展示未还款信用卡使用。
  • 易维护:面对变更容易修改。把所有对外接口都放在对外接口层,一旦外部依赖的接口被修改,只需要改这个层的代码即可。

以上这些既是分层的好处也是分层的原则,大家在分层时需要遵循以上原则,不恰当的分层会违背了分层架构的初衷。

分层架构的缺点

  • 开发成本高:因为多层分别承担各自的职责,增加功能需要在多个层增加代码,这样难免会增加开发成本。但是合理的能力抽象可以提高了复用性,又能降低开发成本。
  • 性能略低:业务流需要经过多层代码的处理,性能会有所消耗。
  • 可扩展性低:因为上下层之间存在耦合度,所有有些功能变化可能涉及到多层的修改。

DDD的特点

DDD到底能帮助我们什么呢?

用一段话总结下:DDD改变了数据表驱动的设计方式,设计驱动方向变为Domain Model驱动

DDD设计的时候,首先将业务概念和业务规则 转换为 Domain Model(包括类型以及类型的属性与行为),而不是将业务概念和业务规则转换为 Entity和表结构,

DDD设计通过运用面向对象的封装、继承、多态等设计要素,降低或隐藏整个系统的业务复杂性,使得复杂的分布式系统具有更好的扩展性,应对纷繁多变的现实业务问题。

DDD试图解决的是软件的复杂性问题,如果软件比较复杂,或者是预期会很复杂,那么都可以开始考虑DDD。

由于维系领域模型需要实现大量的封装和隔离,DDD会带来较大的成本,如果系统比较简单,未来也不会有太多复杂的扩展,那么,传统的数据表驱动(/数据模型驱动)的设计方式,更为合适。

使用DDD的好处

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

DDD的难点

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

深入DDD

领域驱动设计是一种思维方式,也是一组优先任务,是针对复杂系统设计的一套软件工程方法。分为领域分析建模、领域设计建模与领域实现建模三个过程。

领域驱动设计围绕着领域模型进行设计,通过分层架构将领域独立出来。

这里有两个关键词:

  • 领域模型
  • 分层架构

领域模型的对象包括:实体、值对象和领域服务,领域逻辑都应该封装在这些对象中。

聚合是一种边界,它可以封装一到多个实体与值对象,并维持该边界范围之内的业务完整性。

在聚合中,至少包含一个实体,且只有实体才能作为聚合根。

工厂和资源库都是对领域对象生命周期的管理。

工厂负责领域对象的创建,用于封装复杂或者可能变化的创建逻辑。

资源库负责从存放资源的持久层获取、添加、删除或者修改领域对象。

聚合(aggregate)

聚合是领域对象的显式分组,我们把一些关联性极强、生命周期一致的实体、值对象放到一个聚合里。

聚合定义了一组具有内聚关系的相关对象的集合,每个聚合都有一个根对象(聚合根实体)。

我们把聚合看作是一个修改数据的单元。

一个聚合是一组相关的被视为整体的对象。每个聚合都有一个根对象(聚合根实体),从外部访问只能通过这个对象。

根实体对象有组成聚合所有对象的引用,但是外部对象只能引用根对象实体。只有聚合根才能使用仓储库直接查询,其它的只能通过相关的聚合访问。如果根实体被删除,聚合内部的其它对象也将被删除。

为啥需要进行聚合?旨在支持领域模型的行为和不变性,同时充当一致性和事务性边界。

聚合有两个核心要素:

  • 一个聚合根
  • 一个上下文边界

这个边界 根据业务单一职责和高内聚原则,定义了聚合内部应该包含哪些实体和值对象,而聚合之间的边界是松耦合的。

按照这种方式设计出来的服务很自然就是“高内聚、低耦合”的。

聚合在 DDD 分层架构里属于领域层,领域层包含了多个聚合,共同实现核心业务逻辑。

聚合根(aggregate root)

如果把聚合比作组织,那聚合根就是这个组织的负责人。

聚合根也称为根实体,它不仅是实体,还是聚合的管理者。

  • 首先它作为实体本身,拥有实体的属性和业务行为,实现自身的业务逻辑。
  • 其次它作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。
  • 最后在聚合之间,它还是聚合对外的接口人,以聚合根 ID 关联的方式接受外部任务和请求,在上下文内实现聚合之间的业务协同。也就是说,聚合之间通过聚合根 ID 关联引用,如果需要访问其它聚合的实体,就要先访问聚合根,再导航到聚合内部实体,外部对象不能直接访问聚合内实体。

上面讲的还是有些抽象,下面看一个图就能很好理解(同样是源于极客时间欧创新的DDD实战课):

简单概括一下:

  • 通过事件风暴(我理解就是头脑风暴,不过我们一般都是先通过个人理解,然后再和相关核心同学进行沟通),得到实体和值对象;
  • 将这些实体和值对象聚合为“投保聚合”和“客户聚合”,其中“投保单”和“客户”是两者的聚合根;
  • 找出与聚合根“投保单”和“客户”关联的所有紧密依赖的实体和值对象;
  • 在聚合内根据聚合根、实体和值对象的依赖关系,画出对象的引用和依赖模型。

服务(services)

服务提供的操作是它提供给使用它的客户端,并突出领域对象的关系。

所有的service只负责协调并委派业务逻辑给领域对象进行处理,其本身并未真正实现业务逻辑,绝大部分的业务逻辑都由领域对象承载和实现了。

service可与多种组件进行交互,这些组件包括:其他的service、领域对象和repository 或 dao。

服务又细分为领域服务和应用服务。

领域服务(Domain Service)

接下来,看看领域服务和应用服务两个核心概念。

领域中的一些概念,如果是名词,适合建模为对象的一般归类到实体对象或值对象。

如果是动词,比如一些操作、一些动作,代表的是一种行为,如果是和实体或值对象密切相关的,也可以合并到某个实体或者值对象中。

但是,有些操作不属于实体或者值对象本身,或会涉及到多个领域对象,并且需要协调这些领域对象共同完成这个操作或动作,这时就需要创建领域服务来提供这些操作。

当一些逻辑不属于某个实体时,可以把这些逻辑单独拿出来放到领域服务中。可以使用领域服务的情况:

  • 执行一个显著的业务操作
  • 对领域对象进行转换
  • 以多个领域对象作为输入参数进行计算,结果产生一个值对象

领域服务有两个特征:

  • 1)操作代表了一个领域概念,且不是实体或者值对象的一个自然的部分;
  • 2)被执行的操作涉及领域中的其他对象;操作是无状态的。领域服务还有一个好处可以避免领域逻辑泄露到t应用层。

因为如果没有领域服务,那么应用层会直接调用领域对象完成本该是属于领域服务该做的操作。

此外,如果实体操作过多或者过大,为了避免臃肿,也可以使用领域服务来解决。

但是,不能把所有的东西都搬到领域服务里,过度使用可能会导致产生的太多的贫血对象。

理想的情况是没有领域服务,如果领域服务使用不恰当,慢慢又演化回了以前逻辑都在 service 层的局面

应用服务(Application Service)

应用层作为展现层与领域层的桥梁,是用来表达用例和用户故事的主要手段。

应用层通过应用服务接口来暴露系统的全部功能。

在应用服务的实现中,它负责编排和转发,它将要实现的功能委托给一个或多个领域对象来实现,它本身只负责处理业务用例的执行顺序以及结果的拼装。

通过这样一种方式,它隐藏了领域层的复杂性及其内部实现机制。

应用层相对来说是较“薄”的一层,除了定义应用服务之外,在该层我们可以进行安全认证,权限校验,持久化事务控制,或者向其他系统发生基于事件的消息通知,另外还可以用于创建邮件以发送给客户等。

领域服务和应用服务的不同:

  • 领域服务和应用服务是不同的,领域服务是领域模型的一部分,用来处理业务逻辑,而应用服务不是。
  • 应用服务是领域服务的直接客户,负责处理事务、安全等操作,它将领域模型变成对外界可用的软件系统。
  • 跨多个实体的业务逻辑通过领域服务来实现,跨多个聚合的业务逻辑通过应用服务来实现。

跨多个实体的业务逻辑通过领域服务来实现,跨多个聚合的业务逻辑通过应用服务来实现。

  • 比如有的业务场景需要同一个聚合的 A 和 B 两个实体来共同完成,我们就可以将这段业务逻辑用领域服务来实现;
  • 而有的业务逻辑需要聚合 C 和聚合 D 中的两个服务共同完成,这时你就可以用应用服务来组合这两个服务。

领域事件

领域事件是一个领域模型中极其重要的部分,用来表示领域中发生的事件。

领域事件 = 事件发布 + 事件存储 + 事件分发 + 事件处理。

程序事件通常分为:系统事件、应用事件和领域事件。领域事件的触发点在领域模型中。

它的作用是将领域对象从对repository或service的依赖中解脱出来,避免让领域对象对这些设施产生直接依赖。

它的做法就是当领域对象的业务方法需要依赖到这些对象时,就发出一个事件,这个事件会被相应的对象监听到并做出处理。譬如跨限界上下文时,使用关键应用事件触发事件传递。

从尼恩的视角简单来说:领域事件是对 repository或service 的异步解耦。

在尼恩写的深度文章《京东一面:20种异步,你知道几种? 含协程》中,就有EventBus 这样的事件总线,完成模块之间的异步解耦,也有 RocketMQ这样的分布式消息组件,完成进程级别的异步解耦。而领域事件 是设计维度的解耦。EventBus 、RocketMQ是实现层面的异步解耦,当然是先有设计,后有实现。

在DDD中,通过领域事件,一个领域模型可以忽略不相关的领域活动,同时明确领域专家要跟踪或希望被通知的事情,或与其他模型对象中的状态更改相关联。

下面简单说明领域事件:

  • 事件发布:构建一个事件,需要唯一标识,然后发布;
  • 事件存储:发布事件前需要存储,因为接收后的事建也会存储,可用于重试或对账等;
  • 事件分发:服务内直接发布给订阅者,服务外需要借助消息中间件,比如Kafka,RabbitMQ等;
  • 事件处理:先将事件存储,然后再处理。

比如下订单后,给用户增长积分与赠送优惠券的需求。如果使用瀑布流的方式写代码。一个个逻辑调用,那么不同用户,赠送的东西不同,逻辑就会变得又臭又长。

这里的比较好的方式是,用户下订单成功后,发布领域事件,积分聚合与优惠券聚合监听订单发布的领域事件进行处理。

资源仓储/资源库(Repository)

仓储(资源库)是用来管理实体的集合。

仓储介于领域模型和数据模型之间,主要用于聚合的持久化和检索。

仓储里面存放的对象一定是聚合,原因是domain是以聚合的概念来划分边界的;聚合作为一个整体概念,要么一起被取出来,要么一起被删除。

它隔离了领域模型和数据模型,以便我们关注于领域模型而不需要考虑如何进行持久化。

我们将暂时不使用的领域对象从内存中持久化存储到磁盘中。

当日后需要再次使用这个领域对象时,根据 key 值到数据库查找到这条记录,然后将其恢复成领域对象,应用程序就可以继续使用它了,这就是领域对象持久化存储的设计思想。

工厂(Factory)

工厂用来封装创建一个复杂对象尤其是聚合时所需的知识,作用是将创建对象的细节隐藏起来。

客户传递给工厂一些简单的参数,然后工厂可以在内部创建出一个复杂的领域对象然后返回给客户。

工厂(Factory)不是必须的,只有当创建实体和值对象复杂时,建议使用工厂模式。

领域驱动设计一般分为两个阶段

领域驱动设计划分了战略设计战术设计,也提供了诸多模式和工具,但却没有一个统一过程去规范这两个阶段需要执行的活动、交付的工件以及阶段里程碑,甚至没有清晰定义这两个阶段如何衔接、它们之间执行的工作流到底是怎么样的。

除了把领域驱动设计 分为战略设计和战术设计的方法之外,《解构-领域驱动设计》提出的 DDDRUP 方法。

DDDRUP 给出了更细致的

以上是关于阿里一面:谈一下你对DDD的理解?2W字,帮你实现DDD自由的主要内容,如果未能解决你的问题,请参考以下文章

阿里一面,说说你对Mysql死锁的理解

阿里一面 | 说说你对 MySQL 死锁的理解

阿里一面,说说你对zookeeper中ZAB协议的理解?

谈一下你对 uWSGI 和 nginx 的理解??

谈一下你对uWSGI和 nginx的理解(原理)

DDD专栏10DDD的架构变化之道