本文以问题为导向,以某一个业务甚至多个业务为例,一步步解析DDD
(即领域驱动设计,后文均简写成DDD)。业界有句话说,架构脱离业
务就是耍流氓,同样,思想如果脱离实践,只谈理论,就是空谈。
我最近为什么要研究DDD?
最近公司要将一个项目重构,这个项目经过很多人的手,代码接近800M,
这个项目在全球有多个站点,这给我们带来了不少的困难,首先,相关
文档如数据库关系文档、概要和详细设计等文档缺失,其次,业务梳理
困难,可能一些细节会忽视,再框架层面是用SAP比较笨重的hybris框架,
怎么扩展,以及很好的使用这个框架,对应的文档也很少,这给我们开发
人员在快速响应需求,项目出现紧急bug上,在扩展和维护上带来很大的
挑战,以及遇到一些源码级别的坑会很影响效率,要找维护这个框架的公
司的人来解决,在当今,拥抱开源是趋势,最重要的是时间成本上,这个
项目的代码比较多,项目启动比较费时,重启的时间往往比更改很小的内
容所耗的时间要长的多得多,从而导致增加人手来维护这个平台,进而人
力成本上升。
对此,我们曾经提出以下几种解决策略:
1、尽可能的停止在就平台上面继续开发新需求,能迁移的就单独迁移出来
形成一个新的服务。
2、如果不能迁移出来,迁移困难,则将其封装成一个接口,形成所谓的胶水
代码,供新旧服务使用。
3、先迁移简单的,以及先迁移更有价值的,或者频繁维护的。
4、在准备迁移之前,先前后端分离。以及建立api网关及DNS。
5、采取灰度发布二八形式,20%的流量打到新服务,80%的流量打到旧服务,
如果新服务出现问题,及时回滚或者指向回旧服务。
6、数据迁移方面,分静态迁移和动态迁移,静态迁移就是直接导入导出的形
式。而动态迁移就是服务不停机,
关系型数据库直接通过搭建主备数据库双写的形式,缓存必要时可以丢失,
文件服务器直接共用。一般是在用户少使用的时候进行。
最后,在权衡用户,开发,测试,运营等成本得失后,如果重写的成本相对低,
就立马行动!如果我们没法控制住它,不如试法去打破它。重新根据旧项目
的功能去梳理,构建一个新的平台。业务复杂,功能未来多变,要求快速迭代,
并带有不确定性需求的大型项目这么一个场景,是比较适合用DDD去设计的。
这也是我研究DDD的原因的之一。
提出DDD是在什么背景下?
是一个老外,具体什么名字咋们不讲究,说他们从一个小的创业公司逐渐发展
壮大,结果技术无法跟上业务的脚步,代码结构混乱冗杂并且庞大,业务复杂,
严重影响开发维护效率,一来怕担心影响到用户的体验,二是公司人力少,不
敢多招人,目前人均工作量饱和,业务需求和维护量也多,只关注业务的发展,
对没有产生效益的工作不是特别在乎,所以这给重构的推动带来重重困难。
什么是DDD?
domain driver design , 简称DDD, 翻译过来叫领域驱动设计,也就是业务专家
跟程序员打交道时,以领域为界建模的方式,降低沟通成本,达到业务即设计,
设计即代码,代码即文档的一种暂且的相对来说的理想境界。
领域:即业务是属于哪块,电商领域,保险领域,零售领域,又可细化分为子领
域。如电商下(订单交易领域、库存领域、会员领域、物流领域….)
领域专家:一般指熟悉对应领域的产品经理项目经理
有必要一定要推行DDD这种开发方式吗?为什么推荐发展快速且业务复杂的互联网企业要用DDD作为主导设计?使用DDD有什么好处与坏处?
不一定,如果你的项目未来业务不是特别复杂,相对于传统甚至走瀑布流的开发
管理方式,变更需求不强烈,不要求快速迭代,用户增长不高相对稳定的情况下,
不建议采用DDD这种建模方式或者说设计思想。如果是一家初创企业,未来用户会
快速增长,业务会越来越复杂,未来希望能降低重构所耗的时间成本及人力成本,
未来需求不确定,变更频繁,不知道未来是否会演变成微服务,如果会演变,微服
务拆多细,怎么拆的问题,我建议推行DDD这种开发方式,当然,还有我开头所说的,
业务复杂,功能点多且细,无法跟上开发文档的修改速度时,或者项目比较重或者比
较臃肿时,我建议重构后推行DDD这种开发方式。
领域驱动设计主要优势:
1、业务导向。
2、业务逻辑内聚,应用边界清晰。
3、建立领域模型优先。
4、分析、设计、代码和数据有机结合。
5、代码即设计。
6、扩展性好。
数据驱动设计主要特点:
1、技术导向。
2、数据库优先。
3、代码不能反映业务和设计。
4、业务逻辑分散。
5、扩展性不好。
领域驱动设计目的是让业务架构和系统架构形成绑定关系,当我们去响应业务变化调
整业务架构时,系统架构的改变也会随之发生。在领域驱动设计中业务架构的梳理和
系统架构的梳理是同步进行的,其结果是设计出的业务上下文和系统模块结构是绑定的。
什么时候要重构?
什么时候都适合重构,哪怕一个小小的重构,但现实中,往往是难以维护甚至无法维护
时才去重构,或者是影响开发效率,扩展功能耗时太长其原因代码太笨重太复杂影响,
开发效率严重影响业务需求时,即时间成本太高时才去重构。
怎么运用领域驱动设计思想去做业务设计和开发?举例说明
DDD 分层架构包括:展现层、应用层、领域层和基础层。
展现层:可以理解成前端。
应用层:可以理解成传统三层架构中的控制层,应用层还可进行安全认证、权限校
验、分布式和持久化事务控制等。
领域层:它实现了全部业务逻辑并且通过各种校验手段保证业务正确性,通俗的说
就是按业务建模,校验函数也写在这一层。它包含业务所涉及的领域对象(实体、
值对象)、领域对象之间的关系。它负责表达业务概念、业务状态以及业务规则。
基础设施层(infrastructure):封装基础资源服务,为应用层和领域层提供基础
资源服务(如数据库、缓存、api网关等基础资源)
接口返回值在Adapter层集合,类命名以Adapter结尾, 如CargoTrackingViewAdapter。
命令如更新,删除接口,入参写在web层,类命名以Command结尾,如UpdateXxxCommand。
interfaces层下有多个命名的业务动作,如取快递,送快递等,还有dto层(DTO相关类)、
assembler层(DTO 与领域对象domain之间的相互转换)。这些动作层下面有分逻辑处理
facade层和web层,web层下面有XxxController,XxxCommand, XxxAdapter,
此外还有工具层utils, 比如时间格式校验,时间转换等工具类。
还有配置层Config, 放相关的配置信息。
业务开展步骤:
1、场景分析
场景分析是一个发散的过程。根据不同角色的可能经历的场景分析,
尽可能全面的梳理从前端操作到后端业务逻辑发生的所有操作、命令、领域事件以及外
部依赖关系等信息。
2、领域建模
领域建模是一个收敛的过程。这个收敛过程分三步:
第一步根据场景分析中的操作集合定义领域实体;
第二步根据领域实体业务关联性,定义聚合;
第三步根据业务及边界等因素,定义限界上下文。
3、微服务设计和拆分
理论上一个限界上下文可以设计为一个微服务,但还需要综合考虑多种外部因素,
如:领域单一性、性能差异、版本发布频率、团队沟通效率和技术异构等要素。
4、代码结构模型设计
即设计类及分层结构,并写上一部分代码如领域实体,领域之间的关系,接口名称及出入参等。
5、详细设计
详细设计主要结合具体的业务功能来开展,主要工作包括:系统界面、数据库表以及字
段、服务参数规约及功能等。
6、代码开发;软件开发人员只需要按照设计文档和功能要求,找到业务功能对应的代
码位置,完成代码开发和服务编排即可。
7、测试和发布
完成代码开发后,由开发人员编写单元测试用例,基于挡板模拟依赖对象完成跨服务的测试,
测试人员编写测试用例,运维人员编写自动化测试脚本。
具体代码和详情介绍请看下面,这是我看到相对比较好的文章。
理论篇:https://www.infoq.cn/article/7QgXyp4Jh3-5Pk6LydWw
操作步骤篇:https://www.infoq.cn/article/s_LFUlU6ZQODd030RbH9
代码篇:https://github.com/citerus/dddsample-core
转DDD需要多大的成本及资源投入?效率怎么样?有那些指标体现?有没有备选方案或折衷?
人力成本:开发、测试、运营、运维等。
时间成本:如果未来能节省时间成本,提高开发与维护效率,重构是不错的方案。
重构前需提前和业务沟通!
技术选型最好要成熟,一定要经历过生产级别验证过的。能用公司的基础服务就用基
础服务,能借鉴别的团队就借鉴别的团队!
重构量化指标比如用户操作的响应时间缩短到 100 毫秒、单元测试的覆盖率达到 80%、
发现问题时长降低到 30 分钟以内等等,能让系统更加完善的都可以提出明确指标。
性能指标:响应时间、超时率、错误率、资源占用率、运行周期、支持最大并发数。
资源使用率:CPU占用率、内存使用率、磁盘I/O、网络I/O。
错误率:返回的状态码如404,500等所占请求总数的比率。
衡量架构复杂度的几个指标:
depth(深度,从 nginx 开始到最深路径上的微服务个数),
fan-in(入度,一个服务被几个服务调用),
fan-out(出度,一个微服务调用了几个微服务)。
你应该尽力降低架构深度和出度,提升服务的入度。
良好的单元测试不但是重构的先决条件和好帮手,而且能帮我们整理设计的思路,从而
更好的写出优秀的代码。因为在写单元测试的时候,我们会假设自己是一个“代码破坏
者”,思考如何破坏代码的运行、寻找那些可能出错的边界条件。单元测试的编写和运
行可以在写完代码后进行,也可以在写代码之前动手。先写单元测试再写代码的技巧
叫作测试驱动开发(TDD),也是敏捷开发的基石之一。
数据平滑迁移方案
1、分可停机和不可停机,可停机直接导入导出,静态迁移,不可停机找用户少使用
时,数据库双写、定时任务load相对不太实时的数据、读取操作数据库日志变更的形式。
2、分数据类型,关系型数据库直接导入导出,或者双写的形式。缓存数据大部分是
经常访问或者处理过的数据,必要时可丢失。文件数据则共用的形式。
3、分业务需求,变更表结构,如拆表;变更存储介质,如mysql的数据迁移到mongodb
中;两者都可以开发个小工具处理。而分库个数追加,则只需更改库路由配置。
4、迁移完后还需进行缓存预热或者开发一个小工具进行新旧数据比对,保证数据一致。
服务迁移方案
1、灰度发布,二八流量原则,20%的流量打到新服务,80%的流量打到新服务,如果出
现问题,及时切回旧服务。
2、全量发布,在不影响旧业务的情况下,如抽取封装成一个子服务,测试在预发布环
境也没问题,可全量发布,这种情况较少。