解构领域驱动设计

978-7-115-56623-2
作者: 张逸
译者:
编辑: 刘雅思

图书目录:

详情

本书全面阐释了领域驱动设计(domain-driven design,DDD)的知识体系,内容覆盖领域驱动设计的主要模式与主流方法,并在此基础上提出“领域驱动设计统一过程”(domain-driven design unified process,DDDUP),将整个软件构建过程划分为全局分析、架构映射和领域建模3个阶段。除给出诸多案例来阐释领域驱动设计统一过程中的方法与模式之外,本书还通过一个真实而完整的案例全面展现了如何进行领域驱动设计统一过程的实施和落地。为了更好地运用领域驱动设计统一过程,本书还开创性地引入了业务服务、菱形对称架构、领域驱动架构、服务驱动设计等方法与模式,总结了领域驱动设计能力评估模型与参考过程模型。本书提出的一整套方法体系已在多个项目中推广和落地。 本书适合希望领会软件架构本质、提高软件架构能力的软件架构师,希望提高领域建模能力、打磨软件设计能力的开发人员,希望掌握业务分析与建模方法的业务分析人员,希望学习领域驱动设计并将其运用到项目中的软件行业从业人员阅读参考。

图书摘要

版权信息

书名:解构领域驱动设计

ISBN:978-7-115-56623-2

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。


著    张 逸

责任编辑 刘雅思

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


本书全面阐释了领域驱动设计(domain-driven design,DDD)的知识体系,内容覆盖领域驱动设计的主要模式与主流方法,并在此基础上提出“领域驱动设计统一过程”(domain-driven design unified process,DDDUP),将整个软件构建过程划分为全局分析、架构映射和领域建模3个阶段。除给出诸多案例来阐释领域驱动设计统一过程中的方法与模式之外,本书还通过一个真实而完整的案例全面展现了如何进行领域驱动设计统一过程的实施和落地。为了更好地运用领域驱动设计统一过程,本书还开创性地引入了业务服务、菱形对称架构、领域驱动架构、服务驱动设计等方法与模式,总结了领域驱动设计能力评估模型与参考过程模型。本书提出的一整套方法体系已在多个项目中推广和落地。

本书适合希望领会软件架构本质、提高软件架构能力的软件架构师,希望提高领域建模能力、打磨软件设计能力的开发人员,希望掌握业务分析与建模方法的业务分析人员,希望学习领域驱动设计并将其运用到项目中的软件行业从业人员阅读参考。


谨以本书献给我的妻子漆茜,以及我们的孩子张子瞻。同时,也献给生我养我的父母!




——周吉鑫[1]

本书的读者是幸运的!我运用领域驱动设计磕磕绊绊十余年,读过本书的内容之后,深感它是一本可与《领域驱动设计》和《实现领域驱动设计》互补的书,它在领域驱动设计落地方面尤其出色。

2007年,我阅读了Eric Evans的《领域驱动设计》。2014年,我又阅读了由Eric Evans作序、Vaughn Vernon编写的《实现领域驱动设计》,后来,我有幸认识了该书的审校者张逸老师,和张逸老师的沟通令我受益匪浅。虽然知道张逸老师在领域驱动设计方面功力颇深,但在拜读了张逸老师的这本书的初稿之后,我依然非常吃惊,觉得张逸老师真正做到了将领域驱动设计知识融会贯通。张逸老师在这本书中对限界上下文、聚合、领域服务概念进行了深刻阐述,并通过案例的运用让研发人员在使用这些概念时不再迷惑。他还在参透了六边形整洁架构、架构与分层架构的本质后,大胆突破,提出了精简的菱形对称架构,从架构角度让领域驱动设计更加容易理解和落地,并通过服务驱动设计,以任务分解的方式让测试驱动开发和领域驱动设计无缝结合,让设计可以推导验证,让开发人员可以自然而然写出不再“贫血”的代码。

本书不仅具备国内作者难得的宽阔视野和理论深度,而且有丰富的案例与实战经验总结,其中一些总结还细心地标明了出处,如关于过度设计和设计不足的权衡案例后面的总结:“具有实证主义态度的设计理念是面对不可预测的变化时,应首先保证方案的简单性;当变化真正发生时,可以通过诸如提炼接口(extract interface)[6]341的重构手法,满足解析逻辑的扩展。”

本书中有很多这样的总结,因此阅读这本书相当于吸收了很多本书的精华。记得张逸老师曾向我推荐过Robert Martin的《架构整洁之道》,当时我告诉他那是我2019年读的最好的一本书,而今天,我要告诉他,他的这本书是我2020年读到的最好的一本书!

[1] 周吉鑫,京东资深业务架构师,2011年起至今在京东公司进行物流系统的建模、分析和设计工作,主要工作包括京东亚洲一号WMS、WMS3.0~WMS6.0系统、云仓、国际化物流系统、无人仓系统等的建模、分析和设计。


——高翊凯[1]

2019年的冬天,我从台湾赶往上海,为公司内的团队进行领域驱动设计(DDD)与事件风暴的培训,在完成了培训工作后马不停蹄地赶往北京,只为了与领域驱动设计社区的伙伴王威、张逸相聚。在2019年领域驱动设计中国峰会(2019 DDD China Conference)上,我们分享了领域驱动设计台湾社区对领域驱动设计的理解与实践方式。是日恰巧迎来了北京的初雪,趁着此情此景,一行人把酒言欢,正是“初雪纷飞夜访,奇闻经历共话,点拨思绪再整,把酒笑谈学涯”。

当时我们探讨了一个很重要的话题:很多人在学习领域驱动设计时,往往初探不得其窍门,而工作背景不同的人看待这一方法又往往仅侧重于一部分战略指导,或者只关注战略设计实践的代码层级,但不管侧重于哪一部分,都会让这种经典的指导协作与实现业务战略目标的软件工程方法略显失重,无法尽得其精要。

2003年,Eric Evans的著作《领域驱动设计》从欧洲席卷而来,乃至于全球的软件工作者都渴望从他提出的领域建模方法中得到帮助,但十多年来,Eric Evans本人经常被问到:“有没有一种方法可以很好地指引我们进行业务建模?什么样的建模才是合理的,或者说可以一次就成其事达其标?”Eric Evans本人在很多公开场合都提过,其实这一切都依然需要依赖一些经验和持续积累的领域知识。对于这样需要由高度经验法则施行的领域驱动设计,一般的软件架构师、程序员以及相关的从业人员往往望而却步,个中原因便是始终少了一个系统性的指引,将业务流程梳理的产物对接到后续的程序开发中,实现从业务架构到系统架构的良好实践,使常见的业务与技术之间的隔阂降到最低。

对于这本书,我首先要向张逸老师表达感激之情,然后祝贺本书的读者!感激张逸老师花了多年时间梳理并融合了其在软件设计领域的实务经验,将战略推进到战术过程中佚失的部分,并通过领域驱动设计统一过程(DDDUP),使领域驱动设计方法更加完备。祝贺本书的读者在拿到本书时就几乎综览了过去20多年的软件开发历程所提到的诸多重要元素。本书结合大量实务案例来探讨为何需要战略指导、为何需要以固有的战术设计范式指导实践,并辅以领域驱动设计统一过程的指导原则,指引读者逐步落地。本书不单纯以领域驱动设计来讲老生常谈的方法,而更像一位坐在你身边的资深架构师,与你结对进行系统架构设计,一起探索软件架构设计的奥秘。

如果读者对软件工程有极大的热情,渴望更好地理解、实施领域驱动设计,解决复杂的业务问题,就千万别错过这本书。但我最真切地提醒读者,在购书之后务必阅读与身体力行兼具。行之才是学之。

[1] 高翊凯(Kim Kao),Amazon Web Services(AWS)资深解决方案架构师,领域驱动设计台湾社区共同发起人之一。他的专长是软件系统设计,并致力于无服务器服务推广,推动企业透过领域驱动设计与便捷的云端服务打造更适切的建构系统方案,解决实际的业务问题。


——王立[1]

自2003年Eric Evans的著作《领域驱动设计》面世以来,领域驱动设计(DDD)相关的实践书籍并不多,整体的理论发展速度并不快,以至于很长一段时间,开发团队的实践过程总是磕磕绊绊,这让他们觉得领域驱动设计的门槛很高,甚至有人怀疑领域驱动设计是否是一种足够成熟与体系化的方法论。根据我个人的经验,我确实发现其中不少问题仍旧没有什么经典论著能完全覆盖与讨论。看过这本书的内容后,我的感受是:无论是理论还是实践,领域驱动设计知识体系确实都已经成熟了,与国内外的经典领域驱动设计著作相比,这本书包含了更多案例,覆盖了更多问题场景,回答了更多人们不常考虑的细节。本书作者不仅继承了各类经典著作的精华,更难得的是他能够在实践中深入细节进行推敲,批判与改良一些不成熟的理论,甚至有了自己的理论创新,例如,提出了菱形架构概念、对强一致事务与聚合的边界的一致性提出挑战……特别是,他还创造性地提出了领域驱动设计统一过程(DDDUP),很好地总结了完整的领域驱动设计知识体系。

有些读者可能不理解本书为什么这么厚。网络上有大量碎片式的领域驱动设计文章,一个案例只有几页,市场上也有不少领域驱动设计方面的培训,两天就能帮我们“搞定”领域驱动设计,领域驱动设计的知识体系似乎并没有我们想象的那么丰满。但事实上,这本书将告诉我们,领域驱动设计背后完整的知识体系并没有那么简单,我们需要掌握的是从业务到技术的整个技能栈。我们必须接受的事实是:领域驱动设计是有一定学习曲线的。所以,不要拒绝一本足够厚的书,这恰恰是其价值的体现。这本书的各个部分不是泛泛而谈,而是通过展开细节,层层推进,帮助读者建立扎实的理论基础,并通过大量翔实的案例,让读者能灵活运用理论知识。对于初学者,本书尽可能详尽地把问题展开、讲透;对于有一定经验的老手,本书也有更多有深度的细节思考和理论拓展。相信这本书会成为国内领域驱动设计技术书籍的一个标杆。

张逸先生是我国最早一批接触并实践领域驱动设计的先行者,经验极其丰富。本书不仅是他在该领域十多年实战经验的沉淀和升华,也是他多年教学经验的总结和提炼。他曾经为很多行业巨头提供过咨询服务,是国内在领域驱动设计方面影响力最大的布道者之一。看到张逸先生的书终于要出版了,我感到非常高兴,我们太需要这样一本既有理论升华又如此接地气的大作了。

