乐趣区

关于架构设计:谈谈代码降低复杂度从放弃三层架构到DDD入门

本文首发于泊浮目标简书:https://www.jianshu.com/u/204…

版本 日期 备注
1.0 2021.8.1 文章首发

1. 前言

最近我发现团队我的项目中的某个利用复杂度越来越高,具体表现为:

  • 代码可读性较差:各个服务之间调用简单,流程不清晰
  • 批改局部业务导致大量测试用例失败,但很难疾速的寻找出这些测试用例失败的根因

基于这些状况,我开始寻找升高复杂度的计划,于是就有了这篇再谈 DDD 的文章。

1.1 具体问题

1.1.1 宏观角度

从宏观来说,软件架构模式演进经验了三个阶段。

  • 第一阶段是单机架构:采纳面向过程的设计办法,零碎包含客户端 UI 层和数据库两层,采纳 C/S 架构模式,整个零碎围绕数据库驱动设计和开发,并且总是从设计数据库和字段开始。
  • 第二阶段是集中式架构:采纳面向对象的设计办法,零碎包含业务接入层、业务逻辑层和数据库层,采纳经典的三层架构,也有局部利用采纳传统的 SOA 架构。这种架构容易使零碎变得臃肿,可扩展性和弹性伸缩性差。
  • 第三阶段是散布式微服务架构:随着微服务架构理念的提出,集中式架构正向散布式微服务架构演进。微服务架构能够很好地实现利用之间的解耦,解决单体利用扩展性和弹性伸缩能力有余的问题。咱们晓得,在单机和集中式架构时代,系统分析、设计和开发往往是独立、分阶段割裂进行的。

比方,在零碎建设过程中,咱们常常会看到这样的情景:A 负责提出需要,B 负责需要剖析,C 负责零碎设计,D 负责代码实现,这样的流程很长,经手的人也很多,很容易导致信息失落。最初,就很容易导致需要、设计与代码实现的不统一,往往到了软件上线后,咱们才发现很多性能并不是本人想要的,或者做进去的性能跟本人提出的需要偏差太大。

而且在单机和集中式架构这两种模式下,软件无奈疾速响应需要和业务的迅速变动,最终错失倒退良机。此时,散布式微服务的呈现就有点恰逢其时的意思了。

下面这部分来自于极客工夫,这外面指出个别 DDD 是应用在微服务设计与拆分上,但我认为在单体利用中做模块的拆分也是能够并举荐的,这能够让你的模块在须要时能够即刻拆分进来——变成一个独立的微服务。相干能够参考【ZStack】4. 过程内服务,这是一个开源,并施行于生产中很好的一个案例。

1.1.2 宏观角度

这个问题很简略,service 的代码必然会越堆越多,而且聚拢越来越多的业务。

2.DDD 入门

咱们先来看一张图:

从最外层开始——什么是畛域?大白话来说就是一系列问题的聚合。举个例子:

  • 电商平台中的电商域,你要解决的一系列问题有:

    • 用户认证
    • 挪动收付
    • 订单
    • 报价

能够看到,域是出现进去的是一系列的业务畛域问题。

在不同域中,同一个数据实体的形象状态往往是不同的。比方,Bookstore 利用中的书本,在销售畛域中关注的是价格,在仓储畛域中关注的是库存数量,在商品展现畛域中关注的是书籍的介绍信息。

2.1 上下文边界

往里面,咱们应该看到的是限界上下文。其实这个翻译并不好,原文叫 bounded context,叫做 上下文边界 更为得当。实质上来说,它定义了边界。再具体点,即:用来封装通用语言和畛域对象,提供上下文环境,保障在畛域之内的一些术语、业务相干对象等(通用语言)有一个确切的含意,没有二义性。

2.2 聚合

接下来,咱们看到了聚合。聚合 就是由业务和逻辑严密关联的实体和值对象组合而成的,聚合是数据批改和长久化的根本单元,每一个聚合对应一个仓储,实现数据的长久化。

