<领域驱动设计:软件核心复杂性应对之道> 这个书名高度概括的领域驱动设计(DDD)要解决的问题,那就是软件核心的复杂度。
那什么是软件的核心?什么是软件复杂度?DDD又是怎么应对的?
什么是软件的核心
<人月神话> 里面提到软件开发的任务分成 2 种
- 根本任务:打造构成抽象软件实体的复杂概念结构
- 次要任务:使用编程语言表达这些抽象实体,在空间和时间限制下将它们映射成机器语言
简单来说根本任务就是如何把业务通过抽象的软件实体表达出来,而次要任务就是如何用代码实现它。根本任务面对的更多是业务复杂度,而次要任务则是技术的复杂度。举个例子,如何通过软件实体表达"登录"的逻辑,
-
首先要确定登录的流程。
- 用户通过输入用户名和密码进行登录。
- 系统校验用户名长度是否合法。
- 系统校验输入登录的次数,超过10次则拒绝登录,需要等待10分钟后方可继续登录。
- 系统判断登录密码是否正确,正确则返回登录成功,失败则返回密码错误。
-
设计软件实体。软件实体其实就是领域层的元素,例如聚合,实体,值对象,领域服务等。
聚合
java
public void login(String usernameEntered, String passwordEntered){
validateAccountLength(usernameEntered)
User user = userRepository.getByUsername(usernameEntered)
user.validateLoginTime()
user.validatePassword()
}
- validateAccountLength,validatePassword,userRepository.getByUsername 等等的具体实现。
经过步骤1的梳理,我们得出我们步骤2的设计,最终通过具体代码的实现让整个流程能跑起来。如何实现步骤2就是软件开发的根本问题。它只是通过面向对象的设计方式把业务流程表达出来,和语言无关和技术细节无关。而具体步骤3要怎么实现,例如getByUsername是通过数据库还是第三方接口,校验登录次数要用数据库还是缓存等。
根本任务并不意味着在每个项目里面这部分都是最难最复杂的。不同项目的难点也会不一样,例如对于秒杀系统来说,技术复杂度会比业务复杂度更高,次要任务的难度也就会比根本任务要更高。但对于大部分复杂系统来说,根本任务难度会更高,
纯技术的问题其实都有非常多通用解决方案了,例如秒杀,存储,事务等。但几乎不会有业务一模一样的系统。不同系统面对的领域是不同的,要解决的问题也不一样。例如金融领域,电商领域等等,他们都有自己的业务流程和模型,甚至同一类型不同公司都有差异,没法通过通用的解决方案解决,因此<人月神话>的作者才说软件没有银弹,大部分技术的发展都是为了解决次要问题,但没法解决根本问题。
什么是软件的复杂度
<解构领域驱动设计> 中提到了规模,结构和变化是软件复杂度的成因。规模太大,结构混乱导致理解困难。软件的可变性导致系统会被频繁修改, 频繁修改会进一步导致规模变大,结构愈发混乱。
我认为结构其实就是为了减少规模带来的问题,结构是解决方案,只是结构如果运用得不好,会适得其反。软件的可变性会使得结构从一开始可能是合理的,但随着迭代的进行若不进行调整会变得不再合适,逐渐成为负担。
软件的规模
一开始的软件规模比较小,比较简单,完全可以不用分结构。像以前刚学 SSH 框架的时候,也是把逻辑都写在 JSP,这样逻辑一目了然,改起来也方便,随着逻辑越来越多,一个 JSP 越来越大,要改一个地方很难找到对应的代码,也很难看得懂代码,或者一次要改好几个地方。这时候我们就需要结构了。
软件的结构
结构就是一种分类。服务拆分,分层,设计模式,提取方法,都是一种结构。我们通过结构分而治之,服务拆分更多的是从业务领域的角度进行拆分,分层是从技术和业务的角度拆分,设计模式是从复用性的角度拆分,提取方法则是从代码的职责拆分。
结构是为了隔离,把概念,实现,变化等隔离开,让不同的部分减少相互影响,可以独自演进。
结构也是有成本的,首先需要先理解结构的构成。并且每次修改逻辑都需要知道要改哪些结构。糟糕的结构收益很小,成本又很高。例如糟糕的分层架构难以看出领域层与其他层的边界,还要结合几层的逻辑才看得懂业务逻辑。又例如糟糕的微服务拆分导致分布式单体(一次修改涉及到多个服务)。
所以合理的结构才是我们要追求的最终目标。
软件的可变性
软件的可变性使得结构有了时间的属性,在不同的时间里面相同的结构会带来不同的效果。而结构的改变往往成本都比较高,而且随着结构的层次的提升,改动的代价也会越来越大,当然收益也会更大。例如服务拆分可能会伤筋动骨,但能优化架构。局部代码重构的成本不高,但优化的范围有限。
需求是迭代的,逐步变化的,结构也是缓慢腐化的。而结构的变化常常只能一步到位。在业务压力下,结构往往只能将就着,大量的遗留系统常常就是结构不适用导致的问题。
DDD又是怎么应对软件复杂度
软件的规模是事实,无法控制。但我们可以利用结构分而治之。DDD 同样是通过结构来解决规模带来的问题。DDD 提供了几种结构。
- 从业务层面划分子领域,并通过限界上下文隔离业务概念
- 通过聚合,实体,值对象隔离业务逻辑
- 通过分层架构隔离技术和业务的复杂度
- 通过和领域专家协作构造领域模型来预测软件变化的方向
子领域
在 <实现领域驱动设计> 里面提到,从广义上讲,领域即是一个组织所做的事情以及其中所包含的一切。
领域是一个很宽泛和抽象的概念,而且领域有宏观有微观,有包含和被包含的关系。上面定义的领域是一个大的领域,它内部还可以拆分成多个子领域。子领域的划分并没有一个标准,和技术无关,每个人可能都有自己的划分方案。为什么要这么分比如何分更加重要,只要逻辑通,都是合理的。
子领域的划分除了便于我们理解领域组成之外,我们还可以基于子领域去识别资源的投入。所以子领域常常也被分为核心子领域,通用子领域和支撑子领域。对于核心子领域,我们可以投入更好的团队和更好的时间去打造领域模型,而非核心子领域,则可以通过购买,或者通过事务脚本的方式快速开发。通过核心子领域的划分隔离核心和非核心领域,让我们更关注核心领域的复杂度。
限界上下文
领域更偏向于业务划分,而限界上下文则更偏向于落地时模块的划分。限界上下文大体是基于领域划分的,但由于质量属性或者历史原因的影响导致限界上下文和领域划分的不一致。例如在原来系统中统计分析只是一个小功能,不足以成为一个子领域,因此一开始和原系统同属一个上下文。随着业务的发展统计分析变得越来越重要,迭代频率也变得越来越高,统计分析逐步成为一个支撑子领域。但因为代码模块尚未拆分成一个独立的上下文,因此变成一个上下文对应多个领域的情况。
限界上下文是子领域在落地时的权衡结果,需要同时考虑业务,技术和修改成本。
聚合,实体,值对象
DDD 采用面向对象分析和设计的方法,实际上就拥有面向对象带来的好处。面向对象让对象不仅拥有数据,还能拥有行为。对象成了一个自治的单元,屏蔽了实现细节。
面向对象的难点不在如何构建有行为的对象,而在于对象粒度的设计。粒度太大会导致某个对象职责过多,粒度太小会导致对象过多。DDD 提供了2层抽象,一层是聚合,一层是实体和值对象。聚合包含实体和值对象,聚合之间只能通过聚合根进行关联。通过这两层抽象能更好的提取粒度适中的对象。具体的设计方法后面再单独分析。
统一语言和领域模型
在设计不足和实际过度之间,我们总是难以找到合适的位置。关键还是我们的输入不足,无法支撑我们的设计意图。DDD 强调领域模型需要和领域专家一起沟通得到,这样能最大程度保证模型是合理的和可拓展的。沟通的前提是我们统一了语言,理解领域模型的价值。这就需要开发人员和领域专家各进一步,一起去学习和设计领域模型。
DDD 给我们带来什么
DDD 可能并没有什么创新的东西,包括面向对象,敏捷,领域模型。但 DDD 能把这些都串起来,取精去粕,让我们重新意识到领域模型的价值,这就是 DDD 的价值。
我们业务开发常常喜欢卷底层技术,把解决更难技术问题当成自己的目标。随着技术的发展分工越来越细,越来越多技术公司专注于解决各类技术问题,我们的机会越来越少。我们大部分时间都在写业务,也不是每个业务都需要高并发高可用的,如果只是看技术能力我们肯定是不如这类技术公司的开发人员。
业务开发同学需要在业务中找到自己的竞争力,而 DDD 能帮助我们找到方向。