我熟读了几乎所有的领域驱动设计经典著作,但仍旧从张逸先生的书中获益良多。我认为本书的广度、深度与创新性已经可以与该领域的国际经典著作看齐,这也是国人的骄傲。本书的出版是领域驱动设计理论界的一个重要事件,是对软件行业在领域驱动设计方面的巨大贡献,必将降低整个行业掌握领域驱动设计的门槛,加速领域驱动设计的普及。能为这本书作序是我的荣幸,同为领域驱动设计布道者,我将向我的同行强烈推荐本书。这本书也是我本人将来开展工作的重要理论指导。

[1] 王立,微信支付12级专家工程师、技术领导者。他从2006年起开始研究领域驱动设计,曾经在阿里巴巴、神州数码、网宿科技等上市公司担任技术专家与技术经理,现在负责腾讯微信支付和智慧零售技术团队在领域建模、分析和设计方面的实践指导。


——于君泽(右军)[1]

领域驱动设计方面的书现在不是太多,而是太少。想必不少读者受过《领域驱动设计》和《实现领域驱动设计》两本书的启蒙。本书是我特别推荐的领域驱动设计方面的技术书,为何特别推荐,且听下文。

大约在2007年,我第一次读《领域驱动设计》一书时,如读天书,主要记住了类似实体、值对象、工厂、仓储等概念。近年来,随着微服务的流行,国内对领域驱动设计的研究和实践愈发多了起来。

我对领域驱动设计的态度是:相对于战术设计,应该更看重战略设计。数年前,我醉心于研究领域模型。领域是业务变化中接近不变性的部分,业务包括领域对象、业务逻辑和界面交互3个层次,其中领域对象是最稳定的。2015年我组织领域建模工作坊活动时,用的就是《分析模式:可复用的对象模型》一书中的一个需求场景。2016年我写了一篇文章,强调了问题域和解决方案域的区分。张逸兄在GitChat上的两个连载专栏历时两年,创作数十万字,内容之丰满,关键节点探讨之深刻,于我之所见,浩瀚领域专家,无出其右者。虽大家都各自奔忙,仅偶有线上问候或者面聊,但皆有受益。本书的成书过程尤其令人钦佩,张逸兄不是直接将专栏调整成书,而是重新组织架构,提炼出自己的方法体系,可以说是推陈出新,自成一家。

张逸兄敢言人之所未言。领域驱动设计有四大不足:领域驱动设计缺乏规范的统一过程,领域驱动设计缺乏与之匹配的需求管理体系,领域驱动设计缺乏规范化的、具有指导意义的架构体系,领域驱动设计的领域建模方法缺乏固化的指导方法。他创造性地提出领域驱动设计统一过程,虽然此方法有无调整空间,一定是要在不断实践中去检验的,但单就他的这份胆识和专业,足以让人钦佩。

如果说非要给本书提一点儿意见的话,我觉得本书有点儿厚了。我认为一本好书也要兼顾读者的情况,最好能达到让读者快速上手的学习效果。但张逸兄坚持让本书以集大成者的面貌出现,洋洋洒洒数十万字,力求让其成为一本值得珍藏的技术书。

凡学习,须循序渐进。我建议读者把面向对象的分析(object-oriented analysis,OOA)、面向对象的设计(object-oriented design,OOD)、统一建模语言(unified modeling language,UML)、模式等相关知识作为阅读本书的前序内容。《领域驱动设计》一书也特别提到了“复杂性”,有一定的软件从业经验的朋友对“复杂性”更感同身受。

每个人心中都有一个哈姆雷特,每一位读者都可以登临领域驱动设计的阁楼,从不同的角度或俯瞰、或仰望、或凝视。我之所得:于道,是对限界上下文特别有共鸣的部分,以及问题空间(域)与解空间(域);于术,是作者提出的领域驱动设计的“三大纪律八项注意”,可作为团队执行作战任务的纪律规范。其中,“三大纪律”是实施领域驱动设计的准则:

信笔至此,兹为张兄推荐。本书精彩之处甚多,留待读者去发现。祝阅读愉快!

[1] 于君泽(右军),技术专家,《深入分布式缓存:从原理到实践》和《程序员的三门课:技术精进、架构修炼、管理探秘》联合作者。


写下本书第一个字的具体时间已不可考。从文档创建的时间看,本书的写作至少可以追溯到2017年11月,屈指算来,三载光阴已逝。为了本书,我已算得上呕心沥血。回想这三年多时光,无论是在万米高空的飞行途中,还是在蔚蓝海边的旅行路上,抑或工作之余正襟危坐于书桌之前,我的心弦一刻不敢放松,时刻沉思体系的构建,纠结案例的选择,反复推敲文字的运用。我力求输出最好的内容,希望打造领域驱动设计技术书籍的经典!

我在ThoughtWorks的前同事滕云开我的玩笑:“老人家,你写完这本书,也就功德圆满了!”“老人家”是我在ThoughtWorks的诨名。我虽然对此称呼一直敬谢不敏,不过写作至今,我已心力交瘁,被称作“老人家”,也算“名副其实”了。至于是否“功德圆满”,就要交给读者诸君来品评了。

本书内容主要来自我在GitChat发布的课程“领域驱动设计实践”。该课程历经两年打造,完成于2020年1月21日。当时的我,颇有感慨地写下如此后记:

课程写作结束了。战略篇一共34章,约15.5万字;战术篇一共71章,约35.1万字。合计105章,50.6余万字,加上2篇访谈录、2篇开篇词与这篇可以称为“写后感”的后记,共110章。如此成果也足可慰藉我为之付出的两年多的艰辛时光!

我对“领域驱动设计实践”课程的内容还算满意,然而,随着我对领域驱动设计的理解的蜕变与升华,我的“野心”也在不断膨胀,我不仅希望讲清楚应该如何实践领域驱动设计,还企图对这套方法体系进行深层次的解构。这也是本书书名《解构领域驱动设计》的由来。

所谓“解构”,就是解析与重构:

我钦佩并且尊敬Eric Evans对领域驱动设计革命性的创造,他对设计的洞见让我尊敬不已。尤其在彻底吃透限界上下文的本质之后,微服务又蔚然成风,我更加佩服他的远见卓识。然而,尊敬不是膜拜,佩服并非盲从,在实践领域驱动设计的过程中,我确实发现了这套方法体系天生的不足。于是,我在本书中提出了我的GitChat课程不曾涵盖的领域驱动设计统一过程(domain-driven design unified process,DDDUP),相当于我站在巨人Eric Evans的肩膀上,构建了自己的一套领域驱动设计知识体系。

领域驱动设计统一过程的提出,从根基上改变了本书的结构。我调整和梳理了本书的写作脉络,让本书呈现出与“领域驱动设计实践”课程迥然有别的全新面貌。本书不再满足于粗略地将内容划分为战略篇和战术篇,而是在领域驱动设计统一过程的指导下,将该过程的3个阶段——全局分析、架构映射和领域建模作为本书的3个核心篇,再辅以开篇和融合,共分为5篇(20章)和4个附录,全面而完整地表达了我对领域驱动设计的全部认知与最佳实践。在对内容做进一步精简后,本书仍然接近600页,算得上是软件技术类别的大部头了。

该如何阅读这样一本厚书呢?

若你时间足够充裕,又渴望彻底探索领域驱动设计的全貌,我建议还是按部就班、循序渐进地进行阅读。或许在阅读开篇的3章时,你会因为太多信息的一次性涌入而产生迷惑、困扰和不解,这只是因为我期望率先为读者呈现领域驱动设计的整体面貌。在获得领域驱动设计的全貌之后,哪怕你只是在脑海中存留了一个朦胧的轮廓,也足以开启自己对设计细节的理解和认识。

若你追求高效阅读,又渴望寻求领域驱动设计问题的答案,可以根据目录精准定位你最为关心的技术讲解。或许你会失望,甚至产生质疑,从目录中你获得了太多全新的概念,而这些概念从未见于任何一本领域驱动设计的图书,这是因为这些概念都是我针对领域驱动设计提出的改进与补充,是我解构全新领域驱动设计知识体系的得意之笔——要不然,一本技术图书怎么会写三年之久呢?

我将自鸣得意的开创性概念一一罗列于此。

以上概念皆为领域驱动设计统一过程的设计元素,又都能与领域驱动设计的固有模式有机融合。对软件复杂度成因的剖析,对价值需求和业务需求的划分,在领域驱动设计统一过程基础上建立的能力评估模型与参考过程模型,提出的诸多新概念、新方法、新模式、新体系,虽说都出自我的一孔之见,但也确乎来自我的一线实践和总结,我自觉其可圈可点。至于内容的优劣,还是交给读者评判吧。

若读者在阅读本书时有任何意见与反馈,可关注我的微信公众号“逸言”与我取得联系,我也会在公众号上发布后续我对领域驱动设计体系的更多探索与思考,也欢迎读者加入我的知识星球“NoDDD”,与我共同探讨软件技术的二三事。

照例给出致谢!

感谢GitChat创始人谢工女士,没有她的支持与鼓励,就不会有“领域驱动设计实践”课程的诞生,自然也就不会让我下定决心撰写本书。感谢人民邮电出版社异步图书的杨海玲女士,她没有因为错过最好的出版时机而催促我尽快交稿,她的宽容与耐心使我有足够充裕的时间精心打磨本书的内容。感谢本书的责任编辑刘雅思以及异步图书的其他素未谋面的幕后工作者,是他们认真严谨地保障了本书顺利走完“最后一公里”,抵达终点。感谢京东周吉鑫、AWS高翊凯(Kim Kao)、腾讯王立与技术专家于君泽(花名“右军”)诸兄的抬爱,他们不仅拨冗为我的著作作序,也给了我许多好的建议与指点,提升了本书的整体质量。感谢老东家ThoughtWorks的徐昊、王威、肖然、滕云、杨云等同事,他们曾经是我同一战壕的战友,在写书过程中,我也得到了他们的鼎力相助。感谢阿里的彭佳斌(花名“言武”)、自主创业人张闯、中航信杨成科、工商银行劳永安,四位兄台作为试读本书的第一批读者,花费了大量时间认真阅读了我的初稿,提出了非常宝贵的反馈意见,帮助我订正了不少错误。感谢我的领域驱动设计技术交流群的近1600名群友,他们的耐心等待以及坚持不懈的督促,使我能够坚持写完本书。