聚合有一个聚合根和上下文边界,这个边界依据业务繁多职责和高内聚准则,定义了聚合外部应该蕴含哪些实体和值对象,而聚合之间的边界是松耦合的。依照这种形式设计进去的微服务很天然就是“高内聚、低耦合”的。

那聚合根是什么呢?

聚合根的次要目标是为了防止因为简单数据模型短少对立的业务规定管制,而导致聚合、实体之间数据不一致性的问题。

传统数据模型中的每一个实体都是对等的,如果任由实体进行无管制地调用和数据批改,很可能会导致实体之间数据逻辑的不统一。而如果采纳锁的形式则会减少软件的复杂度,也会升高零碎的性能。

如果把聚合比作组织,那聚合根就是这个组织的负责人。聚合根也称为根实体,它不仅是实体,还是聚合的管理者。

首先它作为实体自身,领有实体的属性和业务行为,实现本身的业务逻辑。

其次它作为聚合的管理者,在聚合外部负责协调实体和值对象依照固定的业务规定协同实现独特的业务逻辑。

最初在聚合之间,它还是聚合对外的接口人,以聚合根 ID 关联的形式承受内部工作和申请,在上下文内实现聚合之间的业务协同。也就是说,聚合之间通过聚合根 ID 关联援用,如果须要拜访其它聚合的实体,就要先拜访聚合根,再导航到聚合外部实体,内部对象不能间接拜访聚合内实体。

2.3 实体与值对象

在 DDD 中有这样一类对象,它们领有惟一标识符,且标识符在历经各种状态变更后仍能保持一致。对这些对象而言,重要的不是其属性,而是其延续性和标识,对象的延续性和标识会逾越甚至超出软件的生命周期。咱们把这样的对象称为 实体。其实很像数据库里自带不变 id 的一行行业务数据。

值对象绝对不是那么重要,因为它是用来形容实体的一组属性集。很多零碎中的实现会以 json 来实现,比方【ZStack】7. 标签零碎。

为了不便了解,这边做个小结。实体和值对象的目标都是形象聚合若干属性以简化设计和沟通,有了这一层形象,咱们在应用人员实体时,不会产生歧义,在援用地址值对象时,不必列举其全副属性,在同一个限界上下文中,大幅升高误会、放大偏差,两者的区别如下:

  1. 两者都通过属性聚类造成,实体有唯一性,值对象没有。在本文案例的限界上下文中,人员有唯一性,一旦某个人员被零碎纳入治理,它就被赋予了在事件、流程和操作中被惟一辨认的能力,而值对象没有也不用具备唯一性。
  2. 实体着重唯一性和延续性,不在意属性的变动,属性全变了,它还是原来那个它;值对象着重描述性,对属性的变动很敏感,属性变了,它就不是那个它了(意味着不可变性,它可能是从内部查问来的)。
  3. 策略上的思考框架稳固不变,战术上的模型设计却灵便多变,实体和值对象也有可能随着零碎业务关注点的不同而更换地位。比方,如果换一个非凡的限界上下文,这个上下文更关注地址,而不那么关注与这个地址产生分割的人员,那么就应该把地址设计成实体,而把人员设计成值对象。

3. DDD 上手

3.1 从三层模型到 DDD

这里先简略介绍一下三层模型到 DDD 对应的一个变动。

能够的看得出来,次要是对 service 进行了拆分。个别能够拆成三层:

  • 应用服务层:多个畛域服务或内部应用服务进行封装、编排和组合,对外提供粗粒度的服务。应用服务次要实现服务组合和编排,是一段独立的业务逻辑。
  • 畛域服务层:由多个实体组合而成,一个办法可能会跨实体进行调用。在代码过于简单的时候,能够将每个畛域服务拆分为一个畛域服务类,而不是将所有畛域服务代码放到一个畛域服务类中。
  • 实体:是一个充血模型。同一个实体相干的逻辑都在实体类代码中实现。

3.2 建模简介

