打造一个适合开发复杂软件的低代码平台,从领域驱动设计说起 (上)
刁伟鸿
2022-12-07

复杂性与软件设计


低代码开发是一种快速开发应用软件的方式。使用这种方式,开发人员只需要编写少量的代码,甚至完全不写代码,大部分工作都是用图形化、拖放式的界面完成。


如果我们要打造一个低代码平台,用它来开发任意业务应用软件,就要考虑业务软件的复杂性,考虑低代码平台如何帮助开发人员分解和管理软件复杂性。


业务软件的复杂性,来自于业务领域中概念与概念之间的关系。一个软件系统,一开始或许很简单,但随着需求的增加、功能的迭代,引入的概念就会越来越多。概念越多,概念之间发生联系或者冲突的可能性就越高,理解这些关系、使用软件来处理这些关系的难度就越大。


在软件开发这个行当里,处理这种复杂性的环节,是软件设计。


这里说的软件设计,指的不是从用户需求的角度决定软件有什么功能、长什么样,而是通过设计软件的内在结构,来有效应对业务软件在长期迭代过程中积累的复杂性。


设计是为了让软件在长期更容易适应变化。
—— Kent Beck


在实际工作中,这一步经常被省略。产品经理把界面设计和功能需求给程序员,程序员直接写代码,能实现就行。但没有设计也是一种设计,一种不确定的设计,有可能是好的,更有可能是糟糕的。糟糕意味着软件质量低,不好理解、不好修改、不容易添加新的功能。软件随着迭代变得越来越复杂,维护成本也就会越来越高,甚至达到无法修改的地步。


关于设计是否必要或是否负担得起的问题,根本都没有问到点上:设计总是存在。除了优秀设计就是糟糕设计,根本不存在“不做设计”一说。
—— Douglas Martin, "Design:A Practical Introduction"


良好的软件设计,并不能消除业务内在的复杂性。它能做的,主要是合理地分解业务复杂性。



什么是领域驱动设计


领域驱动设计(Domain Driven Design,经常被简称为 DDD),是当前被业界多数人采纳的一种软件设计方法。


领域驱动设计是一种模型驱动的设计方法。


模型是对现实的表达,它忽略了无需关注的细节,使用严谨的术语、词汇和逻辑关系,无歧义地把现实的某一部分表达出来。对软件内在结构的设计,也是一种建立模型的过程。


领域驱动设计,主张从领域知识出发、而不是围绕着技术要素建立模型。领域知识,是指跟软件所要解决的问题相关的知识,可以粗略地等同于软件开发中人们常说的业务知识。


从领域知识出发建立的模型,就是领域模型(Domain Model)。在领域驱动设计中,领域模型必须反映软件的内在结构。从另一个角度说,软件实现必须跟领域模型紧密关联,模型中的所有元素都能在软件实现中找到对应的代码。


领域模型不仅仅用于开发,也用于业务规则和功能需求的描述。


业务方(泛指客户、产品经理、业务人员等)与技术团队沟通功能需求时,要使用基于领域模型形成的共同语言。领域驱动设计里把这个共同语言叫统一语言(Ubiquitous Language)


举个很简单的例子,同一个动作,如果业务方说的是“下单”,但开发人员说“创建订单”,他们使用的就不是共同语言。“下单”通常隐含一定的业务规则;“创建订单”指的可能是业务方所说的“下单”,也可能是一个不包含任何业务规则的、纯粹的添加数据操作。


统一语言和领域模型就是领域驱动设计方法的核心概念。领域模型与统一语言相关联,用同样的词汇,表达同样的意思;领域模型也与软件实现相关联,反映软件的内在结构。


还是以前面的“下单”为例,如果它作为一个动作出现在领域模型中,它就属于统一语言的一部分,当业务方和开发人员沟通时使用到这个词,指的就是一个确定的意思,代表特定业务流程和业务规则。另外,以“下单”命名的动作,也会跟它的业务流程和业务规则一起,整体地存在于软件的“源代码”中。


这样做的好处是显而易见的:因为模型反应了软件的内在结构,开发人员只要理解了模型,就能大概明白软件代码的结构;因为业务方使用统一语言表达需求,开发人员很容易把需求跟领域模型联系上,进而定位到需要改动的代码。


另外,因为模型中富含提炼和沉淀下来的领域知识,它可以被用作知识的传递,降低软件迭代和维护的难度。