之所以“三年磨一剑”,是希望通过我的努力让本书的质量对得起读者!可是,对得起读者的同时,我却对不起我生命中最重要的两个人:我的妻子漆茜与儿子张子瞻。这三年我把大部分业余时间都用于写作这本书,多少个晚上笔耕不缀,妻子陪着儿子,我则陪着电脑,对此我深感愧疚。妻儿为了支持我的创作,没有怨怼,只有默默的支持,子瞻还为本书贡献了一幅美丽的插图。本书的出版,有他们一大半的功劳!最后,还要感谢我的父母,每次匆匆回家看望他们,都只有极短的时间和他们聊天,挤出来的时间都留给了本书的写作!

在写这篇前言的前一天,我偶然读到苏东坡的一首小词:

春未老,风细柳斜斜。试上超然台上看,半壕春水一城花。烟雨暗千家。

寒食后,酒醒却咨嗟。休对故人思故国,且将新火试新茶。诗酒趁年华。

蓦然内心被叩击,仿佛心弦被优美的辞章轻轻地带着诗意拨弄。吾身虽不能上超然台,然而书成之后,可否看到半壕春水一城花?未曾饮酒,却咨嗟,是否多情笑我早生华发?如今的我,已然焙出新火,恰当新火试新茶,却不知待到明年春未老时,能否做到何妨吟啸且徐行的落拓不羁?无论如何,还当诗酒趁年华——仰天大笑出门去,吾辈岂是蓬蒿人!

张逸

于公元2020年11月24日夜

时旅居北京顺义区蓝天苑


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,点击“提交勘误”,输入勘误信息,点击“提交”按钮即可(见下图)。本书的作者和编辑会对您提交的勘误信息进行审核,确认并接受您的建议后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

本书责任编辑的联系邮箱是liuyasi@ptpress.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频或者参与技术审校等工作,可以直接发邮件给本书的责任编辑。

如果您来自学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接通过邮件发给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端和网络技术等。

异步社区

微信服务号


开篇,明义。

领域驱动设计(domain-driven design,DDD)需要应对软件复杂度的挑战!那么,软件复杂度的成因究竟是什么,又该如何应对?概括而言,即:

领域驱动设计对软件复杂度的应对之道可进一步阐述为:

子领域、限界上下文、分层架构和聚合皆为领域驱动设计的核心元模型,分属战略设计和战术设计,贯穿了从问题空间到解空间的全过程。

领域驱动设计的开放性是其生命长青的基石,但它过于灵活的特点也让运用它的开发团队举步维艰。我之所以提出领域驱动设计统一过程,正是要在开放的方法体系指导之下,摸索出一条行之有效的软件构建之路,既不悖于领域驱动设计之精神,又不吝于运用设计元模型,通过提供简单有效的实践方法,建立具有目的性和可操作性的构建过程。

领域驱动设计统一过程分为3个阶段:

每个阶段的过程工作流既融合了领域驱动设计既有的设计元模型,又提出了新的模式、方法和实践,丰富了领域驱动设计的外延。领域驱动设计统一过程对项目管理、需求管理和团队管理也提出了明确的要求,因为它们虽然不属于领域驱动设计关注的范畴,却是保证领域驱动设计实践与成功落地的重要因素。

领域驱动设计统一过程是对领域驱动设计进行解构的核心内容!


计算机编程的本质就是控制复杂度。

——Brian Kernighan

复杂的事物中蕴含着无穷的变化,让人既沉迷其美,又深恐自己无法掌控。我们每日每时对软件的构建就在与复杂的斗争中不断前行。软件系统的复杂度让我觉得设计有趣,因为每次发现不同的问题,都会有一种让人耳目一新的滋味油然而生,仿佛开启了新的旅程,看到了不同的风景。同时,软件系统的复杂度又让我觉得设计无趣,因为要探索的空间实在太辽阔,一旦视野被风景所惑,就会迷失前进的方向,感到复杂难以掌控,从而失去构建高质量系统的信心。

那么,什么是复杂系统?

我们很难给复杂系统下一个举世公认的定义。专门从事复杂系统研究的Melanie Mitchell在接受Ubiquity杂志专访时,“勉为其难”地为复杂系统给出了一个相对通俗的定义:“由大量相互作用的部分组成的系统。与整个系统比起来,这些组成部分相对简单,没有中央控制,组成部分之间也没有全局性的通信,并且组成部分的相互作用导致了复杂行为。[1]388

这个定义庶几可以表达软件复杂度的特征。定义中的“组成部分”对于软件系统,就是所谓的“软件元素”,基于粒度的不同可以是函数、类、模块、组件和服务等。这些软件元素相对简单,然而彼此之间的相互作用却导致了软件系统的复杂行为。软件系统符合复杂系统的定义,不过是进一步证明了软件系统的复杂度。然而该如何控制软件系统的复杂度呢?恐怕还要从复杂度的成因开始剖析。

Jurgen Appelo从理解能力与预测能力两个维度分析了复杂度的成因[2]39。这两个维度各自分为不同的复杂层次:

两个维度都蕴含了“复杂”的含义:前者与简单相对,意为复杂至难以理解,可阐释为“复杂难解”;后者与有序相对,意为它的发展规律难以预测,可阐释为“复杂难测”。在预测能力维度,“难测”还不是最复杂的层次,最高层次为混沌,即根本不可预测。两个维度交叉,可以形成6种代表不同复杂意义的层次定义,Jurgen Appelo通过图1-1形象地说明了各个复杂层次的特征。

图1-1 复杂系统的特征[1]

以下是Jurgen Appelo对这些例子给出的说明[2]39

我的内衣很简单。我很容易理解它们的工作原理。我的手表是精密复杂的,如果把它拆开,我需要很长时间才能了解其设计原理和组件。但是我的手表或我的内衣都没有什么让人吃惊的(至少对我而言)。它们是有序的、可以预测的系统。

一个三人软件开发团队也是简单的,只需要开几次会议,提供一些晚餐,外加几杯啤酒,就可以了解这个团队的每一个人了。一座城市是不简单的、繁杂的,出租车司机需要几年时间才能熟悉这座城市的所有街道、胡同、宾馆和饭店。但同时,团队和城市又都是复杂的。不管你有多了解它们,总会有意想不到的事情发生它们身上。在某种程度上,它们是可预测的,但是你永远不清楚明天会发生什么。

双摆(两个摆锤互相连接)也是一个简单的系统,容易制作也很容易理解。但因为对钟摆的初始设置具有高度敏感性,所以它进行的是不可预测的混沌运动。股票市场也是混沌的,根据定义,它是不可预测的,否则每个人都知道怎么利用股票交易来赚钱,就会导致整个系统崩盘。但是,股票市场又不像钟摆那样,它是相当繁杂的。

软件系统属于哪一个复杂层次呢?

大多数软件系统需要实现的整体功能往往是难以理解的,同时,随着需求的不断演进,它又在一定程度具有未来的不可预测性,这意味着软件系统的“复杂”同时覆盖了“复杂难解”(complicated)与“复杂难测”(complex)两个层面,对标图1-1给出的案例,就是一座城市的复杂特征。无独有偶,Pete Goodliffe也将软件系统类比为城市,他说:“软件系统就像一座由建筑和后面的路构成的城市——由公路和旅馆构成的错综复杂的网络。在繁忙的城市里发生着许多事情,控制流不断产生,它们的生命在城市中交织在一起,然后死亡。丰富的数据积聚在一起、存储起来,然后销毁。有各式各样的建筑:有的高大美丽,有的低矮实用,还有的坍塌破损。数据围绕着它们流动,形成了交通堵塞和追尾、高峰时段和道路维护。”[5]33既然如此,那么设计一个软件系统就像规划一座城市,既要考虑城市布局,以便居民的生活与工作,满足外来游客或商务人员的旅游或出差需求,又要考虑未来因素的变化,例如“当居民对城市的使用方式有所变化,或者受到外力的影响时,城市就会相应地演化”[3]13。参考城市的复杂度特征,我们要剖析软件系统的复杂度,就可以从理解能力预测能力这两个维度探索软件复杂度的成因。

是什么阻碍了开发人员对软件系统的理解?设想项目组招入一位新人,当这位新人需要理解整个项目时,就像一位游客来到一座陌生的城市。他是否会迷失在错综复杂的城市交通体系中,不辨方向?倘若这座城市实则是乡野郊外的一座村落,只有房屋数间,一条街道连通城市的两头,他还会生出迷失之感吗?

因而,影响理解能力的第一要素是规模

软件的需求决定了系统的规模。一个只有数十万行代码的软件系统自然不可与有数千万行代码的大规模系统相提并论。软件系统的规模取决于需求的数量,更何况需求还会像树木那样生长。一棵小树会随着时间增长渐渐长成一棵参天大树,只有到了某个时间节点,需求的数量才会慢慢稳定下来。当需求呈现线性增长的趋势时,为了实现这些功能,软件规模也会以近似的速度增长。

系统规模的扩张,不仅取决需求的数量,还取决于需求功能点之间的关系。需求的每个功能不可能做到完全独立,彼此之间相互影响相互依赖,修改一处就会牵一发而动全身,就好似城市中的某条道路因为施工需要临时关闭,车辆只得改道绕行,这又导致了其他原本已经饱和的道路因为涌入更多车辆而变得更加拥堵。这种拥堵现象又会顺势向其他分叉道路蔓延,形成辐射效应。

软件开发的拥堵现象或许更严重,这是因为:

随着软件系统规模的扩张,软件复杂度也会增长。这种增长并非线性的,而是呈现出更加陡峭的指数级趋势。这实际上是软件的熵发挥着副作用。正如David Thomas与Andrew Hunt认为的:“虽然软件开发不受绝大多数物理法则的约束,但我们无法躲避来自熵的增加的重击。熵是一个物理学术语,它定义了一个系统的‘无序’总量。不幸的是,热力学法则决定了宇宙中的熵会趋向最大化。当软件中的无序化增加时,程序员会说‘软件在腐烂’。”[4]6

软件之所以无法躲避熵的重击,源于我们在构建软件时无法避免技术债(technical debt)[2]。不管软件的架构师与开发人员有多么的优秀,他们针对目前需求做出的看似合理的技术决策,都会随着软件的演化变得不堪一击,区别仅在于债务的多少,以及偿还的利息有多高。根据Ward Cunningham的建议,对付技术债的唯一方案就是尽量让它可见,例如通过技术债列表或者技术债雷达等可视化形式及时呈现给团队成员,并制订计划主动地消除或降低技术债。

我曾经负责设计与开发一款商业智能(business intelligence,BI)产品,它需要展现报表下的所有视图。这些视图的数据来自多个不同的数据集,视图的展现类型多种多样,如柱状图、折线图、散点图和热力图等。在这个“逼仄”的报表问题空间中,需要满足如下业务需求:

