领域驱动设计精粹(中)
领域驱动设计核心概念
领域驱动设计学习拦路虎之一就是众多的概念,第一次接触这些概念会有一定的理解成本,不过正是这些概念支撑起的领域驱动设计,接下来会以电商为例对其中的核心概念做介绍。
电商案例
网上购物已经成为我们生活中不可分割的一部分,作为一个用户而言我们经历的流程有以下几点:
- 从商品列表页面选择需要的商品。
- 查查商品的促销活动,凑凑满减。
- 在购物车选择需要买的商品下单。
- 下完单通过微信或者支付宝付钱。
- 然后等着物流送货上门。
作为电商的管理人员我们需要做的则是以下几点:
- 从采购点采购商品,存放到仓库。
- 编辑商品信息,上架售卖。
- 编辑一些优惠信息展示在平台上。
- 将用户下单的商品通知仓库发货。
- 营收成本的清结算。
电商平台作为一个复杂系统主要有多阶段、⻓链路、多角⾊参与、多信息互通的商品/服务交换过程的特点。而领域驱动设计中的概念能支撑我们将电商复杂流程拆解消化,并且建立一个易扩展、更稳定的系统。
通用语言和限界上下文
既然有多方协作参与系统的建设和运营,就需要沟通,而降低沟通成本的一个关键就是统一概念和认知,比如我们对于商品的认知,同样都是 iPhone 13,蓝色和粉色,128G 和 256G ,我们说卖掉了一个 iPhone 13 还是卖掉了一个 iPhone 13 蓝色 256G 要怎么表达,这时我们需要有两个概念 SKU 和 SPU 来区分,SKU 作为商品最小售卖单元表达后者,SPU 作为商品信息聚合的最小单位表达前者。
正是因为不同参与角色可能有不同的理解,为了降低大家沟通的障碍,提出了通用语言和限界上下文这两个重要概念。
使团队交流达成共识的能够明确简单清晰地描述业务规则和业务含义的语言就是通用语言。 解决各岗位的沟通障碍问题,促进不同岗位的和合作,确保业务需求的正确表达。通用语言贯穿于整个设计过程,基于通用语言可以开发出可读性更好的代码,能准确的把业务需求转化为代码。
界限上下文则是用来封装通用语言和领域对象,提供上下文环境,保证在上下文内的业务概念和流程等有一个确切的含义,没有二义性。
业务概念往往由领域专家带领团队统一通用语言,明确上下文边界,以结算单这个概念在订单上下文和结算上下文的差异来举例:
- 订单上下文:记录一笔订单所购买商品的消费明细,包括商品原始金额、各项优惠金额、实付货币金额及种类。
- 结算上下文:记录的是商家、平台、供货方在一段时间之内的应收应付款项。
明确上下文边界后,我们跟不同岗位的人沟通即使使用相同词汇也能准确理解其含义。
领域专家和领域知识
领域驱动设计强调由领域专家带领大家进行领域建模。领域专家指的是对一个领域的概念和业务流程精通的人,能快速识别或预判业务风险并能给出有效解决方案的人。 他可以是各个岗位的人,包括一个开发也能成为领域专家。领域知识则是这个领域的各种概念和业务流程。
战略设计与战术设计
领域驱动设计作为一种设计方法论,从两个方向指导设计思想,提出了战略设计和战术设计的概念。
战略设计是从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言下的限界上下文。它是从顶层视角来审视我们的软件系统各个子模块之间的边界。
拿上面的流程举例来说明,一个有经验的领域专家会带领大家通过事件风暴建模的方法进行子域拆分,大致分为交易域、营销域、支付域、商品域、履约域。
战术设计则是从技术视角出发,侧重于领域模型的技术实现,完成软件开发和落地,它主要关注的是技术层面的实施。战术设计识别出来的是聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计和实现。
什么是领域模型
我们都不喜欢写 CRUD 的代码,只因为这些代码往往逻辑很简单,也不具备足够的扩展性,单一场景下可以很快开发出来,如果再加一个场景就又要开发一套,如果场景复杂并且不断变化,开发效率不仅会变慢,而且会更难以维护。下面通过支付系统来举例。
对于 CRUD 的实践来说,在对接支付渠道的时候,给每一家渠道都增加渠道单记录表,字段参照渠道参数定义的,对接微信时增加 wechat_trade 表,增加支付宝时增加 alipay_trade 表。问题就是当渠道增多时每次都建表显然不现实。
正常的做法则是,统一支付单记录,提取支付关键信息,通过总表和渠道表来记录,总表记录关键信息,把次要信息放入渠道表。相当于把支付单信息做了一次垂直拆分。
随着发展,新增了连续订阅业务,产品说需要在支付单中识别出是系统扣费还是用户主动付费的,这时你会想着扩列来支持,可是业务千变万化,不能每次都这样做。
其实软件开发中的许多问题,例如沟通问题、演化问题都和领域模型有关。领域模型是对领域内的概念类或现实世界中对象的可视化表示。它专注于分析问题领域本身,发掘重要的业务领域概念,并建立业务领域概念之间的关系。
实体和值对象
实体和值对象是组成领域模型的基础单元。
实体拥有唯一标识符,且标识符在历经各种状态变更后仍能保持一致。 对实体而言,重要的不是其属性,而是其延续性和标识,对象的延续性和标识会跨越甚至超出软件的生命周期。我们把这样的对象称为实体。从上面的实例来说,支付单有唯一的 ID,渠道单有自己的唯一 ID,它们都是实体。
当一个对象用来描述一个实物,而没有唯一的标识符,叫做值对象。 值对象本质就是一个集合,可以保证属性归类的清晰和概念的完整性。由于金额不能单独表达用户的消费额,需要由支付金额和货币类型组合才能表达,消费额是一组值对象。
聚合与聚合根
聚合是领域模型的具体表达。
聚合是业务和逻辑紧密关联的实体和值对象组合而成,聚合是数据修改和持久化的基本单元,一个聚合对应一个数据的持久化。 聚合在 DDD 分层架构中属于领域层,一个聚合提供一个业务核心能力,领域层包含了多个聚合,聚合内的实体以充血模型实现个体业务能力,以及业务逻辑的高内聚。
聚合根也叫做根实体,它不仅仅是实体,还是实体的管理者。 聚合之间通过聚合根关联引用,如果需要访问其他聚合的实体,先访问聚合根,再导航到聚合内部的实体。即外部对象不能直接访问聚合内的实体。
拿上面支付的例子来说,支付是一个聚合,支付单是聚合根,渠道单是依附于聚合根的另一个实体,渠道单的所有行为都要通过支付单进行操作。
上面说到聚合之间通过聚合根关联引用,一个实体是否属于聚合根取决于所处的聚合。在退款聚合中,退款单是聚合根,绑定的支付单,在这里支付单是普通实体。所以是否是聚合根取决于具体场景。
聚合的特点:高内聚、低耦合,它是领域模型中最底层的边界,可以作为拆分微服务的最小单位。
概念关系
关于领域驱动设计的核心概念已经介绍了一部分,后面还有一部分。关于这些概念的涵盖范围见下图。
从事件风暴建模学到什么
在这里我说一下电商中比较核心的一个流程。在京东购物我们会选择很多需要的商品添加到购物车,在双十一的时候会凑单满减,然后从购物车选中下单。现在我们要设计的部分是用户在选择多件商品时自动给用户使用上最优的多种促销活动,在用户下单的时候能够计算好用户应该付多少钱,每件商品分别应付和优惠多少钱。后面的表达我会用算价来代表这个流程。
领域知识的构成
在领域驱动设计中很强调领域专家这角色,与团队人员共同协作完成任务。而往往团队人员就拥有领域专家所拥有的部分知识,从而承担领域专家的职责,那么剩下的领域知识就需要靠团队人员借助外援来填补,方式包括但不限于以下三种方式:
- 通过网络渠道(论文、文章、书籍)获得。
- 请教身边有相关经验的朋友。
- 通过竞品分析获得。
当我们团队获得该领域下主要的领域知识后,需要结合实际需求进行战略设计和战术设计,就可以通过事件风暴建模方法进行领域建模。
本来是想着拿实际的例子来讲一遍事件风暴建模的过程,现在想想与其照本宣科的讲知识,不如写写经验和感悟来的实在。
事件风暴 VS 传统开发
事件风暴建模的标准流程可以很轻松地找到,这里不再赘述。主要说下从传统软件开发模型到领域驱动设计的领域建模,发生了什么变化。
传统模式:产品需求->需求分析->详细设计->ER模型->UML 设计
DDD 模式:事件风暴->产品愿景->场景分析->领域建模->微服务拆分与设计。
在传统模式下的产出的是可直接落地的设计结果,但是缺乏顶层设计,对于后期的变更维护难以高效支撑。而 DDD 的关注点更多的是顶层设计和概念模型,概念模型并不是可直接落地的结果,这样的优势便是在后期的扩展和变更中更容易。
子域拆分的关键经验
关于如何拆分子域,看了很多的内容后得到的一句话:『凭经验』,这个就让人很糊涂,我如何知道我拆分的是否准确。
当我带着问题去找书查资料,收获还是比较快的,有一段话驱散了一部分迷雾:『领域的边界划分不断演绎,只要发现复杂性凝聚的地方,就划定为有界上下文,割裂它与其他系统的关系,并派出精兵强将专门对付。』它给了我两个点醒:
- 领域的边界是不断演绎的。
- 领域内部是高内聚的,领域间是低耦合的。
从这两点出发,可以通过以下两点执行:
- 和领域专家沟通现在,并预判一下未来。
- 分析领域内头部公司的策略。
领域建模的关键经验
假定产品愿景是可行并且可执行的。在场景分析和领域建模的过程,有个通用的范式。
- 提取业务中的动词和名词识别为领域概念。
- 通过业务中的定语对领域概念进行归纳抽象。
- 对确定的领域概念进行关系确认。
由此我们可以得出领域分析模型,这是一个比较抽象的模型,此时还无法落地。从复杂性角度来看领域建模控制的是业务复杂性。
复杂性问题控制方式
在之前的文章中也提到过三点:
- 抽象
- 分治
- 领域知识
现在反过来看,提炼领域概念是抽象,子域拆分是分治,而要做到这两点的正需要的是领域知识。领域驱动设计不仅告诉了我们『道』,也告诉了我们『术』。