咱们能够用三步来划定畛域模型和微服务的边界。

  • 第一步:在事件风暴中梳理业务过程中的用户操作、事件以及内部依赖关系等,依据这些因素梳理出畛域实体等畛域对象。
  • 第二步:依据畛域实体之间的业务关联性,将业务严密相干的实体进行组合造成聚合,同时确定聚合中的聚合根、值对象和实体。在第二章的图里,聚合之间的边界是第一层边界,它们在同一个微服务实例中运行,这个边界是逻辑边界,所以用虚线示意。
  • 第三步:依据业务及语义边界等因素,将一个或者多个聚合划定在一个限界上下文内,造成畛域模型。在下面的图里,限界上下文之间的边界是第二层边界,这一层边界可能就是将来微服务的边界,不同限界上下文内的畛域逻辑被隔离在不同的微服务实例中运行,物理上互相隔离,所以是物理边界,边界之间用实线来示意。

3.3 实际:设计一个 MiniStack

为了便于大家了解,我在这里会设计一个很简略的 Iaas 平台,并在外面代入最根本的 DDD 概念。

3.3.1 产品愿景

  • 为了:企业的外部的开发者、运维人员
  • 他们的:计算、存储、网络资源管理
  • 这个:MiniStack
  • 是一个:公有云平台
  • 它能够:治理计算、存储、网络资源管理,帮用户简略疾速的创立虚拟机
  • 而不像:OpenStack
  • 咱们的产品:简略、强壮、智能

串起来就是:为了满足企业的外部的开发者和运维人员,他们的硬件资源管理,咱们建设里这个 MiniStack,它是一个公有云平台,它能够治理计算、存储、网络资源管理,帮用户简略疾速的创立虚拟机,而不像 OpenStack,咱们的产品简略、强壮、弹性。

3.3.2 场景剖析

因篇幅起因,咱们来聊个最典型的场景——创立虚拟机,以便理出相干的畛域模型。

在这里咱们须要留神,咱们要尽可能的梳理整个零碎产生的操作、命令、畛域工夫以及依赖变动等。

3.3.2.1 创立虚拟机

  1. 用户登陆零碎:从数据库中对信息进行校验,实现登陆认证
  2. 创立虚拟机:填写虚拟机名、集群、计算规格、L3 网络以及镜像。如果需要的话(简略的体现),能够指定所在的物理机、以及网段。

    • VM 服务须要提供创立虚拟机接口
  3. 提交至 MiniStack 引擎,引起开始做相干调度:

    1. 寻找合乎计算、存储资源的低负载物理机,并更新 vm 所属的物理机

      • 物理机服务须要提供查问接口
    2. 调配 L3 网络中的闲暇 IP,并更新 vm 相干的网络信息

      • 网络服务须要提供 IP 调配接口
    3. 通知物理机 agent:从镜像服务器拉取镜像到第 1 步寻找出的物理机

      • 物理机服务须要提供拉取镜像接口
    4. 通知物理机 agent 启动参数,拉起 vm

      • VM 服务须要提供启动接口
  4. 界面上返回创立胜利,用户能够看到 vm

但创立完虚拟机当前并不是就这么完事了,万一哪天这台物理机 carsh 了呢?哪天 CPU 因为奇怪的过程而打满了呢?因而为了咱们的指标——智能,创立 vm 后,MiniStac 每 5 分钟收集一系列的监控信息:

  1. 向物理机 agent 发送心跳包,确保物理机状态失常
  2. 向虚拟机 agent 发送心跳包,并会返回:计算、存储、网络的相干状态

3.3.3 宏观设计:领域建模

在这一步,咱们须要对业务进行剖析,建设畛域模型。个别步骤为:

  1. 找出畛域实体和值对象等畛域对象
  2. 找出聚合根,依据实体、值对象与聚合根的依赖关系,建设聚合
  3. 第三步依据业务及语义边界等因素,定义限界上下文

3.3.3.1 定义实体

咱们大抵能够找出几个实体:

  • 虚拟机

    • 启动
    • 进行
  • 物理机的存储资源

    • 查问
    • 调配
    • 开释
  • 物理机的计算资源

    • 查问
    • 调配
    • 开释
  • L3 网络

    • 调配 IP
  • 镜像服务器

    • 查问镜像
    • 增加镜像
    • 公布镜像

3.3.3.2 定义聚合与限界上下文