以上业务需求都是事先规划好,并且可以清晰预见的,由于它们都对视图进行操作,因此视图控件的多个操作之间出现冲突。例如,高亮与级联都需要响应相同的点击事件。钻取同样如此,不同之处在于它要判断钻取开关是否已经打开。在操作效果上,高亮与钻取仅针对当前视图,联动与筛选则会因为当前视图的操作影响到同一张报表下相同数据集的其他视图。对于拖曳操作,虽然它监听的是MouseDown事件,但该事件与Click事件存在一定的冲突。

多个功能点的开发实现以及功能点之间存在的千丝万缕的关系带来了软件规模的成倍扩张:不同的业务场景会增加不同的分支,导致圈复杂度的增加;设计上如果未能做到功能之间的正交,就会使得功能之间相互影响,导致代码维护成本的增加;没有为业务逻辑编写单元测试,建立功能代码的测试网,就可能因为对某一处功能实现的修改引入了潜在的缺陷,导致系统运行的风险增加。纷至沓来的技术债逐渐积累,一旦累积到某个临界点,就会由量变引起质变,在软件系统的规模达到巅峰之时,迅速步入衰亡的老年期,成为“可怕”的遗留系统(legacy system)。这遵循了饲养场的奶牛规则:奶牛逐渐衰老,最终无奶可挤;与此同时,奶牛的饲养成本却在上升。

软件规模的一个显著特征是代码行数(lines of code)。然而,代码行数常常具有欺骗性。如果需求的功能数量与代码行数之间呈现出不成比例的关系,说明该系统的生命体征可能出现了异常,例如,代码行数的庞大其实可能是一种肥胖症,意味着可能出现了大量的重复代码。

我曾经利用Sonar工具对咨询项目的一个模块执行代码静态分析,分析结果如图1-2所示。

图1-2 代码静态分析结果

该模块代码共计40多万行,重复代码竟然占到了惊人的33.9%,超过一半的代码文件混入了重复代码。显然,这里估算的代码行数并没有真实地体现软件规模;相反,重复的代码还额外增加了软件的复杂度。

Neal Ford认为需要通过指标指导设计[3],例如使用面向对象设计质量评估的平台工具iPlasma,通过它生成的指标可以作为评价软件规模的要素,如表1-1所示。

表1-1 质量评估指标

编  码

说  明

NDD

直接后代的数量

HIT

继承树的高度

NOP

包的数量

NOC

类的数量

NOM

方法的数量

LOC

代码行数

CYCLO

圈复杂度

CALL

每个方法的调用数

FOUT

分散调用(给定的方法调用的其他方法数量)

在面向对象设计的软件项目里,除了代码行数,包、类、方法的数量,继承的层次以及方法的调用数,还有我们常常提及的圈复杂度,都会或多或少地影响整个软件系统的规模。

你去过迷宫吗?相似而回旋繁复的结构使得封闭狭小的空间被魔法般地扩展为一个无限的空间,变得无穷大,仿佛这空间被安置了一个循环,倘若没有找到正确的退出条件,循环就会无休无止,永远无法退出。许多规模较小却格外复杂的软件系统,就好似这样的一座迷宫。

此时,结构成了决定系统复杂度的一个关键因素。

结构之所以变得复杂,多数情况下还是由系统的质量属性(quality attribute)决定的。例如,我们需要满足高性能、高并发的需求,就需要考虑在系统中引入缓存、并行处理、CDN、异步消息以及支持分区的可伸缩结构;又例如,我们需要支持对海量数据的高效分析,就得考虑这些海量数据该如何分布存储,并如何有效地利用各个节点的内存与CPU资源执行运算。

从系统结构的视角看,单体架构一定比微服务架构更简单,更便于掌控,正如单细胞生物比人体的生理结构要简单。那么,为何还有这么多软件组织开始清算自己的软件资产,花费大量人力物力对现有的单体架构进行重构,走向微服务化?究其主因,还是系统的质量属性。

纵观软件设计的历史,不是分久必合合久必分,而是不断拆分的微型化过程。分解的软件元素不可能单兵作战。怎么协同,怎么通信,就成了系统分解后面临的主要问题。如果没有控制好,这些问题固有的复杂度甚至会在某些场景下超过分解带来的收益。例如,对企业IT系统而言,系统与系统之间的集成往往通过与平台无关的消息通信来完成,由此就会在各个系统乃至模块之间形成复杂的通信网结构。要理清这种通信网结构的脉络,就得弄清楚系统之间消息的传递方式,明确消息格式的定义,即使在系统之间引入企业服务总线(Enterprise Service Bus,ESB),也只能减少点对点的通信量,而不能改变分布式系统固有的复杂度,例如消息通信不可靠,数据不一致等因为分布式通信导致的意外场景。换言之,系统因为结构的繁复增加了复杂度。

软件系统的结构繁复还会增加软件组织的复杂度。系统架构的分解促成了软件构建工作的分工,这种分工虽然使得高效的并行开发成为可能,却也可能因为沟通成本的增加为管理带来挑战。管理一个十人团队和百人团队,其难度显然不可相提并论,对百人团队的管理也不仅仅是细分为10个十人团队这么简单,这其中牵涉到团队的划分依据、团队的协作模式、团队成员组成与角色构成等管理因素。

康威定律(Conway’s law)[4]就指出:“任何组织在设计一套系统(广义概念上的系统)时,所交付的设计方案在结构上都与该组织的沟通结构保持一致。”Sam Newman认为是需要“适应沟通途径”使得康威定律在软件结构与组织结构中生效[3]163。他分析了一种典型的分处异地的分布式团队。整个团队共享单个服务的代码所有权,由于分布式团队的地域和时区界限使得沟通成本变高,因此团队之间只能进行粗粒度的沟通。当协调变化的成本增加后,人们就会想方设法降低协调和沟通的成本。直截了当的做法就是分解代码,分配代码所有权,物理分隔的团队各自负责一部分代码库,从而能够更容易地修改代码,团队之间会有更多关于如何集成两部分代码的粗粒度的沟通。最终,与这种沟通路径匹配形成的粗粒度应用程序编程接口(application programming interface,API)构成了代码库中两部分之间的边界。

注意,与设计方案相匹配的团队结构指的是负责开发的团队组织,而非使用软件产品的客户团队。我们常常遇见分布式的客户团队,例如,一些客户团队的不同的部门位于不同的地理位置,他们的使用场景也不尽相同,甚至用户的角色也不相同,但在对软件系统进行架构设计时,我们却不能按照部门组织、地理位置或用户角色来分解模块(服务),并错以为这遵循了康威定律。

我曾经参与过一款通信产品的改进与维护工作。这是一款为通信运营商提供对宽带网的授权、认证与计费工作的产品,它的终端用户主要由两种角色组成:营业厅的营业员与购买宽带网服务的消费者。最初,设计该产品的架构师就错误地按照这两种不同的角色,将整个软件系统划分为后台管理系统与服务门户两个完全独立的子系统,为营业员与消费者都提供了资费套餐管理、话单查询、客户信息维护等相似的业务。两个子系统产生了大量重复代码,增加了软件系统的复杂度。在我接手该通信产品时,因为数据库性能瓶颈而考虑对话单数据库进行分库分表,发现该方案的调整需要同时修改后台管理系统与服务门户的话单查询功能。

无论设计是优雅还是拙劣,系统结构都可能因为某种设计权衡而变得复杂。唯一的区别在于前者是主动地控制结构的复杂度,而后者带来的复杂度是偶发的,是错误的滋生,是一种技术债,它会随着系统规模的增大产生一种无序设计。《架构之美》中第2章“两个系统的故事:现代软件神话”详细地罗列了无序设计系统的几种警告信号[5]34

看一个无序设计的软件系统,就好像隔着一层半透明的玻璃观察事物,系统的软件元素都变得模糊不清,充斥着各种技术债。细节层面,代码污浊不堪,违背了“高内聚松耦合”的设计原则,要么许多代码放错了位置,要么出现重复的代码块;架构层面,缺乏清晰的边界,各种通信与调用依赖纠缠在一起,同一问题空间的解决方案各式各样,让人眼花缭乱,仿佛进入了没有规则的无序社会。

分层架构的引入原本是为了维护系统的有序性,而如果团队却不注意维护逻辑分层确定的边界,不按照架构规定的层次分配各个类的职责,就会随着职责的乱入让逻辑分层形成的边界变得越来越模糊。我在对一个项目进行架构评审时,曾看到图1-3所示的三层架构。

图1-3 层次混乱的架构

虽然架构师根据关注点的不同划分了不同的层次,但各个逻辑层没有守住自己的边界:业务逻辑层定义了EditableControlBaseEditablePageBasePageBase等类,它们都继承自ASP.NET框架的UserControl用户控件类,同时又作为自定义用户控件的父类,提供了控件数据加载、提交等通用职责;继承这些父类的子类属于用户控件,定义在用户展现层,如EditablePageBase类的子类(如DictionaryTypeViewDictionaryViewDictionaryTypeList等)。一旦逻辑层没有守住自己的边界,分层架构模式就失去了规划清晰结构的价值。随着需求的增加,系统结构会变得越来越混乱,最终陷入无序设计的泥沼。

当我们掌握了事物发展的客观规律时,就具有了一定的对未来的预测能力。例如,我们洞察了万有引力的本质,就能够对观察到的宇宙天体建立模型,相对准确地推测出各个天体在未来一段时间的运行轨迹。然而,宇宙空间变化莫测,或许一个星球“死亡”产生的黑洞的吸噬能力,就可能导致那一片星域产生剧烈的动荡,这种动荡会传递到更远的星空,从而使天体的运行轨迹偏离我们的预测结果。毫无疑问,影响预测能力的关键要素在于变化。对变化的应对不妥,就会导致过度设计或设计不足。

设计软件系统时,变化让我们患得患失,不知道如何把握系统设计的度。若拒绝对变化做出理智的预测,系统的设计会变得僵化,一旦有新的变化发生,修改的成本会非常大;若过于看重变化产生的影响,渴望涵盖一切变化的可能,若预期的变化没有发生,我们之前为变化付出的成本就再也补偿不回来了,这就是所谓的“过度设计”。

我曾经在设计一款教育行业产品时,因为考虑太多未来可能的变化,引入了不必要的抽象来保证产品的可扩展性,使得整个设计方案变得过于复杂。更加不幸的是,我所预知的变化根本不曾发生。该设计方案针对产品的UI引擎(UI engine)模块。作为驱动界面的引擎,它主要负责从界面元数据获取与界面相关的视图属性,并根据这些属性来构造界面,实现界面的可定制。产品展现的视图由诸多视图元素组合而成,这些视图元素的属性通过界面元数据进行定制。为此,我为视图元素定义了抽象的ViewElement接口,作为所有视图元素类型包括SelectViewCheckboxGroupView的抽象类型。

