摘要:在企业内部网站的建设过程中,网站后端最初采用传统的表模式的开发方式。这种方式极易导致站点的核心业务逻辑和业务规则分布在架构的各个层和对象中,这使得系统业务逻辑的复用性不高。为了解决这个问题,作者在后期的开发过程中引入了领域驱动设计的开发方式,把系统的业务逻辑独立建模、充分地复用,并基于这些模型打造易于扩展的开发框架,提高了整个团队开发业务逻辑的效率,最终网站如期上线,稳定运行至今。
关键词:表模式;贫血模型;领域驱动设计;领域模型
中图分类号:TP311 文献标识码:A
文章编号:1009-3044(2022)10-0044-04
数字化是每个IT企业系统建设必不可少的一个环节,而企业内部网站往往也是数字化的重要标志和组成部分。企业内部站点建设项目就是在这种场景下诞生的,主要就是为顺利开展研发部门的日常工作,提供所需的管理各项流程制度的功能。
在項目的开始阶段,作者在研发方式上选择了常见的表模式来开发网站后端,在架构上选用了传统的多层架构来组织代码。这种技术选型和搭配常用于开发业务逻辑比较简单的小型项目,整个开发的过程都聚焦在具体的数据库设计和面向流程的开发上,对于团队成员来说简单易于上手。
不过,当网站的功能逐渐增多以后,系统的复杂度迅速攀升,采用这种搭配使得系统的业务逻辑得不到聚焦,最终它们零散地分布在架构的各个层和对象中,业务逻辑的复用性很差。
为了解决这个问题,作者引入了领域驱动设计的开发方式。这种开发方式聚焦于多变的业务逻辑,把它们作为系统开发的核心模型管理起来,这样就使得系统的业务逻辑得到了充分的复用,整个团队开发的效率就得到了显著的提高。
1 软件开发的三种模式
在软件开发中,根据研发过程的不同,一般把开发流程分为事务脚本、表模式和领域驱动设计三种模式。
1.1 事务脚本
1) 模式介绍
时至今日,虽然主流编程语言都是面向对象语言,但是很多团队都是采用面向过程的开发模式,代码中使用的对象并没有实际的意义,纯粹只是代码脚本的载体。
事务脚本(Transaction Script) 就是这样一种简单的开发模式,它完全采用的是面向过程的做法,直接将用户在表现层上所做的操作翻译成代码脚本(比如SQL语句、批处理语句等) 执行的模式[1]。
2) 实现方式
通常,实现事务脚本模式不会进行对象设计,也不会对涉及的组件进行分层,所有从表现层到底层的操作全部放到一起。
典型的例子就比如展示图书列表的页面,开发的时候通常就是在界面上放置一个列表控件,然后该控件直接绑定从数据库中获取图书的SQL语句进行数据填充。增加和修改图书的时候也是类似,直接提供SQL语句操作数据库。
当然,为了使整个程序的结构更加清晰,在使用事务脚本模式时,很多团队也会进行简单的分层,这就是基本的多层架构的由来,也就是把应用分为如图1所示的三层。
在分层架构下,应用把整个业务流程分割并组织在不同的层级中,与用户直接交互的组件放在表现层中,用户在表现层的操作会被翻译成系统的输入数据模型并进入业务逻辑层,业务逻辑层再调用底层的数据访问层的脚本操作数据库、文件系统等数据源,完成业务逻辑。
在这个过程中,系统设计的重点依然是面向过程的,各个层和对象只是过程的载体,具体叫什么其实并没有什么太大的区别。
3) 应用场景
事务脚本是一种常用的开发模式,针对那些交互逻辑简单,业务模式几乎不会改变和发展的简单场景,比如常见的“增删改查型”应用,整个开发过程的效率会非常高。
1.2 表模式
1) 模式介绍
在研发过程中,数据库作为最重要的数据载体,在任何一个项目的开发过程中几乎是必然会出现的。在事务脚本中,与数据库交互基本都是通过在代码中直接使用SQL语句来完成的。
在常见的编程语言中,SQL语句都是作为字符串存在的,这就带来一个问题,由于字符串的内容不参与编译,所以如果SQL语句存在语法问题,那就只能到了应用运行时产生错误才会被发现,编译时发现不了,这有时会带来潜在的发布风险。
为了优化操作数据库的体验,提高代码编写的效率,在数据访问层,经过工程师们不断地尝试和努力,就出现了一种全新的数据交互方式,这就是通过编程语言中的实体对象来操作数据库中的数据。
根据实体影响数据库中数据的具体实现不同,以最为核心的表操作为例,如果一个实体对象能执行数据库表中多条数据的相关操作,那么这种模式就称为“表模式(Table Module) ”,如果一个实体对象只执行表中一条数据的相关操作,那么这种模式就称为“活动记录(Active Record) ”。这两种模式本质上并没有太大的区别。
2) 实现方式
以表模式为例,从具体实践上来说,它们是在事务脚本的基础上,把脚本操作变成了对象的操作。这个过程一般是通过特定的框架来完成的,这种框架一般被称为“对象关系映射(Object Relational Mapping,以下简称ORM) ”。
ORM框架把关系数据库中的结构映射成语言中的实体对象,当调用实体的方法的时候,ORM框架就会将对象操作翻译成SQL语句,然后作用于数据库,如图2所示。
从具体实践上来说,该模式在事务脚本的基础上,把脚本操作变成了对象的操作。由于这些对象只是数据库中表的映射,只具有保存数据的字段,而没有任何实际的业务操作,所以,这些实体对象一般也被称为“贫血模型”。
3) 应用场景
表模式优化了数据库访问的体验,但是并不涉及任何业务逻辑的处理,所以表模式和事务脚本一样,都非常适合那些业务逻辑简单的场景,对于较为复杂的应用场景则不太合适。
1.3 领域驱动设计模式
1) 模式介绍
事务脚本和表模式两种开发模式,主要采用的是面向过程的编程方法,而且在应用开发的过程中,需求分析和系统设计大都是分离的,这样就把应用开发前期的工作割裂开来,这也就导致了需求分析的结果与系统设计的代码不能完全匹配,以至于软件上线后,客户才发现许多功能不是自己想要的。
不同于这两种模式,领域驱动设计(Domain Driven Design,以下简称DDD) 开发模式是纯粹的面向对象的设计过程,它以业务领域为核心,分析领域中的问题,通过设计和建立对应的“领域模型(对象) ”来有效的解决领域中的核心问题[2]。
在上述的过程中,通过“领域分析”到“领域建模”,DDD统一了需求分析和设计编码两个阶段,这使得软件能够灵活快速的跟随需求的变化而变化。
在编程实践上,DDD丰富了实体类的行为,把业务逻辑也封装到了实体类中,实体类成为承载系统核心业务逻輯和规则的载体,实体类是整个应用设计的核心。
要复用业务逻辑,就是要复用这些实体类,为了更好地做到这一点,在DDD中,就把实体类相关的模型都聚拢到一起,放到领域模型层中。
2) 实现方式
DDD是一种设计思路,为了实现聚焦业务逻辑,构建领域模型的目的,具体实施时一般分为战略设计和战术设计两个阶段。
战略设计阶段主要完成的工作是识别应用的领域,将领域细化得到子域,为每个子域设计限界上下文,并在这个过程中,通过与业务专家的充分讨论,得到描述业务的统一语言。
战术设计阶段完成的工作是根据限界上下文和统一语言,设计领域层中的领域模型对象,具体包括实体、值对象、领域事件、仓储、聚合和领域服务等(具体概念和细节可以参见文献[3]) 。
DDD的具体实现也存在多种形式,本次网站的实践采用的就是经典的DDD的分层架构,整体架构图如图3所示。
表现层与以往的架构相同,主要是与用户交互的界面元素。
应用服务层是薄薄的一层,主要是把用户的输入转换成系统需要的数据模型和把系统生成的数据模型转化成表现层需要的对象模型,需要注意的是应用服务层主要完成转换和交接工作,调用基础设施完成一些诸如持久化的工作,千万不能实现任何的业务逻辑。
领域层是整个系统的核心层,它包含所有的领域模型。这些领域模型承载和实现了所有的业务逻辑和业务规则,所有的其他层都依赖于领域层,但是领域层不会依赖于任何其他层。
基础设施层完成了的一些辅助的功能,比如一些第三方类库、具体的持久化实现如ORM等。
当按照DDD走完代码设计的过程以后,领域模型就封装了所有的业务逻辑,这样复用领域模型就可以复用整个业务逻辑,同时也能保证这部分复杂多变的逻辑被单独地隔离起来,不会对系统其他部分存在影响。
3) 应用场景
DDD开发模式特别适用于那些业务场景比较复杂、业务逻辑变化比较频繁、系统复杂度比较高的场景,典型的就比如中台系统的搭建[4]、微服务的开发[5]。
2 开发模式的应用
2.1 初期采用表模式开发模式
在最初的开发过程中,作者带领团队采用表模式开发系统,也就是每个业务功能的开发都遵循如下的流程:
1) 根据业务的需求分析得到数据表的字段,把数据库表建好。
2) 通过ORM将数据库中的表映射成代码中的实体对象。
3) 在业务逻辑层根据功能的需要补充业务逻辑和规则,操作实体对象进行数据的读取和持久化。
这种开发方式简单易行,但是随着网站的功能逐渐增多,逻辑越发地复杂,如下问题逐渐显现出来:
1) 通过ORM得到的实体对象都是贫血模型,它们只有数据库映射的属性和字段,并不具有具体的业务行为。
2) 因为没有明确的约束,核心业务逻辑和业务规则非常容易从业务逻辑层扩散到其他层和对象中。有的跑到了帮助类中、有的跑到了存储过程里,这导致了业务逻辑的复用性不高。
为了解决上述问题,作者再次深入研究了上述的软件开发模式并决定引入DDD来开发和管理项目。
2.2 后期采用DDD开发模式来聚焦业务逻辑
使用DDD开发模式来开发和管理项目,作者带领团队实践了如下的过程。
1) DDD战略设计
①将整个应用程序域进行划分,剥离各个子域
在该项目中,经过分析,作者共拆分出账号管理、任务管理、积分管理、图书管理等子域。
②对每个子域进行限界上下文的设计
在每个限界上下文中,保证任何一个对象模型的语义是确定的、无歧义的。
比如在任务管理子域,定义一个User对象,作为任务创建人和执行人存在,它就只具有与之相关的属性和行为。而在账号管理子域中,又定义了另一个User对象,它却具有常见的用户名、密码、邮箱等属性和对应的行为。
虽然两个User对象具有相同的名字,但是它们是完整的用户对象在不同上下文中的投影,是两个不同的对象。
在项目实施的过程中,作者选择将子域和限界上下文一一对应,独立部署在每个AppService中。
2) DDD战术设计
DDD战术设计的核心就是把核心业务逻辑集中放到一起,组成领域层。其他层设计为依赖于该层。领域层中包含所有重要的领域模型,包括聚合根、实体、值对象、领域事件、仓储、领域服务等。
①设计聚合根、实体和领域服务
在该项目的领域层中,最重要的对象就是聚合根和实体。它们完成了主要的业务逻辑。
使用聚合根还是实体,在实践中一般都可以,但是如果完成一个业务操作,就一个主要的实体就可以了,不需要其他相关实体的配合,那么暴露这个实体给其他层就可以了。
比如Book这个实体,不仅包含了所有图书的所有信息,还包括增加封面、修改作者、增加点评等各种业务行为。
但是如果完成一个业务操作,需要一个主要的实体,配合上其他一些相关的实体才能完成,那么就将主要的实体暴露成聚合根给其他层使用,把相关的其他实体全部隐藏起来,这些相关的实体的操作统统通过聚合根来暴露。
比如Task这个实体,不仅包含了所有Task自身的信息和操作,还包括其包含的子对象TaskItem实体的信息和各种操作,所以Task实体就暴露为聚合根,向外提供所有Task和TaskItem的相关操作,其他层无法直接操作TaskItem对象以及其方法。如图6所示。
除此以外,还有一种情况就是,完成一个业务逻辑需要多个主要的实体配合,这个时候,就可以设计一个领域服务来实现这个业务逻辑,在这个领域服务中,它会使用其他相关的聚合根或实体来完成功能。
不过在该项目中,一般使用聚合的概念、暴露聚合根就可以实现需求了,需要使用领域服务的情况比较少,所以没有用到。
②设计领域事件
该项目中,非常常见的一个需求就是一个实体改变了,需要通知其他的一些实体就行一些对应的变化。这种需求就需要通过事件模式来实现。
在项目中,作者实现了一个EventBus作为通用的领域事件模型,任何实体都可以挂接这个对象,发布消息,或关注感兴趣的消息并做出响应,这个部分与通用的事件模型并没有太大的不同,示意图如图7所示。
③设计仓储
使用仓储模式可以很好地隔离数据库的操作和领域对象。
在领域层,作者定义了各个仓储接口,仓储与聚合根或实体一一对应,包含对应的增删改查等操作,它们是数据库操作的接口。
需要注意的是,在领域层中,只是定义了接口,并没有具体的持久化的实现。具体的持久化工作放在了基础设施层中。
3) 其他层的一些细节
①基础设施层
这里重点说明一下的是数据库持久化的选型。
针对领域层提供的仓储接口,作者实现时选择了使用Entity Framework Core(以下简称EF Core) 作为基础框架。使用此类ORM框架大大简化了数据库的操作,使用对象而不是SQL语句的方式可以避免很多语法上的错误,有效地提升了代码的健壮性。
而且使用了EF Core的Code First的方式后,项目只要使用EF Core的包命令就可以非常简单地根据实体对象的变更来创建和维护数据库。
②应用服务层
在该项目中,应用服务层比较简单,它实现了一个公用的AppService,封装了公用的分页的逻辑。其他各项服务比如BookAppService都继承自该基类,实现了自己的逻辑。
在各个服务中,为了调用领域层的业务逻辑,首先需要把前端传过来的输入数据传输对象(Data Transfer Object,以下简称DTO) 转换成领域层的数据模型,然后调用领域层的领域模型和基础设施层的对象完成功能,最后再将结果转换成输出DTO返回给前端。
③后端接口层和前端表现层
接口层使用Restful Api去提供服务,前端表现层使用Vue 3.0实现展示,不管是否使用DDD,这些部分都是一样的,不去多解释。
经过上述的分析、设计与重构,最终得到如下新的架构,如图8所示。
3 结束语
采用了DDD来开发和管理项目以后,项目的业务逻辑沉淀到了领域层的领域对象中,领域对象变成了包含业务逻辑的充血模型,这样业务逻辑得到了复用,程序的扩展性得到了保证。
此外,由于存在清晰明确的分层和职责,团队日常的开发效率得到了显著的提升。目前该站点已经上线并稳定地运行至今。
參考文献:
[1] Fowler M.企业应用架构模式[M].北京:人民邮电出版社,2009.
[2] Evans E.领域驱动设计[M].北京:人民邮电出版社,2010.
[3] Vaughn V.实现领域驱动设计[M].滕云,译.北京:电子工业出版社,2014.
[4] 欧创新,邓頔.中台架构与实现:基于DDD和微服务[M].北京:机械工业出版社,2020.
[5] Richardson C.微服务架构设计模式[M].喻勇,译.北京:机械工业出版社,2019.
【通联编辑:谢媛媛】
收稿日期:2022-01-25
作者简介:董向阳(1982—) ,男,江苏灌云县人,中级工程师(上海) ,硕士,研究方向为计算机应用技术。