10.领域驱动设计(译)
原文:https://herbertograca.com/2017/09/07/domain-driven-design/
这篇文章是软件架构编年史(译)的一部分,这部编年史由一系列关于软件架构的文章(译)组成。在这一系列文章中,我将写下我对软件架构的学习和思考,以及我是如何运用这些知识的。如果你阅读了这个系列中之前的文章,本篇文章的的内容将更有意义。
Eric Evans 于 2003 年出版了精采绝伦的《领域驱动设计:软件核心复杂性应对之道》,在书中他创造了领域驱动设计方法。Eric Evans 的这本著作十分重要,现今许多我们认为理所当然的软件开发概念都是在本书中被正式提出的。
我不可能在一篇博客中全面地回顾 DDD。和 DDD 相关的重要概念实在是太多了。幸好,这篇文章志不在此。而我要做的就是列出一些 DDD 概念,我认为这些概念对我喜欢的组织代码方式和我对架构的看法而言更有意义:系统范围内构成特性开发基础的概念。
在这片文章里,我将着重探讨:
- 统一语言
- 分层
- 限界上下文
- 防腐层
- 共享内核
- 通用子域
统一语言
在软件开发中,围绕着代码的理解始终有一些问题,代码是什么,它们干了什么,它们如何做到的,它们为什么要这么做...如果代码中使用的术语和领域专家使用的术语不一样的话就更复杂了,例如,领域专家谈到的是老用户而代码谈到的却是管理者,在讨论应用是这可能带来很多的困惑。但是,绝大多数的混淆都可以通过正确的命名类和方法来解决,让它们表达出在领域上下文中对象是什么以及方法干了什么。
统一语言的主要思想是让应用能和业务相匹配。这是通过在业务和代码中的技术之间采用共同的语言达成。这门语言起源于公司的业务侧-他们拥有需要实现的概念,语言中的术语由他们和公司的技术侧通过协商来定义(意味着业务侧也不能总是选到最好的命名),目标是创造可以被业务、技术和代码自己无歧义使用的共同术语,即统一语言。代码、类、方法、属性和模块的命名必须和统一语言相匹配。必要的时候需要对代码进行重构!
分层
之前的文章中我已经谈到过分层,但我觉得这里关键的是记住通过 DDD 识别的层次:
-
用户界面
负责绘制用户用和应用交互的屏幕并将用户的输入翻译成应用的命令。值得留意的是“用户”可以是人类也可以是连接我们 API 的其他应用,它们和EBI架构中的边界对象完全对应。 -
应用层
协调领域对象完成用户要求的任务:用例。它不包含业务逻辑。应用层和EBI架构中的交互器相对应,只有一点不同,交互器是和界面或实体无关的任意对象,而这里应用层只包含和用例相关的对象。应用服务属于这一层,它们是用例对资源库、领域模型、实体、值对象或是任何其它领域对象进行编配的容器。 -
领域层
这个层次包含了所有的业务逻辑,如领域服务、实体、时间和其他包含业务逻辑的任何对象类型。显然它和 EBI 架构中的实体对象类型对应。这是系统的心脏。领域服务摆包含的领域逻辑不太适合放到膜个实体中,通常是为了完成某个领域作用而对多个实体的编配。 -
基础设施
支持上述三个层次的技术能力,例如,持久化或者消息机制。
限界上下文
在企业应用中,模型和工作在代码仓库之上的团队规模都增长得很快。这会给我们带来两个问题:
- 开发者工作的代码仓库越大,认知超载就越严重,代码就越难理解,这会导致 BUG 的产生和错误的判断;
- 在同一个代码仓库上工作的开发者越多,就越难协作以及达成共同的应用领域和技术愿景。
换句话说,我们面临的问题太大了。
通常的解决方法就是把大问题切分成较小的问题,“限界上下文”就是这样干的。
一般来说,两个子系统一定服务于迥然不同的用户群体。——Eric Evans 2014, Domain-Driven Design Reference
限界上下文定义了一个模型中分离的部分可以适用的上下文。这种隔离可以通过解耦技术逻辑,分割代码仓库,分割数据库 Schema 或者团队组织方式来达成。和往常一样,限界上下文将拆分到何种程度取决月实际情况:我们的需求和可能性。
有趣的是,这不是一个全新的概念。早在 1992 年,Ivar Jacobson 在他的书中就有子系统的描述,比 Eric Evans 早了十一年!
那时他就提出了一些关于这个主题的具体想法:
- 系统由若干子系统组成,而它们自己又有自己的子系统。这个层级结构的最底层就是分析对象。于是子系统就成为了进一步开发和维护系统的结构方式。
- 子系统的任务就是打包对象,达到降低复杂度的目的。
- 和功能的特定部分相关的全部对象都将被放在同一个子系统中。
- 目标是子系统内的强功能耦合和子系统间的弱耦合(现在被称为高内聚低耦合)
- [一个子系统]最好应该只和一个角色耦合,因为变化通常由一个角色引发。
- [...]首先把控制对象放入子系统,然后将强耦合的实体对象和界面对象放到同一个子系统中
- 拥有强相关功能耦合的所有对象都将被放入同一个子系统之中[...]
- 一个对象中的变化会导致其它对象中的变化吗?(现在被称作共同封闭原则——一起变化的类应该放在同一个包中——由 Robert C. Martin 在他 1996 年的论文“Granularity
”中发布,比 Ivar Jacobson 的书晚了四年) - 它们是和同一个角色通信吗?
- 这两个对象都是依赖第三个对象吗?例如同一个界面对象或实体对象?
- 这个对象会在其它对象上执行多个操作吗?(现在被称作共同重用原则——一起被使用的类应该放在同一个包中——由 Robert C. Martin 在他 1996 年的论文“Granularity
”中发布,比 Ivar Jacobson 的书晚了四年)
- 一个对象中的变化会导致其它对象中的变化吗?(现在被称作共同封闭原则——一起变化的类应该放在同一个包中——由 Robert C. Martin 在他 1996 年的论文“Granularity
- 子系统划分的另一个标准是不同子系统之间的通信应该尽可能少(低耦合)
- 对大型项目来做,还有其它一些子系统划分的标准,例如:
- 不同的开发小组拥有不同的能力或者资源,针对性的分配开发任务可能是可取的(小组还可能分布在不同的地点)
- 在分布式环境中,每个逻辑节点需要的可能就是一个子系统(SOA、Web 服务以及微服务)。
- 如果现存的产品可以在系统中使用,它可以被认为是一个子系统(我们的系统所依赖的库,例如ORM)
防腐层
防腐层基本就是两个系统之间的中间件。它用来隔离两个子系统,让它们都依赖防腐层而不是直接互相依赖。这样,如果我们重构或者完全替换掉其中一个子系统时,只需要更新防腐层,而不需要动其它的子系统。
在将一个新系统和遗留系统进行集成时防腐层特别有用。为了不让遗留的结构显著我们设计新系统的想像力,我们会创建一个防腐层,将遗留子系统的 API 按照新的子系统的需要进行适配。
它有三个主要关注点:
- 按照客户端子系统的需要对其它子系统 API 进行适配;
- 对系统间传递的数据和命令进行转换;
- 根据需要建立单向或多向的通信。
当我们无法控制全部子系统或某个子系统时,使用这项技术的理由更加充分。但在我们能控制所有涉及的子系统时,这项技术也有意义,尽管这些子系统设计良好但仅仅是拥有迥然不同的模型,而我们想要组织一个模型对另一个模型的侵蚀(为了满足一个子系统的需要而修改另一个子系统)。
共享内核
在某些情况下,我们除了渴望完全隔离和解耦的组件之外,在多个组件之间共享一些领域代码也很有意义。
这会让组件之间保持解耦,尽管他们会和同一份代码——共享内核——耦合在一起。
例如,有由一个组件触发并由其它一个或多个组件监听的事件就是这样的例子。但服务接口和事件实体也可能是这样。
不过,我们应该限制共享内核的大小,对它进行修改时要小心翼翼,才不会毫不知情地破坏使用它的代码。共享内核中的代码不经过其它使用它的团队的同意就不能修改,这一点非常重要。
通用子域
子域是领域中非常独立的一部分。通用子域并非是特定于我们的应用的子域,它可以在任何类似的应用中使用。
所以,如果我们的应用中有一部分是关于财务的,也许我们可以在应用中使用现有的财务相关的库。但是,无路以哪种方式实现,哪怕我们没有现成的库可用而要自己构建,如果这部分是通用子域,那么它就不是我们的核心业务,它应该被当作必要的而非决定性的因素。它不是我们应用中最重要部分,所以不是我们最好的专家重点关注的地方,毫无疑问它甚至不应该在主要的源代码之中,可能是通过依赖管理工具安装的。
结语
再一次说明,这里我选择探讨的 DDD 概念多是关于单一职责、低耦合、高内聚、逻辑隔离,这样我们的应用才能变得更一致、更简单、更快地变化并适应业务的需要。
引用来源
1992 – Ivar Jacobson – Object-Oriented Software Engineering: A use case driven approach
1996 – Robert C. Martin – Granularity
2003 – Eric Evans – Domain-Driven Design: Tackling Complexity in the Heart of Software
2014 – Eric Evans – Domain-Driven Design Reference