ViewElement决定了视图元素的类型,从而确定呈现的格式;至于真正生成视图呈现代码的职责,则交给了视图元素的解析器。由于我认为视图元素的呈现除需要支持现有的JSP之外,未来可能还要支持HTML、Excel等实现元素,因此在设计解析器时,定义了ViewElementResolver接口:

public interface ViewElementResolver {
   String resolve(ViewElement element);
}

ViewElementResolver接口确保了解析功能的可扩展性,为了更好地满足未来功能的变化,我又引入了解析器的工厂接口ViewElementResolverFactory以及实现该接口的抽象工厂类AbstractViewElementResolverFactory

public interface ViewElementResolverFactory {
   ViewElementResolver create(String viewElementClassName);
}
public abstract class AbstractViewElementResolverFactory implements 
    ViewElementResolverFactory {
   public ViewElementResolver create(String viewElementClassName) {
      String className = generateResolverClassName(viewElementClassName);
      //通过反射创建ViewElementResolver对象
   }

   private String generateResolverClassName(String viewElementClassName) {
      return getPrefix() + viewElementClassName + "Resolver";
   }

   protected abstract String getPrefix();
}

public class JspViewElementResolverFactory extends AbstractViewElementResolverFactory {
   @Override
   protected String getPrefix() {
      return "Jsp";
   }
}

ViewElement接口可以注入ViewElementResolverFactory对象,由它来创建ViewElementResolver,由此完成视图元素的呈现,例如SelectViewElement

public class SelectViewElement implements ViewElement {
   private ViewElementResolver resolver;
   private ViewElementResolverFactory resolverFactory;
   public void setViewElementResolverFactory(ViewElementResolverFactory resolverFactory) {
      this.resolverFactory = resolverFactory;
   }
   public String Render() {
      resolverFactory.create(this.getClass().getName()).resolve(this);
   }
}

整个UI引擎模块的设计如图1-4所示。

如此设计看似保证了视图元素呈现的可扩展性,也遵循了单一职责原则,却因为抽象过度而增加了方案的复杂度。扩展式设计是为不可知的未来做投资,一旦未来的变化不符合预期,就会导致过度设计。具有实证主义态度的设计理念是面对不可预测的变化时,应首先保证方案的简单性。当变化真正发生时,可以通过诸如提炼接口(extract interface)[6]341的重构手法,满足解析逻辑的扩展。方案中工厂接口与抽象工厂类的引入,根本没有贡献任何解耦与扩展的价值,反而带来了不必要的间接逻辑,让设计变得更加复杂。到产品研发的后期,我所预期的HTML和Excel呈现的需求变化实际并没有发生。

图1-4 UI引擎模块的类图

要应对需求变化,终归需要一些设计技巧。很多时候,因为设计人员的技能不足,没有明确识别出未来确认会发生的变化,或者对需求变化发展的方向缺乏前瞻,所以导致整个设计变得过于僵化,修改的成本太高,从而走向了过度设计的另外一个极端,我将这一问题称为“设计不足”。

设计不足的方案只顾眼前,对于一定要发生的变化视而不见,这不仅导致方案缺乏可扩展性,甚至有可能出现技术实现方向的错误。这样的设计不是恰如其分的简单设计,而是对于糟糕质量视而不见的简陋处置,是为了应付进度蒙混过关用的临时花招,表面看来满足了进度要求,但在未来偿还欠下的债务时,需要付出几倍的成本。如果整个软件系统都由这样设计不足的方案构成,那么未来任何一次需求的变更或增加,都可能成为压垮系统的最后一根稻草。

我曾负责一个基于大数据的数据平台的设计与开发,该数据平台需要实时采集来自某行业各个系统各种协议的业务数据,并按照主题区的数据模型标准来治理数据。当时,我对整个行业的数据标准与规范尚不了解,对于数据平台未来的产品规划也缺乏充分认识。迫于进度压力,我选择了采用快速而简洁的硬编码方式实现从原始数据到主题区模型对象的转换,这一设计让我们能够在规定的进度周期满足同时应对多家客户治理数据的要求。

然而,作为一款数据平台产品,在该行业内进行广泛推广时,随着面向的客户越来越多,需要采集数据的上游系统也变得越来越多。此时,回首之前的方案设计,不由后悔不迭:方案的简陋导致了开发质量的低下和生产力的降低。此时的主题区划分已经趋于稳定,虽然需要支持的客户和上游系统越来越多,但要治理的数据所属的主题仍然在已有主题区范围之内,换言之,原始数据的协议是变化的,主题区的范围却相对稳定。通过对主题区模型与数据治理逻辑进行共性与可变性分析[7],我识别出了原始数据消息的共性特征,建立了抽象的消息模型,又为主题区模型抽象出一套树形结构的核心主题模型,并基于此核心模型建立新的主题区模型。在确保主题区模型不变的情况下,找到数据治理逻辑中不变的转换过程与规则,将不同上游系统遵循不同数据协议而带来的变化转移到一个定义映射关系的样式配置文件中,形成对变化的隔离,实现了一个相对稳定的数据治理方案,如图1-5所示。

图1-5 隔离变化的设计方案

采用新方案之后,如果需要采集一个不超出主题区范围的全新系统,只需定义一个样式映射文件,并付出极少量定制开发的成本,就能以最快的迭代进度满足新的数据治理需求。正因为改进了旧有方案,团队才能够在不断涌入新需求的功能压力下,基本满足产品研发的进度要求。只可惜,之前的数据治理功能已经被多家客户广泛运用到生产环境中,对应的数据交换逻辑也依托于旧的主题区模型,使得整个数据平台产品在近两年的开发周期中一直处于新旧两套主题区模型共存的尴尬局面。由于一部分数据治理和数据交换逻辑要对接两套主题区模型,因此,迫于无奈,也必须实现两套数据治理和数据交换逻辑,无谓地增加了团队的工作量。由于改造旧模型的工作量极为繁重,团队一直未能获得喘息的机会对模型以新汰旧,因此这一尴尬局面还会在一段时间内继续维持下去。这正是设计不足在应对变化时带来的负面影响。

我们无法预知未来,自然就无法预测未来可能发生的变化,这就带来了软件系统的不可预测性。软件设计者不可能对变化听之任之,却又因为它的不可预测性而无可适从。在软件系统不断演化的过程中,面对变化,我们需要尽可能地保证方案的平衡:既要避免因为设计不足使得变化对系统产生根本影响,又要防止因为满足可扩展性让方案变得格外复杂,最后背上过度设计的坏名声。故而,变化之难,难在如何在设计不足与过度设计之间取得平衡。

[1] 图片来自Jurgen Appelo的《管理3.0:培养和提升敏捷领导力》。

[2] 技术债由Ward Cunningham提出,他用债务形象地说明为遵循软件开发计划而做出推迟的技术决策,如文档、重构等。技术债是不可避免的,关键在于要通过维护一个技术债列表让技术债可见,并及时“还债”,避免更高的“利息”。

[3] Neal Ford在《演化架构与紧急设计》系列文章中提到了通过指标指导紧急设计,包括使用iPlasma生成质量评估指标。

[4] 该定律由Melvin E. Conway在1967年发表的论文“How Do Committees Invent?”中提出。Fred Brooks在《人月神话》中引用了该思想,并明确称其为康威定律。本书多个章节都提及了康威定律对团队组织结构与软件体系架构的影响。


软件的核心是其为用户解决领域相关的问题的能力。
所有其他特性,不管有多么重要,都要服务于这个基本目的。

——Eric Evans,《领域驱动设计》

应对复杂度的挑战,或许是构建软件的过程中唯一亘古不变的主题。为了更好地应对软件复杂度,许多顶尖的软件设计人员与开发人员纷纷结合实践提出自己的真知灼见,既包括编程思想、设计原则、模式语言、过程方法和管理理论,又包括对编程利器自身的打磨。毫无疑问,通过这些真知灼见,软件领域的先行者已经改变或正在改变我们构建软件的方法、过程和目标,我们欣喜地看到了软件的构建正在向着好的方向改变。然而,整个客观世界的所有现象都存在诸如黑与白、阴与阳、亮与暗的相对性,任何技术的发展都不是单向的。随着技术日新月异向前发展,软件系统的复杂度也日益增长。中国有一句古谚:“道高一尺,魔高一丈。”又有谚语:“魔高一尺,道高一丈。”究竟是道高还是魔高,就看你是站在“道”的一方,还是“魔”的一方。

在构建软件的场景中,软件复杂度显然就是“魔”,控制软件复杂度的方法则是“道”。在软件构建领域,“道”虽非虚无缥缈的玄幻叙述,却也不是绑定在具象之上的具体手段。软件复杂度的应对之道提供了一些基本法则,这些基本法则可以说放之四海而皆准,其中一条基本法则就是:能够控制软件复杂度的,只能是设计(指广泛意义上的设计)方法。因为我们无法改变客观存在的问题空间(参见2.1.2节对问题空间和解空间的阐释),却可以改变设计的质量,让好的设计为控制复杂度创造更多的机会。如果我们将软件系统限制在业务软件系统之上,又可得到另外一条基本法则:“要想克服”(业务系统的)复杂度,就需要非常严格地使用领域逻辑设计方法。[8]1在近20年的时间内,一种有效的领域逻辑设计方法就是Eric Evans提出的领域驱动设计(domain-driven design)。

Eric Evans通过他在2003年出版的经典著作《领域驱动设计》(Domain-Driven Design: Tackling Complexity in the Heart of Software)全方位地介绍了这一设计方法,该书的副标题旗帜鲜明地指出该方法为“软件核心复杂性应对之道”。

领域驱动设计究竟是怎样应对软件复杂度的?作为一种将“领域”放在核心地位的设计方法,其名称足以说明它应对复杂度的态度。用Eric Evans自己的话来说:“领域驱动设计是一种思维方式,也是一组优先任务,它旨在加速那些必须处理复杂领域的软件项目的开发。为了实现这个目标,本书给出了一套完整的设计实践、技术和原则。”[8]2

结合我们通过理解能力和预测能力两个维度对软件系统复杂度成因的剖析,确定了影响复杂度的3个要素:规模、结构与变化。控制复杂度的着力点就在这3个要素之上!领域驱动设计对软件复杂度的应对,是引入了一套提炼为模式的设计元模型,对业务软件系统做到了对规模的控制、结构的清晰化以及对变化的响应。

