架构设计篇之领域驱动设计(DDD)
本篇文章基于 Eric Evans 作者和翻译作者孙向晖,霍泰稳的书,做的软件思想笔记,向作者们致敬。
不要畏惧你所不知道的领域,如果你有需要,那么就搞定它。
- 刘晓成 @小诚信驿站
一、定义
领域驱动设计的定义:
领域驱动设计,其实就是业务驱动设计,通过软件开发架构师和开发人员与最熟悉该领域的业务专家一起勾勒出需求领域模型去实际解决业务场景遇到的问题。
二、核心
DDD 核心是什么?
DDD 核心是关注精简的业务模型及实现的匹配。
三、为什么需要 DDD?
DDD 希望解决什么问题?
目前我们使用的面向对象程序设计,也有面向过程程序设计,为什么还需要领域驱动设计?
设计之选择性的三个问题
在面向对象编程的过程中,哪些对象是对我们的系统有用的?(精简业务模型)
哪些是对我们拟建系统没有用处的?(克制系统边界,需求边界)
我们应该如何保证我们选取的模型对象恰好够用?(适度不蔓延,不泛化)
设计之封装边界的问题
对象封装解决的一部分对象关联依赖关系。但是如何更高点的层次上,通过保留对象之间有用的关系去除无用的关系,并且限定变更影响的范围来降低系统的复杂度?(去除无用的关系,更好的封装不变需求范围)
设计之业务和研发思维问题
开发人员会沉溺于技术思维,对业务理解和深入付出很少。业务理解对于技术实现不能理解,只想表达业务视角看到的内容。
解决方式:看清楚我们提炼出的模型,在整个架构和整个开发过程中所处的位置和地位。
如何处理模型和对象实现
解决方式: 哪些具有什么样素质和技能的人来处理模型和对象实现,应该用什么样团队模型来匹配业务模型
如何应用开源项目设计思想和历史项目设计思想?
解决方式:尽信书不如无书,切勿生搬硬套,要有自己的方法论,进行切合实际场景的使用。
四、DDD 方法论
4.1、理解你的领域,熟悉你的业务
所有的技术实现都是围绕业务场景开展,在一个系统设计之前一定要充分理解你的应用场景。业务的需求(痛点),你要从哪个切入点(抓手)开展?从根本上看,你解决了什么问题?
软件和领域和谐相处其实就是软件更好的为你所涉及的业务领域服务。我们需要做的是理解你的业务领域核心概念和元素,并精确实现它们之间的关系。软件需要对领域进行建模。
那么这里的问题点是我们应该保留什么?放弃什么?软件需要对领域进行抽象。
我们比如是滴滴的客服系统,则我们应该更好的记录用户的问题,解答客户的问题,放弃商业化的售卖东西。
我们比如是淘宝的运营系统,则我们应该更好的记录用户的购买转化率,点击率,曝光率,直接下单率,双 11 同环比 GMV,以及净利润营收等,而不应该关注用户是否可以用滴滴 APP 打到车,美团 APP 是否可以下单订到外卖。
那么领域专家和技术专家如何共享知识和信息,软件需要对领域进行交流。
3.1、比如将模型图形化:图、用例、画和图片等(UML 少量元素的场景来描述)。
3.2、针对要交流的领域内的特定问题建立一种语言(prd 原型图)
3.3、用代码作为语言沟通,优秀的高可维护性的代码(伪代码来约定)
4.2、构建领域知识,实践你的业务
对于你的业务和你不熟悉的领域,都需要从 0-1 开始构建你的业务领域知识体系。
构建的方式,最好的方式是交流
与领域专家交谈,比如淘宝的运营系统是和运营和商家交谈,比如滴滴客服系统是跟滴滴的客服和司机乘客交谈。你需要尽可能多地从专家处学 习领域知识。通过提出正确的问题,正确地处理得到的信息,你和专家会开始勾勒领域的骨架视图,也就是领域模型。这种骨架视图既不完整也不能保证是正确的,但它却是你需要的开始点,可以尽 力判断出领域的基础性概念。比如美人鱼电影中的警察根据报警人员的描述,勾勒出美人鱼的画像(初期领域建模不一定非常正确或者完美,但一定是尽力还原领域业务专家的描述)。
2.构建时间损耗,多次沟通不耐其烦
软件架构师和开发人员和领域专家,会在一起创建领域的模型,可能这个过程需要耗时很久很久,但是为了目标最后的建模的准确性,这个多次沟通是最有效的方式。
4.3、模型驱动设计,模型设计完成编码
4.3.1、我们应该如何动手处理从模型到代码的转换
从分析模型出发,中间可以过程化编程(函数调用和数据结构算法来表达),如果是复杂的模型驱动,则不建议过程化编程,可以伪代码实现。
4.3.2、模型驱动设计的基本构成要素
4.3.2.1、分层架构设计之逻辑清晰(MVC)
上面的的 4 个概念层其实就是我们所说的 view- controller- service(domain)- dao(mapper),是不是非常符合我们的 MVC 架构。
4.3.2.2、实体与值对象(entity 与各种 O)
建议选择那些符合实体定义的对象作为实体,将剩下的对象处理成值对象,没有标识符,值对象就可以被轻易地创建或者丢弃。没有人关心创建一个标识符,在没有其他对象引用时,垃圾回收会处理这个对象。
一条箴言是:值对象就是我们所说的 dto 或者 vo,根据 entity 或者 domain 实体产生的多个副本(派生的对象)。
4.3.2.3、服务(系统服务)
一个服务应该不是对通常属于领域对象的操作的替代。我们不应该 为每一个需要的操作来建立一个服务。但是当一个操作凸现为一个领域中的重要概念时,就需要为它建立一个服务了。
以下是服务的 3 个特征:
1. 服务执行的操作涉及一个领域概念,这个领域概念通常不属于一 个实体或者值对象。
2.被执行的操作涉及到领域中的其他的对象。
3.操作是无状态的。
4.3.2.4、模块(功能模块)
对一个大型的复杂项目,模型趋向很大。模型到达了一 个作为整体很难讨论的点,理解不同部件之间的关系和交互变得很 困难。将模型组织进模块。模块被用来作为组织相关概念和任务以便降低复杂性的一种方法。
4.3.2.5、聚合(对象的封装组合)
聚合是一个用来定义对象所有权和边界的领域模式。工厂和数据库,用来帮助我们处 理对象的创建和存储问题
一个模型会包含众多的领域对象。不管在设计时做了多少考虑,我们都会看到许多对象会跟其他的对象发生关联,形成了一个复杂的关系网
4.3.2.6、工厂(工厂方法和抽象工厂)
工厂方法和抽象工厂,只关心对象的创建和强化所有的不变量,返回对那个对象的引用或者拷贝
4.3.2.7、资源库(数据库)
从数据库或者 new 创建到删除或者 gc 回收的过程
使用一个资源库,它的目的是封装所有获取对象引用所需的逻辑。领域对象不需处理基础设施,以得到领域中对其他对象的所需的引用。只需从资源库中获取它们,于是模型重获它应有的清晰和焦点。
资源库会保存对某些对象的引用。当一个对象被创建出来时,它可以被保存到资源库中,然后以后使用时可从资源库中检索到。如果 客户程序从资源库中请求一个对象,而资源库中并没有它,就会从 存储介质中获取它。换种说法是,资源库作为一个全局的可访问对 象的存储点而存在。
资源库可能包含一定的策略。它可能基于一个特定的策略来访问某个或者另一个持久化存储介质。它可能会对不同类型的对象使用不 同的存储位置。最终结果是领域模型同需要保存的对象和它们的引 用中解耦,可以访问潜在的持久化基础设施。(如下图式例)
资源库的接口是纯粹的领域模型(比如根据业务场景实现的查找用户或者添加用户的功能)
4.3.2.8、规约(比如 API 约定,接口文档,交互协议等)
指定一个查询条件,规约允许定义更复杂的条件。来提供服务支持。
4.3.3、面向深层理解的重构
基于模型驱动设计从模型我们开发用代码进行了表达,以及对于业务领域进行了基本构成拆分形成了一个服务或者多个服务系统。正如我们之前所说的领域模型初期不一定非常完美切合业务场景。那么随着不断深入了解业务以及功能逐渐完善,重构也是在这个过程中必不可少的环节。持续重构将是保证我们的好的方案以及应对变化的需求的保障护航舰队。
一个好的模型产生于深层的思考、理解、经验和才能。
4.3.3.1、持续重构
设计方案应对需求的变化
4.3.3.2、凸显关键概念
业务领域建模的时候,约束(限制条件,校验参数)、过程(实现业务的逻辑过程)和 规约(约定的接口文档交互方式等)这些是一些关键的核心概念,而这些需要我们不断完善,但并不是要经常变动的。
4.3.3.3、保持模型一致性(每个领域保证业务领域模型与整体一致)
之前我们说过,针对业务专家描述的业务领域可能会有部分需要独立部署服务系统,那么这些单独部署的服务系统,从整体上来看应该都符合整体业务领域模型的内容。
划清边界,界定上下文
比如分层领域,需要界定应用上下文,每个系统服务的职能。
持续集成
持续集成是基于模型中概念的集成,然后再通过测试实现。任何不完整的模型在实现过程中都会被检测出来。持续集成应用于界定的上下文,不会被用来处理相邻上下文之间的关系。
上下文映射
(Context Map)是指抽象出不同界定上下文和它们之间关系的文 档,它可以是像下面所说的一个试图(Diagram),也可以是其他任何写就的文档。
共享内核(共享基础服务)
共享内核的目的是减少重复,但是仍保持两个独立的上下文。对于 共享内核的开发需要多加小心。两个开发团队都有可能修改内核代 码,还要必须整合所做的修改。如果团队用的是内核代码的副本, 那么要尽可能早地融合( Merge)代码,至少每周一次。还应该使 用测试工具,这样每一个针对内核的修改都能快速地被测试。内核 的任何改变都应该通知另一个团队,团队之间密切沟通,使大家都 能了解最新的功能。
客户和供应商---实际上是上下游系统
顺从者(如何应对内部需求堆积,合理协配外部需求)
防奔溃层(实际上指的是如何应对模型针对不同外部需求的服务变化可以用门面模式,适配层处理)
独立方法(独立服务部署)
实际上指的是如果我们的领域模型 不相关的服务内容可以作为独立的服务部署和实现。
开放主机服务(提炼核心服务,核心业务)
系统的核心域要看我们如何理解系统。精炼模型。找到核心域,发现一个能轻松地从支持模型和代码中区 分核心域的方法。强调最有价值和特殊的概念。使核心变小。
4.3.4、实践 DDD 的方式
购买现成的方案。 这个方法的好处是可以使用别人已经做好 的全套方案。随之而来的是学习曲线的问题,而且这样的方 案还会引入其他麻烦。比如如果代码有凑五,你只得等待别 人来解决。你还需要使用特定的编译器和类库版本。和自己 系统的集成也不是那么容易。
外包。 将设计和实现交给另外一个团队,有可能是其他公司 的。这样做可以使你专注于核心域,从处理其他领域的重压 下释放出来。不便的地方是集成外包的代码。需要和子域通 信的结构需要预先定义好,还要和外包团队保持沟通。
已有模型。 一个取巧的方案是使用一个已经创建的模型。世 面上已经有一些关于分析模型的书,可以作为我们子域的灵 感来源。直接复制原有的模型不太现实,但确实有些只需要 做少许改动就可以用了。
自己实现。 这个方案的好处是能够做到最好的集成,但这也 意味着额外的付出,包括维护的压力等。
五、DDD 的思维陷进
事必躬亲。模型需要代码。
专注于具体场景。抽象思维需要落地于具体案例。
不要试图对任何事情都进行领域驱动设计。画一张范围表,然后 决定哪些应该进行领域驱动设计,哪些不用。不要担心边界之外的事情。也就是说不要蔓延,要克制。
不停地实验,期望能产生错误。模型是一个创造性的流程。
版权声明: 本文为 InfoQ 作者【小诚信驿站】的原创文章。
原文链接:【http://xie.infoq.cn/article/ad6eef21ac5d7ec1cdb5f5135】。文章转载请联系作者。
评论 (6 条评论)