不从领域出发建立的模型是什么样的
举个简单的例子,当我们用 Excel 表格,或表格驱动的无代码开发平台,构建一个会议室预定系统时,反映它软件实现的模型,就是围绕着工作表以及对工作表行、列、单元格的操作来建立的。
“预定会议室”的动作,在这个模型里可能要这样表达:在”预定记录“表格中插入一行记录;在“占用情况”表格中根据该会议室的行和相应时间段的列,找到对应的单元格,标记为已占用。
这个例子比较简单,算不上典型。跟其他从技术工具(如数据库、存储过程)或者各种软件实现模式出发建立的模型一样,虽然它解决了问题,但不能用来代表经过提炼的领域知识。

领域驱动设计包含什么内容


除了领域模型和统一语言,在领域驱动设计的方法中,还有其他用于建模的概念,或者说,建模工具。篇幅所限,这里只能简单介绍几个主要的。


这些工具分为两组,一组用于在宏观上划分领域模型及其软件实现,另一组用于在微观上描绘模型的具体内容。在领域驱动设计的概念中,前者属于战略设计,需要根据业务价值和重要性进行思考;后者属于战术设计,考虑的是模型的细节。


战略设计中的建模工具包括限界上下文子域上下文映射以及前面介绍的统一语言


业务方和技术团队使用统一语言沟通,用约定好的词汇表达精确的意思。但同一个词汇在不同的上下文可能有不同含义。在一个电商系统中,支付模块里的“订单”和配送模块里的“订单”,代表的对象可能不完全一样,前者是客户一次支付完的总订单,后者是根据配送方式拆分后的订单。


限界上下文(Bounded Context)就是用于处理这个问题的。限界上下文划定了语义的边界,统一语言只在它所属的限界上下文中有效。


限界上下文是分解业务复杂性的主要手段。


根据业务的相关性,可以对领域进行划分,分成多个子域(Subdomain)。根据业务价值和独特性,这些子域又可以分为核心域支撑子域通用子域。子域的不同类型,代表了它在软件开发中的重要性和必要性。


一个子域可以被实现成一个限界上下文,或者多个限界上下文。


在软件实现时,每个限界上下文都应该有独立的源代码版本管理,一个限界上下文只能由一个团队负责。限界上下文之间可能会相互依赖和集成。


不同的限界上下文之间的集成关系,叫上下文映射(Context Mapping)。领域驱动设计的方法定义了很多种上下文映射,代表不同的集成模式。


战术设计中的建模工具包括实体聚合领域事件命令等。它们被用于领域模型的具体细节。


实体(Entity)代表系统中可以用唯一标识符标识出来的、有生命周期的对象。实体有各种属性。一个实体对象的属性可能会变化,但标识符不会变。比如订单,在一次交易过程中,它的状态会变化,但是订单号这种标识符,可以用来标识出状态变化后的订单,仍然是之前那个。


聚合(Aggregate)由一个或多个实体组成,其中最主要的那个实体被称为聚合根(Aggregate Root)。通常来说一个实体就是一个聚合,但有些实体的对象是依附于其他实体对象存在的,比如订单中的订单项,不能脱离订单存在,它们就组成了一个聚合,被依附的实体,就是聚合根。


业务规则中的不变性约束,通常定义在聚合上。比如,订单总额不能超过某个金额、所有订单项的商品都要支持订单中的配送方式,诸如此类的约束,作用在单个订单项上是没有意义的。


领域事件(Domain Event)用来记录在系统中发生的、对业务产生重要影响的事情,它们通常代表系统状态的变化。


领域事件的发生,都是由某个对系统施加的动作造成的,这个动作被称为命令(Command)。命令可能由用户界面发起,也可能是其他原因引起,比如计时器。


命令的执行是作用在实体或者聚合上的,因此实体除了有属性,还有操作接口,代表对应的命令。比如,订单实体可能会有“取消”、“退货”等操作接口。


实体、聚合、领域事件、命令等,都使用统一语言中的词汇表达,都有自己所属的限界上下文。


介绍领域驱动设计的书有很多,如果想要快速地进一步了解上述概念的内涵,推荐阅读 Vaughn Vernon 的《领域驱动设计精粹》

怎么做领域驱动设计


领域驱动设计的目标,是提炼业务知识,建立富含业务知识的领域模型,形成业务方和技术团队共同认可和使用的统一语言。这意味着,领域驱动设计需要业务方和技术团队共同参与,是一个共创的过程。