要深刻体会领域驱动设计是如何控制软件复杂度的,还需要整体了解Eric Evans建立的这一套完整的软件设计方法体系,包括该方法体系提出的设计概念与设计过程。

领域驱动设计作为一个针对大型复杂业务系统的领域建模方法体系(不仅限于面向对象的领域建模),它改变了传统软件开发工程师针对数据库建模的方式,通过面向领域的思维方式,将要解决的业务概念和业务规则等内容提炼为领域知识,然后借由不同的建模范式将这些领域知识抽象为能够反映真实世界的领域模型。

Eric Evans之所以提出这套方法体系,并非刻意地另辟蹊径,创造出与众不同的设计方法与模式,而是希望恢复业务系统设计核心关注点的本来面貌,也就是认识到领域建模和设计的重要性,然而在当时看来,这却是全新的知识提炼。正如他自己所云:“至少20年前[1],一些顶尖的软件设计人员就已经认识到领域建模和设计的重要性,但令人惊讶的是,这么长时间以来几乎没有人写出点儿什么,告诉大家应该做哪些工作或如何去做……本书为做出设计决策提供了一个框架,并且为讨论领域设计提供了一个技术词汇库。”[8]这里提到的“技术词汇库”就是我提到的设计元模型。

领域驱动设计元模型是以模式的形式呈现在大家眼前的,由诸多松散的模式构成,这些模式在领域驱动设计中的关系如图2-1所示。

领域驱动设计的核心是模型驱动设计,而模型驱动设计的核心又是领域模型,领域模型必须在统一语言(参见第4章)的指导下获得。为整个业务系统建立的领域模型要么属于核心子领域(参见第6章),要么属于通用子领域[2]。之所以区分子领域,一方面是为了将一个不易解决的庞大问题切割为团队可以掌控的若干小问题,达到各个击破的目的,另一方面也是为了更好地实现资产(人力资产与财力资产)的合理分配。

为了保证定义的领域模型在不同上下文表达各自的知识语境,需要引入限界上下文(参见第9章)来确定业务能力的自治边界,并考虑通过持续集成来维护模型的统一。上下文映射(参见第10章)清晰地表达了多个限界上下文之间的协作关系。根据协作方式的不同,可以将上下文映射分为如下8种模式[3]

图2-1 领域驱动设计元模型

模型驱动设计可以在限界上下文的边界内部进行,它通过分层架构(layered architecture)将领域独立出来,并在统一语言的指导下,通过与领域专家的协作获得领域模型。表示领域模型的设计要素(参见第15章)包括实体(entity)、值对象(value object)、领域服务(domain service)和领域事件(domain event)。领域逻辑都应该封装在这些对象中。这一严格的设计原则可以避免领域逻辑泄露到领域层之外,导致技术实现与领域逻辑的混淆。

聚合(aggregate)(参见第15章)是一种边界,它可以封装一到多个实体与值对象,并维持该边界范围之内的业务完整性。聚合至少包含一个实体,且只有实体才能作为聚合根(aggregate root)。工厂(factory)和资源库(repository)(参见第15章)负责管理聚合的生命周期。前者负责聚合的创建,用于封装复杂或者可能变化的创建逻辑;后者负责从存放资源的位置(数据库、内存或者其他Web资源)获取、添加、删除或者修改聚合。

哲学家常常会围绕真实世界和理念世界的映射关系探索人类生存的意义,即所谓“两个世界”的哲学思考。软件世界也可一分为二,分为构成描述需求问题的真实世界与获取解决方案的理念世界。整个软件构建的过程,就是从真实世界映射到理念世界的过程。

如果真实世界是复杂的,在映射为理念世界的过程中,就会不断受到复杂度的干扰。根据Allen Newell和Herbert Simon的问题空间理论:“人类是通过在问题空间(problem space)中寻找解决方案来解决问题的”[9],构建软件(世界)也就是从真实世界中的问题空间寻找解决方案,将其映射为理念世界的解空间(solution space)来满足问题空间的需求。因此,软件系统的构建实则是对问题空间的求解,以获得构成解空间的设计方案,如图2-2所示。

图2-2 从问题空间到解空间

为什么要在软件构建过程中引入问题空间和解空间?

实际上,随着IT技术的发展,软件系统正是在这两个方向不断发展和变化的。在问题空间,我们要解决的问题越来越棘手,空间规模越来越大,因为随着软件技术的发展,许多原本由人来处理的线下流程慢慢被自动化操作所替代,人机交互的方式发生了翻天覆地的变化,IT化的范围变得更加宽广,涉及的领域也越来越多。问题空间的难度与规模直接决定了软件系统的复杂度。

针对软件系统提出的问题,解决方案的推陈出新自然毋庸讳言,无论是技术、工具,还是设计思想与模式,都有了很大变化。解决方案不是从石头里蹦出来的,而必然是为了解决问题而生的。面对错综复杂的问题,解决方案自然也需要灵活变化。软件开发技术的发展是伴随着复用性和扩展性发展的。倘若问题存在相似性,解决方案就有复用的可能。通过抽象寻找到不同问题的共性时,相同的解决方案也可以运用到不同的问题中。同时,解决方案还需要响应问题的变化,能在变化发生时以最小的修改成本满足需求,同时保障解决方案的新鲜度。无疑,构成解空间的解决方案不仅要解决问题,还要控制软件系统的复杂度。

问题空间需要解空间来应对,解空间自然也不可脱离问题空间而单独存在。对于客户提出的需求,要分清楚什么是问题,什么是解决方案,真正的需求才可能浮现出来。在看清了问题的真相之后,我们才能有据可依地寻找真正能解决问题的解决方案。软件构建过程中的需求分析,实际就是对问题空间的定位与探索。如果在问题空间还是一团迷雾的时候就贸然开始设计,带来的灾难性结果是可想而知的。徐锋认为,“要做好软件需求工作,业务驱动需求思想是核心。传统的需求分析是站在技术视角展开的,关注的是‘方案级需求’;而业务驱动的需求思想则是站在用户视角展开的,关注的是‘问题级需求’。”[10]2

怎么区分方案级需求和问题级需求?方案级需求就好比一个病人到医院看病,不管病情就直接让医生开阿司匹林,而问题级需求则是向医生描述自己身体的症状。病情是医生要解决的问题,处方是医生提供的解决方案。

那种站在技术视角展开的需求分析,实际就是没有明确问题空间与解空间的界限。在针对问题空间求解时,必须映射于问题空间定义的问题,如此才能遵循恰如其分的设计原则,在问题空间的上下文约束下寻找合理的解决方案。

领域驱动设计为问题空间与解空间提供了不同的设计元模型。对于问题空间,强调运用统一语言来描述需求问题,利用核心子领域通用子领域支撑子领域来分解问题空间,如此就可以“揭示什么是重要的以及在何处付出努力”[11]9。除去统一语言与子领域,其余设计元模型都将运用于解空间,指导解决方案围绕着“领域”这一核心开展业务系统的战略设计与战术设计。

对于一个复杂度高的业务系统,过于辽阔的问题空间使得我们无法在深入细节的同时把握系统的全景。既然软件构建的过程就是对问题空间求解的过程,那么面对太多太大的问题,就无法奢求一步求解,需要根据问题的层次进行分解。不同层次的求解目标并不相同:为了把握系统的全景,就需要从宏观层次分析和探索问题空间,获得对等于软件架构的战略设计原则;为了深入业务的细节,则需要从微观层次开展建模活动,并在战略设计原则的指导下做出战术设计决策。这就是领域驱动设计的两个阶段:战略设计阶段和战术设计阶段。

战略设计阶段要从以下两个方面来考量。

子领域的边界明确了问题空间中领域的优先级,限界上下文的边界则确保了领域建模的最大自由度。这也是战略设计在分治上起到的效用。当我们在战略层次从问题空间映射到解空间时,子领域也将映射到限界上下文,即可根据子领域的类型为限界上下文选择不同的建模方式。例如为处于核心子领域的限界上下文选择领域模型(domain model)模式[12]116,为处于支撑子领域(supporting subdomain)的限界上下文选择事务脚本(transaction script)模式[12]110,这样就可以灵活地平衡开发成本与开发质量。

战术设计阶段需要在限界上下文内部开展领域建模,前提是你为限界上下文选择了领域模型模式。在限界上下文内部,需要通过分层架构将领域独立出来,在排除技术实现的干扰下,通过与领域专家的协作在统一语言的指导下逐步获得领域模型。

战术设计阶段最重要的设计元模型是聚合模式。虽然聚合是实体和值对象的概念边界,然而在获得了清晰表达领域知识的领域模型后,我们可以将聚合视为表达领域逻辑的最小设计单元。如果领域行为是无状态的,或者需要多个聚合的协作,又或者需要访问外部资源,则应该将它分配给领域服务。至于领域事件,则主要用于表达领域对象状态的迁移,也可以通过事件来实现聚合乃至限界上下文之间的状态通知。

战略设计与战术设计并非割裂的两个阶段,而是模型驱动设计过程在不同阶段展现出来的不同视图。战略设计指导着战术设计,这就等同于设计原则指导着设计决策。Eric Evans就明确指出,“战略设计原则必须把模型的重点放在捕获系统的概念核心,也就是系统的‘远景’上。”[8]231当一个业务系统的规模变得越来越庞大时,战略设计高屋建瓴地通过限界上下文规划了整个系统的架构。只要维护好限界上下文的边界,管理好限界上下文之间的协作关系,限制在该边界内开展的战术设计所要面对的就是一个复杂度得到大幅降低的小型业务系统。

人们常以“只见树木,不见森林”来形容一个人不具备高瞻远瞩的战略眼光,然而,若是“只见森林,不见树木”,也未见得是一个褒扬的好词语,它往往可以形容一个人好高骛远,不愿意脚踏实地将战略方案彻底落地。无论战略的规划多么完美,到了战术设计的实际执行阶段,团队在开展对领域的深层次理解时,总会发现之前被遗漏的领域概念,并经过不断的沟通与协作,“碰撞”出对领域的新的理解。对领域概念的新发现与完善除了能帮助我们将领域模型突破到深层模型,还可能促进我们提出对战略设计的修改与调整,其中就包括对限界上下文边界的调整,从而使战略设计与战术设计保持统一。

从战略设计到战术设计是一个自顶向下的设计过程,体现为设计原则对设计决策的指导;将战术设计方案反馈给战略设计,则是自底向上的演化过程,体现为对领域概念的重构引起对战略架构的重构。二者形成不断演化、螺旋上升的设计循环。

