软件开发的本质
商业ToB软件的交付过程需要涵盖市场、工程、技术方面。大致过程是:从市场上收集需求(客户需求、用户需求、工程需求等)提炼出产品需求,选型合适技术,考虑成本做工程排期,上线产品,售卖、最后维护支持。因此决定软件成功的变量很多。短期内工程技术的影响力不大,长期看却是决定性的。关键在于能够把这个循环良性运转起来。而整个过程的关键衔接点是业务逻辑。业务逻辑清晰了技术才能发挥作用。另一方面ToB业务逻辑本身是复杂的。
DDD是什么
Domain Driven Design(DDD)是一种应对软件复杂的设计方法论,提倡聚焦业务领域而非技术,合理规划业务和技术的依赖,从而达到一种稳定的领域模型应对多变的业务需求。对技术开发来说往往关心技术选型、工程实现细节,然而学习和理解DDD要求从软件开发的市场、技术、工程三个方面通盘考虑。先有需求的合理性、领域模型的合理性,才是技术方面的考虑。
- 【需求元素】基于需求提炼领域知识、领域建模、聚焦业务
- 【架构元素】用整洁分层的技术架构指导开发、解耦业务和技术
- 【工程元素】强调面向对象设计规范,坚持四重边界
DDD之需求
什么是业务?
- 现实世界里的业务是一个流程,一个用户和产品提供方的价值交换过程,这个流程会牵扯进很多的关联方,有人有标的物,有不同角色。加上时间、场景的维度,整个过程发生前和发生后需要记录必要的交换数据。ToB业务的生态性使得这个交换过程更加漫长和复杂。这个过程里的关联方联系紧密,关系错综复杂。
- 产品经理讲的业务往往是一个个流程,加上一个个场景、用例。这个比现实世界多了一点抽象,但还是比较接近现实,有角色、动作、数据反馈。侧重交互性、事件动作的触发和结果。
- 开发讲的业务是一个个进一步抽象过的业务对象、系统对象,以及对象里的属性、行为方法。
总的来说,从上而下,业务从原始需求、提炼过的需求、开发设计的需求一层层抽象层次变高,关联关系更精简,复用性更大。软件的交付质量更多的依赖于下层设计实现层面的质量。
业务是怎么变复杂的?
上图来自《架构即未来》这本书的可扩展模型,这个图从三个方面概括了软件复杂的方向。
x轴方向:业务功能一定的前提下,用户请求量变大,可以通过复制服务实例,即多部署几份一样的实例,每份实例分摊处理用户请求。
z轴方向:业务增长除了带来大的请求流量,也会产生大量数据,从而仅仅靠x轴的水平复制方式的扩展会达到瓶颈。这个时候需要z轴扩展,把数据合理地分散在不同地方存储,这样每次处理请求的时候只需要关心一小部分数据。
y轴方向:业务扩展很多时候不仅仅靠流量的变大,更多的是需要满足更多的业务流程、业务场景,做业务创新,发挥业务多元化协同效应。这就需要做规模大的功能,做新业务开发,如何快速响应市场做快速开发交付,y轴的扩展方案没有像x轴和z轴的通用方案,没有拿来就用的有效方案。但实践证明DDD能更好的应对复杂业务。
DDD怎么解决这种复杂?
上述三个软件变复杂的方向中,y轴方向是难点,也是DDD擅长的地方。y轴扩展达到瓶颈往往都在于领域模型抽象的不合理,引入一个新需求需要改动很多老的领域逻辑,特别依赖原先做这块逻辑的人的经验,领域知识没有很好的提炼和传承。
\1. 分而治之:DDD通过规划四重边界,把领域知识做了合理的固化和分层。业务有核心域和支持域,业务域里拆分成多个限界上下文(BC),一个BC里又根据领域知识核心与否做分层,领域层里按照业务的强相关性划为聚合。
- 【第一重边界】确定项目的愿景与目标,确定问题空间,确定核心子领域、通用子领域与支撑子领域
- 【第二重边界】解决方案空间里的限界上下文就是一道进程隔离层面的物理边界
- 【第三重边界】每个限界上下文内,分层架构划分出来的接口层、领域层、应用层、基础设施层之间的隔离
- 【第四重边界】领域层里为了保持领域的完整性和一致性,引入聚合的设计作为隔离领域模型的最小单元
\2. 领域建模:对领域知识抽象、建模,让拥有数据的领域对象也拥有相应的行为能力。通过底层有序的建模可以灵活满足上层不同的业务流程和场景的编排。
- 如何提炼领域知识是建模的前提,详细过程可见DDD的实施流程的模型分析阶段部分。
- 乐高:模型就像有凹凸面的乐高积木,业务流程、场景就像是乐高积木搭出来的各种形状。以有限数量的领域模型满足海量的业务。
- SAP的软件都有这么一个哲理:先讲清楚每个概念/事物是什么,再建立事物/概念之间的联系,再组合出更多的流程、再系统间集成出通用解决方案。
- 百度文化之简单可依赖:每个个体先具备完备的某项能力,某些技术厉害。配合有担当有合作的意愿,再和其他人组成一个小团队具备某个模块的能力,再和更多团队搭配出某项完整业务的能力,再到整个公司。关键的前提在于每个个体完备的能力。
\3. 通用语言:整个团队用通用的业务语言协同,问题和目标容易对齐,跨团队沟通高效。
- 通用语言的适应范围一般是某个BC内的。不同BC针对同一个概念可以有不同的语言。举例:同一个在不同的上下文里,身份和含义会变。一个人是一个国家的公民,是一个公司的职员,一个家庭的成员等。合作伙伴在销售域是客户,在采购域是供应商,在通用平台是第三方等。
- 千万不要低估通用语言的威力。一个无歧义的名词定义,可以贯通从用户沟通,需求文档描述、设计文档编写到代码实现的整个流程。是团队资产的一部分。
DDD之技术架构
软件架构的演变
软件架构的演变也是围绕分而治之解决复杂问题,抽象复用基础设施的思路。靠近上层业务的部分做竖向垂直拆分,靠近底层技术的部分做横向整合复用。目的都是一个:提高应用开发生产力,聚焦各自领域核心。
社会的精细化分工以提高生产力在软件的发展历史也体现的很充分,分离关注点一直是架构分层的思想,DDD提倡分离技术和业务,聚焦业务领域。
- 【软件和硬件分离】早期的软件都是硬件提供商开发,预装在硬件里。后来硬件和软件的分离,有很多专门做软件的公司出现
- 【开发和实施分离】早期的企业软件公司负责开发到实施整个过程。后来开发和实施分离,开发的公司专注开发,实施的公司专注实施
- 【应用和架构分离】大型软件出现分布式架构,底层框架和中间件的出现降低了应用开发的难点
- 【应用软件分层】DDD分层架构把应用代码分为业务和技术层次
- 【软件设计和运行环境分离】云计算把软件运行环境的硬件和操作系统分离,Iaas、Paas、SaaS各层协同合作,云原生变的普及。应用软件的设计态少关心技术、运行态不需要关心技术框架细节。
- 【应用开发和架构彻底分离】service mesh、serverless的背后是想做到设计态的应用软件就跟技术框架无关,让应用开发的专注力最大可能的集中在领域逻辑里
DDD的架构范畴
不同的技术问题归属于不同的架构范畴,DDD的技术架构主要针对应用架构,但作为实施DDD的开发和应用架构师也需要关注业务架构、分布式服务架构和数据架构等。
DDD的架构演变
分层架构
最早出现的DDD相关的架构,由Eric Evans 2003年提出,也是大家俗称的分层架构。
特点:第一次提出把代码按照DDD的概念做了分层归属,特别是应用服务层和领域层是DDD特有的概念。
缺点:各层次的依赖不够合理,都是根据请求调用的方向自上而下产生依赖。比如领域层居然依赖了infrastructure层,这会导致领域层没有完全跟基础设施隔离,领域的变化因素多了技术选型这个方面。
针对分层架构的依赖不够合理这一缺点,后来演变了好几个架构,2005年Alistair Cockburn提出的端口&适配器架构,又称六边形架构。2008年Jeffrey Palermo提出的洋葱架构,2012年Rob Martin提出的整洁架构。下面对这三个架构展开描述。
六边形架构
Alistair Cockburn一开始提出的端口适配器架构,并没有融入太多DDD特色的应用服务层和领域层。它只是笼统的分为内层的业务逻辑层和外层的输入输出相关的适配层。其中适配层又可细分为主动适配层和被动适配层。
主动适配:指来自于UI、命令行等输入型命令,controller就是一种端口,端口的具体实现就是应用逻辑自身。因此端口和具体实现都在应用系统的内部。
被动适配:指访问存储设备,外部服务等。每种访问就是一种端口,具体实现是各个具体的中间件。因此端口在整个应用系统的里部,具体实现在系统的外部。
每一种输入和输出都是一个端口,每个端口都有具体的实现逻辑,因此整个应用系统的架构就是一些列的端口+适配逻辑组成,架构图就是一个多边形形状。有几个端口需要根据应用系统的具体情况而定,只是六个端口比较形象而得名为六边形架构。
特点:1. 外层依赖内层使得依赖更合理。端口就是接口,依赖接口编程。借此保证了应用和实现细节之间的隔离。2. 可测试更好
洋葱架构
洋葱架构针对六边形架构更进一步把内层的业务逻辑分为了DDD概念的应用服务层、领域服务层和领域模型层。
特点:
- 围绕独立的领域模型构建应用
- 内层定义接口,外层实现接口
- 依赖的方向指向圆心(注意:洋葱架构提倡不破坏耦合方向的依赖都是合理的,外层可以依赖直接内层,也可以依赖更里面的层)
- 所有的应用代码可以独立于基础设施编译和运行
整洁架构
整洁架构跟洋葱架构基本一样,只是层次上的叫法没有完全遵循DDD概念。Entities对应领域层,User Cases:应用层,这层需要串联上游和下游的资源完成具体场景的问题。Controllers、Gateways、Presenters:本质上是各类适配器,即用户接口层+基础设施层。
特点:依赖方向是从外层圆向内层圆。任何内层圆里的代码不应该应用外层圆的代码。越里层越解决领域逻辑。
CQRS
CQRS一般结合event sourcing一起使用,是一种读写分离架构,命令类型负责产生和修改数据,查询类型负责读取数据。适合读多写少场景下提高读性能。读写服务物理分离,数据改变通过消息中间件传递消息,达到同步意图。
【评价】:读写分离场景不建议用CQRS。主从延迟会带来的数据的实时性和一致性问题。违背微服务数据私有原则,真正流量大时一般会用别的方式解决,比如缓存技术、分库分表、单独的数据访问服务做统一优化,单元化架构。目前技术栈里复杂度读场景的性能不是瓶颈,利用索引外置方案的ES能满足目前需求。
灵活多变条件下的读场景。读写在同一个服务,让读场景不用遵循领域分层,不需要从DB开始逐层传递数据,从而提高开发效率。
【评价】:一个服务里没有完全使用领域分层,容易使得领域代码腐烂。目前建议查询场景也经过领域模型层,通过其他手段来提高查询场景开发效率。
DDD之工程设计、落地
DDD的四重边界划分过程就是一个设计的过程,在每重边界都需要遵守特定的设计原则。
- 战略设计阶段的核心域、支撑域、通用域如何划分?划分原则是分治(把整个问题域拆分为粒度更小、更容易理解和测量的域),聚焦(资源规划在代表核心竞争力的核心域)
- 如何划分BC - 详见DDD的架构
- BC内如何划分模块层次 - 详见DDD的架构
- 领域层次内如何划分聚合,如何管理模型对象的关系 - 详见DDD的设计原则。这个层次的很多设计原则来自面向对象设计,运用SOLID原则指导对象的关系。并在对象之上引入聚合,进一步内聚领域,减少对象间的依赖。
DDD学习难、落地难
大多数人都有同样的这种感受,看完很多DDD的书和理论,还是不知道怎么开始动手建模、编码。是现在能看到的DDD资料都没有很好的回答下面四个问题:
- 学习落地门槛偏高,掌握DDD的人需要兼备领域业务和技术。也正是因为跨行业领域差异大,通用解决方案或者通用框架流行的可能性就低。
- DDD缺乏固化的建模流程指导,没有衡量手段。战略设计、战术设计(分析、设计、实现),每个流程具体做什么,交付什么,没有说明。
- DDD缺乏通用框架。分层架构虽属是战术层面的技术架构,但每层包含的细节不够明确。也没有开源框架可以学习借鉴。不像其他框架容易入门,比如LAMP、SSM、Spring、Dubbo、Spring Cloud、K8s等。
面对以上现状,爱番番DDD实施指南(DDD实施指南)试图去探索出一套适合爱番番的DDD方法论,在规范设计原则的基础上尝试回答上面的问题,让DDD在爱番番有效落地,从而带来业务和客户价值。
学习和应用DDD需要经历一个过程,借用参禅的三重境界,以此提醒大家多学多用多悟。
- 参禅之初:看山是山,看水是水。初步接触DDD后,开发每行代码都会纠结于DDD的某个原则细节问题,往往忘了DDD的初衷。
- 参禅悟时:看山不是山,看水不是水。有些DDD实战经验后,往往会怀疑按照DDD设计的东西到底有没有用,特别是当业务还不是很复杂时。
- 参禅彻悟:看山仍是山,看水仍是水。比较熟练的掌握了DDD后,才能从容应对各种业务需求开发,对边界和原则拿捏得当。DDD是一种方法论,督促和指导业务开发整个过程要理清业务的本质需求。如果本身的业务需求已经足够简单清晰,是不是DDD已不重要。
DDD有用、但不是银弹
DDD并不是万能的,不是所有业务开发场景都适合用DDD。有些简单业务场景不使用DDD反而更恰当。因为DDD有较高的学习门槛高,需要整个团队统一认识一起协同,需要相应的架构规范落地。学习和落地DDD时要时刻记住自己的出发点,是为了应对现在或者将来的复杂业务领域而来。因此不必太拘泥于某些点是否遵守了DDD原则,如果觉得用了DDD会比没有用DDD的时候更好,也值得踏出这一步。
DDD的阅读书籍
《领域驱动设计精粹》:短小精悍的入门版,快速入门的话强烈推荐这本
《领域驱动设计模式、原理与实践》:名气不如《领域驱动设计》和《实现领域驱动设计》,但讲的更接地气,实操DDD推荐这本。
《领域驱动设计》:入门DDD的必读书籍,偏重设计原则
《实现领域驱动设计》:实操DDD的书籍,偏重实战细节
《架构整洁之道》:架构师入门,特别是应用架构师的推荐书籍,偏重架构本质探索和落地方法