头脑风暴是常见的共创方法。在领域驱动设计社区中,最被推荐的,就是意大利人 Alberto Brandolini 发明的事件风暴法(Event Storming)。顾名思义,这是用头脑风暴的方式组织的事件建模法。


每个事件风暴都是一个工作坊,它从领域事件入手,梳理业务流程和业务规则,提炼用于领域模型的元素、和用于统一语言的词汇。


如前面所述,领域事件是指在系统中发生的、对业务产生重要影响的事实,它们代表了系统状态的变化。先后发生的事件串联起来,就能显露出业务流程的脉络。


世界是事实的总和,而不是事物的总和
—— 维特根斯坦


事件风暴的参与者必须同时包括软件开发人员和相关的业务方。带入会场的资料,可能是初步撰写好的功能需求说明书,也可能是参与者头脑中关于业务领域和已有系统的知识。


就像其他头脑风暴一样,事件风暴也需要一个大白板,很多各种颜色的便利贴。主要分成四步:

  • 第一步,先把领域事件识别出来(橙色),按照时间顺序从左到右,贴在白板上。有些领域事件和其他事件并行发生,可以把这些事件贴在同时发生的事件的下方。

  • 第二步,找出造成这些事件的命令(浅蓝色),紧挨着,贴在它引起的事件左边。一个命令可能会造成多个事件,把同样的便利贴贴在它造成的所有事件的左边。

  • 第三步,找出与命令和事件相关的聚合和实体(浅黄色)。聚合是命令执行和领域事件触发的数据载体,如果业务方一开始不习惯聚合这个词,可以统一称之为实体,甚至是数据。聚合应该贴在它相关的命令和事件的后面,略为靠上。


这三步循环迭代进行,一开始讨论的可能是很宏观的事件,但最终,还是要细化到软件实现的粒度。在这个过程中,如果不同业务人员对相同词汇的定义出现冲突,那就意味着这里可能存在上下文的边界。


- 第四步,根据词汇定义的冲突,事件、命令、聚合等元素的相关度,在白板上画出边界,整理出不同的限界上下文


经过这些步骤,领域模型的各种元素就被初步识别出来,对事件、命令、聚合等的命名和描述,也形成了统一语言。


之后,就是细化模型,以及通过一轮一轮的事件风暴反复迭代调整模型、进一步提炼知识的事了。


低代码平台如何支持领域建模


领域驱动设计应对软件复杂性的方法,是围绕着领域模型进行的:

  • 使用限界上下文划分领域模型,分解业务中的复杂性

  • 让模型关联用统一语言表达的业务知识,帮助技术人员理解业务

  • 让模型直接反映软件实现的内在结构,帮助技术人员(甚至业务方)理解业务对应的软件实现。


要打造一个支持领域驱动设计、支持领域建模的低代码平台,最直接的方式,就是把它设计成一个领域模型的编辑工具和运行平台。


使用这个平台,你可以直接用图形化界面编辑领域模型中的各种具体元素;你编辑好的模型可以直接通过低代码平台运行,不需要另外编写代码。


具体来说,它应该:

  • 提供对限界上下文的支持,包括限界上下文的独立开发和版本管理,以及限界上下文之间的集成机制。

  • 提供对实体/聚合的建模,包括实体数据的存储和标准查询,以及命令接口的定义,后者可以通过图形化工作流编排、或嵌入代码的方式实现。

  • 支持对聚合的业务不变性约束的建模。

  • 提供对领域事件的建模,支持限界上下文之间、限界上下文内部的事件监听

  • 根据实体/聚合自动生成对应的数据管理界面

  • 提供与实体/聚合无缝集成的用户界面编辑工具


我们的 LeanCode ,是一个模型驱动的低代码平台,但它不是基于领域驱动设计的方法论来设计的。使用 LeanCode, 你并不需要学习领域驱动设计的这些概念和术语


只不过,LeanCode 一定程度地满足了上述的大部分要求,这大概是因为我们是以适合开发任意复杂业务软件为目标,来设计和迭代它的


在这个主题的下篇文章,我会详细介绍 LeanCode 低代码平台是如何通过支持领域驱动设计的风格,来应对软件复杂性的。


低代码平台
软件设计
领域驱动设计
我们是一个软件开发团队,我们开发 LeanCode 低代码平台,也基于 LeanCode 平台提供软件定制、数字化转型服务。如果您有需要,欢迎联系我们
© 深圳市群蜂信息技术有限公司 2020-2023
粤ICP备2021006232号