领域驱动设计是一种思维方式[8]2,而模型驱动设计则是领域驱动设计的一种设计元模型。因此,模型驱动设计必须在领域驱动设计思维方式的指导下进行,那就是面向领域的模型驱动设计,或者更加准确地将其描述为领域模型驱动设计

领域模型驱动设计通过单一的领域模型同时满足分析建模、设计建模和实现建模的需要,从而将分析、设计和编码实现糅合在一个整体阶段中,避免彼此的分离造成知识传递带来的知识流失和偏差。它树立了一种关键意识,就是开发团队在针对领域逻辑进行分析、设计和编码实现时,都在进行领域建模,产生的输出无论是文档、设计图还是代码,都是组成领域模型的一部分。Eric Evans将那些参与模型驱动设计过程并进行领域建模的人员称为“亲身实践的建模者”(hands-on modeler)[8]40

模型驱动设计主要在战术阶段进行,换言之,整个领域建模的工作是在限界上下文的边界约束下进行的,统一语言的知识语境会对领域模型产生影响,至少,建模人员不用考虑在整个系统范围下领域概念是否存在冲突,是否带来歧义。由于限界上下文拥有自己的内部架构,一旦领域模型牵涉到跨限界上下文之间的协作,就需要遵循限界上下文与上下文映射的架构约束了。

既然模型驱动设计是面向领域的,就必须明确以下两个关键原则。

模型驱动设计不能一蹴而就。毕竟,即使通过限界上下文降低了业务复杂度,对领域知识的理解是一个渐进的过程。在这个过程中,开发团队需要和领域专家紧密协作,共同研究领域知识。在获得领域模型之后,也要及时验证,确认领域模型有没有真实表达领域知识。一旦发现遗漏或失真的现象,就需要重构领域模型。首先建立领域模型,然后重构领域模型,进而精炼领域模型,保证领域概念被直观而真实地表达为简单清晰的领域模型。显然,在战术设计阶段,模型驱动设计也应该是一个演进的不断完善的螺旋上升的循环过程。

领域驱动设计过程是一条若隐若现的由许多点构成的设计轨迹,这些点就是领域驱动设计的设计元模型。如果我们从问题空间到解空间,从战略设计到战术设计寻找到对应的设计元模型,分别“点亮”它们,那么这条设计轨迹就会如图2-3那样格外清晰地呈现在我们眼前。

领域驱动设计的过程几乎贯穿了整个软件构建的生命周期,包括对业务需求的探索和分析,系统的架构和设计,以及编码实现、测试和重构。面对客户的业务需求,由领域专家与开发团队展开充分的交流,经过需求分析与知识提炼,获得清晰明确的问题空间,并从问题空间的业务需求中提炼出统一语言,然后利用子领域分解问题空间,根据价值高低确定核心子领域通用子领域支撑子领域

通过对问题空间开展战略层次的求解,获得限界上下文形成解空间的主要支撑元素。识别限界上下文的基础来自问题空间的业务需求,遵循“高内聚松耦合”的原则划分领域知识的边界,再通过上下文映射管理它们之间的关系。每个限界上下文都是一个相对独立的“自治王国”,可以根据限界上下文是否属于核心子领域来选择内部的架构。通常,需要通过分层架构将限界上下文内部的领域隔离出来,进入战术设计阶段,进行面向领域的模型驱动设计。

图2-3 领域驱动设计过程

选定一个限界上下文,在统一语言的指导下,针对该上下文内部的领域知识开展领域模型驱动设计。首先进行领域分析,提炼领域知识建立满足统一语言要求的领域分析模型,然后引入实体值对象领域服务领域事件聚合资源库工厂等设计要素开始程序设计,获得设计模型后在它的指导下进行编码实现,输出最终的领域模型。

在领域驱动设计过程中,战略设计控制和分解了战术设计的边界与粒度,战术设计则以实证角度验证领域模型的有效性、完整性和一致性,进而以迭代的方式分别完成对限界上下文与领域模型的更新与演化,各自形成设计过程的闭环。两个不同阶段的设计目标保持一致,形成一个连贯的过程,彼此之间相互指导与规范,最终保证战略架构与领域模型的同时演进。

回到对软件复杂度的本质分析。问题空间的规模与结构制造了理解能力障碍,问题空间的变化制造了预测能力障碍,从而形成了问题空间的复杂度。问题空间的复杂度决定了“求解”的难度,领域驱动设计对软件复杂度的控制之道就是竭力改变设计的质量,也就是在解空间中引入设计元模型,对问题空间的复杂度进行有效的控制。

问题空间的规模客观存在,除了在软件构建过程中通过降低客户的期望,明确目标系统的范围能够有效地限制规模,要在问题空间控制规模,我们手握的筹码确实不多,然而到了解空间,开发团队就能掌握主动权了。虽然不能改变系统的规模,却可以通过“分而治之”的方法将一个规模庞大的系统持续分解为小的软件元素,直到每个细粒度(视问题空间的问题粒度而定)的软件元素能够解决问题空间的一个问题为止。当然,这种分解并非不分原则地拆分,在分解的同时还必须保证被分解的部分能够被合并为一个整体。分而治之的过程首先是自顶向下持续分解的过程,然后又是自底向上进行整合的过程。

分而治之是一个好方法,可是,该采用什么样的设计原则、以什么样的粒度对软件系统进行分解,又该如何将分解的软件元素组合起来形成一个整体,却让人倍感棘手。领域驱动设计提出了两个重要的设计元模型:限界上下文上下文映射,它们是控制系统规模最为有效的手段,也是领域驱动设计战略设计阶段的核心模式。

下面让我们通过一个案例认识到如何通过限界上下文控制系统的规模。

国际报税系统是为跨国公司的驻外雇员提供的、方便一体化的税收信息填报平台。税务专员通过该平台收集雇员提交的报税信息,然后对这些信息进行税务评审。如果税务专员评审出信息有问题,则将其返回给雇员重新修改和填报。一旦信息确认无误,则进行税收分析和计算,并生成最终的税务报告提交给当地政府以及雇员本人。

系统主要涉及的功能包括:

主要涉及的用户角色包括:

采用领域驱动设计,我们将架构的主要关注点放在了“领域”,在与客户进行充分的需求沟通和交流后,通过分析已有系统的问题空间,结合客户提出的新需求,在解空间利用限界上下文对系统进行分解,获得如下限界上下文。

整个系统的解空间分解为多个限界上下文,每个限界上下文提供了自身领域独立的业务能力,获得了图2-4所示的系统架构。

图2-4 引入限界上下文的国际报税系统架构

每个限界上下文都是一个独立的自治单元。根据限界上下文的边界划分团队,建立单独的代码库。团队只为所属限界上下文负责:除了需要了解限界上下文之间的协作接口,以确定上下文映射的模式,团队只需要了解边界内的领域知识,为其建立各自的领域模型。系统复杂度通过限界上下文的分解得到了明显的控制。

保持系统结构的清晰是控制结构复杂度的不二法门。关键在于,要以正确的方式认清系统内部的边界。限界上下文从业务能力的角度形成了一条清晰的边界,它与业务模块不同,在内部也拥有独立的架构(参见第9章),通过分层架构将领域分离出来,在业务逻辑与技术实现之间划定一条清晰的边界。

为何要在业务逻辑与技术实现之间划分边界呢?实际上仍然可以从软件复杂度的角度给出理由。

问题空间由真实世界的客户需求组成,需求可以简单分为业务需求质量需求

业务需求的数量决定了系统的规模,这是业务需求对软件复杂度带来的直观影响。以电商系统的促销规则为例。针对不同类型的顾客与产品,商家会提供不同的促销力度。促销的形式多种多样,包括赠送积分、红包、优惠券、礼品;促销的周期需要支持定制,既可以是特定的日期(例如“双十一”促销),也可以是节假日的固定促销模式。显然,促销需求带来了促销规则的复杂度,包括支持多种促销类型,根据促销规则进行的复杂计算。这些业务需求并非独立的,它们还会互相依赖、互相影响,例如在处理促销规则时,还需要处理好它与商品、顾客、卖家与支付乃至于物流、仓储之间的关系。这对整个系统的结构提出了更高的要求。如果不能维持清晰的结构,就可能因为业务需求的不断变化带来业务逻辑的多次修改,再加上沟通不畅、客户需求不清晰等多种局外因素,整个系统的业务逻辑代码会变得纠缠不清,系统慢慢腐烂,变得不可维护,最终形成一种Brian Foote和Joseph Yoder所说的“大泥球”系统。

我们可以将业务需求带来的复杂度称为“业务复杂度”(business complexity)。

软件系统的质量需求就是我们为系统定义的质量属性,包括安全、高性能、高并发、高可用性等,它们往往给软件的技术实现带来挑战。假设有两个经营业务完全一样的电商网站,但其中一个电商网站的并发访问量是另一个电商网站的一百倍。此时,针对下订单服务,要达到相同的服务水平,就不再是通过编写更好的业务代码所能解决的了。质量属性对技术实现的挑战还体现在它们彼此之间的影响,如系统安全性要求对访问进行控制,无论是增加防火墙,还是对传递的消息进行加密,又或者对访问请求进行认证和授权,都需要为整个系统架构添加额外的间接层。这会不可避免地对访问的低延迟产生影响,拖慢系统的整体性能。又比如为了满足系统的高并发访问,需要对业务服务进行物理分解,通过横向增加更多的机器来分散访问负载;同时,还可以将一个同步的访问请求拆分为多级步骤的异步请求,引入消息中间件对这些请求进行整合和分散处理。这种分离一方面增加了系统架构的复杂度,另一方面也因为引入了更多的资源,使得系统的高可用面临挑战,且增加了维护数据一致性的难度。

我们可以将质量需求带来的复杂度称为“技术复杂度”(technology complexity)。

技术复杂度与业务复杂度并非完全独立的,二者的共同作用会让系统的复杂度变得不可预期、难以掌控。同时,技术的变化维度与业务的变化维度并不相同,产生变化的原因也不一致。倘若未能很好地界定二者之间的关系,确定两种复杂度之间的清晰边界,一旦各自的复杂度增加,团队规模也将随之扩大,再糅以严峻的交付周期、人员流动等诸多因素,就好似将各种不稳定的易燃易爆气体混合在一个密闭容器中,随时都可能产生复杂度的组合爆炸,如图2-5所示。

图2-5 业务复杂度与技术复杂度