在找聚合前,咱们先要找出聚合根。能够分为物理机、网络、镜像服务器、虚拟机。而他们彼此都是独立的上下文,在须要的状况下,也能够拆成一个个微服务,如果是单体利用,则倡议用模块伎俩进行逻辑隔离。

3.3.4 宏观:畛域对象与代码构造剖析

当咱们实现宏观上的建模后,便能够开始做宏观的事:梳理微服务内的畛域对象,梳理畛域对象之间的关系,确定它们在代码模型和分层架构中的地位,建设畛域模型与微服务模型的映射关系,以及服务之间的依赖关系。

大抵上,分位两步:

  1. 剖析畛域对象
  2. 设计代码构造

3.3.4.1 剖析畛域对象

在这一步,咱们须要确认:

  • 服务的分层
  • 应用服务由哪些服务组成
  • 畛域服务蕴含哪些实体和实体办法
  • 哪个实体是聚合根
  • 实体有哪些属性和办法
  • 哪些对象为值对象

因为咱们的用例比较简单,整顿如下:

  • 应用服务:

    • VM 创立服务:负责创立 VM,会调度大量的底层畛域服务
  • 畛域服务:VM 服务、物理机服务、网络服务、镜像服务

    • VM 服务:治理 VM 的生命周期,如创立、删除、启动、进行等
    • 物理机服务:物理机相干服务,如增加、删除、状态变更、心跳感知、资源 RUD 等
    • 网络服务:网络相干服务,如创立删除 L2、L3 网络,IP 治理等
    • 镜像服务:镜像服务器相干服务,如增加、删除、状态变更、减少镜像等
  • 实体:VM 实体、物理机实体、本地存储实体(物理机存储)

    • VM 实体:启动、进行等
    • 物理机实体:状态变更、心跳感知等
    • L3 实体:IP 段增加、删除、IP 调配、开释等
    • 本地存储实体:存储的占用与开释

接下来看一下聚合中的对象,咱们把及格聚合根辨认进去:

  • 物理机聚合的中的聚合根是物理机
  • 网络聚合中的聚合根是 L2 网络
  • 镜像聚合中的聚合根是镜像服务器
  • 虚拟机聚合中的聚合根是虚拟机实体

而下面提到的实体属性与办法咱们曾经在图中出现进去了。

对于值对象,能够参考【ZStack】7. 标签零碎。该设计用于实在生产中。

3.3.4.2 设计代码构造

当咱们实现畛域对象的剖析后,咱们便开始设计各畛域对象在代码模型中的出现形式了——即建设畛域对象与代码对象的映射关系。依据这种映射关系,服务人员能够疾速定位到业务逻辑所在的代码地位。

宏观上,咱们能够参考以下分层模型:

宏观施行上,咱们能够参考 COLA。

4. 小结

本文和大家一起捋了一遍 DDD,并在文里“凭空的”设计了一个我的项目。其实这个我的项目并非凭空,我参考了以前参加的开源我的项目 ZStack 并对它做出了简化——该我的项目目前跑在大量的企业用户的公有云中,迭代已有 6 年多。因而无论从设计还是落地来说,都有肯定的参考教训。

为了大家不便将文中的例子联合 ZStack 代码了解,我这边做了一个映射。

当然,本篇的内容仅仅只能作为入门。并未深刻相干概念,如:子域 外围域 通用域 撑持域 畛域事件 等;对于实战篇也仅仅设计了一个较为简单例子,并没有深究设计准则与架构演进路线。之后有机会的话,我会持续深刻相干方向。

4.1 参考资料

  • 对于 ZStack 的材料

    • 【ZStack】4. 过程内服务
    • 【ZStack】7. 标签零碎
    • 【ZStack】9. 查问 API
    • ZStack 源码分析:如何在百万行代码中疾速迭代
    • ZStack 源码分析之设计模式鉴赏——三驾马车
    • ZStack Github Repo
  • 尽管 ZStack 是个值得参考的我的项目,但其 DDD 的设计并不是特地显著。因而在我的项目分层上也能够参考 COLA
  • 《畛域驱动设计》
  • 极客工夫——DDD 实战课
退出移动版