要避免业务逻辑的复杂度与技术实现的复杂度混杂在一起,就需要确定业务逻辑与技术实现的边界,从而隔离各自的复杂度。这种隔离也符合关注点分离的设计原则。例如,在电商的领域逻辑中,订单业务关注的业务规则包括验证订单有效性,计算订单总额,提交和审批订单的流程等;技术关注点则从实现层面保障这些业务能够正确地完成,包括确保分布式系统之间的数据一致性,确保服务之间通信的正确性等。业务逻辑不需要关心技术如何实现。无论采用何种技术,只要业务需求不变,业务规则就不会变化。换言之,理想状态下,我们应该保证业务规则与技术实现是正交的。

领域驱动设计引入的分层架构规定了严格的分层定义,将业务逻辑封装到领域层(domain layer),支撑业务逻辑的技术实现放到基础设施层(infrastructure layer)。在领域层之上的应用层(application layer)则扮演了双重角色:一方面,作为业务逻辑的外观(facade),它暴露了能够体现业务用例的应用服务接口;另一方面,它又是业务逻辑与技术实现之间的黏合剂,实现了二者之间的协作。

图2-6展示了一个典型的领域驱动设计分层架构。领域层的内容与业务逻辑有关,基础设施层的内容与技术实现有关,二者泾渭分明,然后汇合在作为业务外观的应用层。应用层确定了业务逻辑与技术实现的边界,通过依赖注入(dependency injection)的方式将二者结合起来。

图2-6 领域驱动设计分层架构

抽象的资源库接口隔离了业务逻辑与技术实现。资源库接口属于领域层,资源库实现则放在基础设施层,通过依赖注入[4]可以在运行时为业务逻辑注入具体的资源库实现。无论资源库的实现怎么调整,领域层的代码都不会受到牵连。例如:领域层的领域服务OrderService通过OrderRepository资源库添加订单,OrderService并不会知道OrderRepository的具体实现:

package com.dddexplained.ecommerce.ordercontext.domain;

@Service
public class OrderService {
   @Autowired
   private OrderRepository orderRepository;

   public void execute(Order order) {
      if (!order.isValid()) {
throw new InvalidOrderException(String.format("the order which placed by buyer with %s 
is invalid.", buyerId));
      } 
      orderRepository.add(order);
   }
}

@Repository
public interface OrderRepository {
   void add(Order order);
}

领域驱动设计通过限界上下文隔离了业务能力的边界,通过分层架构隔离了业务逻辑与技术实现,如此就能保证整个业务系统的架构具有清晰的结构,实现了有序设计,可以避免不同关注点的代码混杂在一处,形成可怕的“大泥球”。

未来的变化是无法控制的,我们只能以积极的态度拥抱变化。变被动为主动的方式就是事先洞察变化的规律,识别变化方向,把握业务逻辑的本质,使得整个系统的核心领域逻辑能够更好地响应需求的变化。

领域驱动设计通过模型驱动设计针对限界上下文进行领域建模,形成了结合分析、设计和实现于一体的领域模型。领域模型是对业务需求的一种抽象,表达了领域概念、领域规则以及领域概念之间的关系。一个好的领域模型是对统一语言的可视化表示,可以减少需求沟通可能出现的歧义。通过提炼领域知识,并运用抽象的领域模型去表达,就可以达到对领域逻辑的化繁为简。模型是封装,实现了对业务细节的隐藏;模型是抽象,提取了领域知识的共同特征,保留了面对变化时能够良好扩展的可能性。

领域建模的一个难点是如何将看似分散的事物抽象成一个统一的领域模型。例如,我们要开发的项目管理系统需要支持多种软件项目管理流程,如瀑布、统一过程、极限编程或者Scrum,这些项目管理流程迥然不同,如果需要我们为各自提供不同的解决方案,就会使系统的模型变得非常复杂,也可能引入许多不必要的重复。通过领域建模,我们可以对项目管理领域的知识进行抽象,寻找具有共同特征的领域概念。这就需要分析各种项目管理流程的主要特征与表现,以从中提炼出领域模型。

瀑布式软件开发由需求、分析、设计、编码、测试、验收6个阶段构成,每个阶段都由不同的活动构成,这些活动可能是设计或开发任务,也可能是召开评审会。

统一过程(rational unified process,RUP)清晰地划分了4个阶段:先启阶段、细化阶段、构造阶段和交付阶段。每个阶段可以包含一到多个迭代,每个迭代有不同的工作,例如业务建模、分析设计、配置和变更管理等。

极限编程(eXtreme programming,XP)作为一种敏捷方法,采用了迭代的增量式开发,提倡为客户交付具有业务价值的可运行软件。在执行交付计划之前,极限编程要求团队对系统的架构做一次预研(architectural spike,又被译为架构穿刺)。当架构的初始方案确定后,就可以进入每次小版本的交付。每个小版本交付又被划分为多个周期相同的迭代。在迭代过程中,要求执行一些必需的活动,如编写用户故事、故事点估算、验收测试等。

Scrum同样是迭代的增量开发过程。项目在开始之初,需要在准备阶段确定系统愿景、梳理业务用例、确定产品待办项(product backlog)、制订发布计划以及组建团队。一旦确定了产品待办项以及发布计划,就进入冲刺(sprint)迭代阶段。sprint迭代过程是一个固定时长的项目过程,在这个过程中,整个团队需要召开计划会议、每日站会、评审会议和回顾会议。

显然,不同的项目管理流程具有不同的业务概念,例如瀑布式开发分为6个阶段,却没有发布和迭代的概念;RUP没有发布的概念;Scrum为迭代引入了冲刺的概念。不同的项目管理流程具有不同的业务规则,例如RUP的4个阶段可以包含多个迭代周期,每个迭代周期都需要完成对应的工作,只是不同工作在不同阶段所占的比重不同;XP需要在进入发布阶段之前进行架构预研,而在每次小版本发布之前,都需要进行验收测试和客户验收;Scrum的冲刺是一个基本固定的流程,每个迭代召开的“四会”(计划会议、评审会议、回顾会议和每日站会)都有明确的目标。

领域建模就是要从这些纷繁复杂的领域逻辑中寻找到能够表示项目管理领域的概念,对概念进行抽象,确定它们之间的关系。经过分析这些项目管理流程,我们发现它们的业务概念和规则上虽有不同之处,但都归属于软件开发领域,因此必然具备一些共同特征。

从项目管理系统的角度看,无论针对何种项目管理流程,我们的主题需求是不变的,就是要为这些管理流程制订软件开发计划(plan)。不同之处在于,计划可以由多个阶段(phase)组成,也可以由多个发布(release)组成。一些项目管理流程没有发布的概念,我们也可以认为是一个发布。那么,到底是一个发布包含多个阶段,还是一个阶段包含多个发布呢?我们发现,在XP中明显地划分了两个阶段:架构预研阶段与发布计划阶段,而发布只属于发布计划阶段。因而从概念内涵上,可以认为是阶段(phase)包含了发布(release),每个发布又包含了一到多个迭代(iteration)。至于Scrum的sprint概念,其实可以看作迭代的一种特例。每个迭代可以开展多种不同的活动(activity),这些活动可以是整个团队参与的会议,也可以是部分成员或特定角色执行的实践。对计划而言,我们还需要跟踪任务(task)。与活动不同,任务具有明确的计划起止时间、实际起止时间、工作量、优先级和承担人。

于是可提炼出图2-7所示的统一领域模型。

图2-7 项目管理系统的统一领域模型

为了让项目管理者更加方便地制订项目计划,产品经理提出了计划模板功能。当管理者选择对应的项目管理生命周期类型后,系统会自动创建满足其规则的初始计划。基于增加的这一新需求,我们更新了之前的领域模型,如图2-8所示。

在增加的领域模型中,生命周期规格(life cycle specification)是一个隐含的概念,遵循领域驱动设计提出的规格(specification)模式[8]154,封装了项目开发生命周期的约束规则。

领域模型以可视化的方式清晰地表达了业务含义。我们可以利用这个模型指导后面的程序设计与编码实现:当需求发生变化时,能够敏锐地捕捉到现有模型的不匹配之处,并对其进行更新,使得我们的设计与实现能够以较小的成本响应需求的变化。

图2-8 领域模型对变化的应对

控制软件复杂度是构建软件过程中永恒的旋律,必须明确:软件复杂度可以控制,但不可消除。领域驱动设计控制软件复杂度的中心主要在于“领域”,Eric Evans就认为:“很多应用程序最主要的复杂度并不在技术上,而是来自领域本身、用户的活动或业务。”[8]2这当然并不全面,随着软件的“触角”已经蔓延到人类生活的方方面面,在业务复杂度变得越来越高的同时,技术复杂度也在不断地向技术极限发起挑战,其制造的技术障碍完全不亚于业务层面带来的困难。领域驱动设计并非“银弹”,它的适用范围主要是大规模的、具有复杂业务的中大型软件系统,至于对技术复杂度的应对,它的选择是“隔离”,然后交给专门的技术团队设计合理的解决方案。

领域驱动设计控制软件复杂度的方法当然不仅限于本章给出的阐释和说明,它的设计元模型在软件构建的多个方面都在发挥着作用,其目的自然也是改进设计质量以应对软件复杂度——这是领域驱动设计的立身之本!如果你要构建的软件系统没有什么业务复杂度,领域驱动设计就发挥不了它的价值;如果构建软件的团队对于软件复杂度的控制漠不关心,只顾着追赶进度而采取“头痛医头,脚痛医脚”的态度,领域驱动设计这套方法可能也入不了他们的法眼。即便认识到了领域驱动设计的价值,怎么用好它也是一个天大的难题。我尝试破解落地难题的方法,就是重新梳理领域驱动设计的知识体系,尝试建立一个固化的、具有参考价值的领域驱动设计统一过程。

[1] 指的是《领域驱动设计》一书出版时(2003年)的20年前,也就是20世纪80年代。

[2] Eric Evans提出了核心领域与通用子领域,Vaughn Vernon在《实现领域驱动设计》一书中补充了支撑子领域。为了统一,我将“核心领域”称为“核心子领域”。

[3] Vaughn Vernon在《实现领域驱动设计》一书中补充了合作伙伴模式。

[4] 由Martin Fowler提出,以利于有效解耦,参见文章Inversion of Control Containers and the Dependency Injection pattern


相关图书

程序员的README
程序员的README
有限元基础与COMSOL案例分析
有限元基础与COMSOL案例分析
现代控制系统(第14版)
现代控制系统(第14版)
现代软件工程:如何高效构建软件
现代软件工程:如何高效构建软件
GitLab CI/CD 从入门到实战
GitLab CI/CD 从入门到实战
科学知识图谱:工具、方法与应用
科学知识图谱:工具、方法与应用

相关文章

相关课程