结构化程序设计解决了第一次软件危机。60年代~70年代计算机刚刚投入商业使用,主要的编程方式还是汇编语言在特定的机器上编写程序。当软件规模较小,基本上处于计算机科学家个人编码设计、使用的方式。随着软件规模扩大,复杂度增加,依赖特定机器、无结构化的编程方式无法应对软件的发展,带来了第一次软件危机。为了克服这个问题,业界提出了”软件工程“的概念,1972 年 C 语言的出现,解决了代码结构化、抽象性、可移植的问题。
面向对象解决了第二次软件危机。随着软件在商业中大规模使用,软件变得原来越复杂,即使结构化的 C 语言也无法满足业界对可维护性、可拓展性的需求。标志性的事件是 IBM 公司开发的 OS/360 系统失败,该系统有 4000 多个模块,约 100 万条指令,以及大量的 bug。面向对象的编程语言,Java、C#、C++ 出现,面向对象带来了更自然地代码组织方式,软件开发变得越像建筑业。
对接入层来说,我们可以看到,实际上接入层是依附于应用层存在的,随着前后端分离,Restful API 成了主流,对简单的系统来说这一层越来越弱化。对于有终端接入的系统来说,接入层并不简单,需要处理各种协议适配:XMPP、websocket、MQTT 等。在复杂度不高的情况下,我们往往把接入层和应用层合并部署,这里往往凭经验来决定。如果对分布式级别有了认识,可以更为科学的选择是否要将接入层和应用部署到一起。接入层的特点:
关心视图和对外的服务,Restful、页面渲染、websocket、XMPP 连接等
如果没有多种接入方式,可以和应用层合并
对应到分布式系统中的网关、BFF、前台等概念
只产生接入异常,例如数据校验,对应 HTTP 状态码 400、415 等
一个应用可以有多个接入层
接入层做和业务规则无关的 bean validation 验证
准单体系统下,按照连接方式分包
领域层
对于领域层来说,很多互联网公司没有这个概念,将这些实现混合在应用层隐藏实现了,造成业务规则不一致。随着前后端分离的发展,2013 年左右我也开始前后端分离实践,接入层剥离出去后,后端开发者开始审视是否需要抽象出一层来复用业务逻辑。当时大部分互联网公司称为服务,也就是 SOA 架构,大量使用 XML 和 SOAP 技术。领域层的特点:
准单体系统架构下,所有的代码在一个代码仓库,四层架构依然,往往通过多模块组织代码。应用层通过不同的模块实现,然后将领域服务抽出来一个公用模块。很多小型项目依然保持这种形态,每层能保持良好的依赖关系非常重要。每层之间最好依次向下调用,DDD 的书中有一个不好的示例,上层可以跳过中间层直接调用下层。很多内网部署的传统项目单机就能满足,小型公司的 OA 软件、餐饮软件、会员管理系统的单机版就是通过这种方式部署。
低级别分布式系统
将应用水平拓展,数据库进行主从拆分,Redis 使用主从或哨兵模式,本质上和准单体系统没有区别,应用没有垂直拓展复杂性不会有特别大的提升。还有一种折中的方式,应用层各个模块单独部署,领域层的业务逻辑单独部署或者通过 Jar 包的方式加载应用中,实现应用层的解耦,并且不会带来分布式的问题。基于上面这种模式的变体,下面这种部署方式也有很多,通过这种部署方式,领域服务使用严谨的 Java 实现,接入层和应用层使用 php、Nodejs 等动态语言实现。
高级别分布式系统
如果我们把应用和领域层都独立部署,就得到了现在主流的微服务架构。只不过在微服务的语境下,应用层 + 接入层被称为 BFF (Backend for Frontend),领域层负责实现业务逻辑,应用层用于各种业务场景下的适配。然而这种设计会受到一些批评,他们认为这不是正宗的微服务,而像现在所说的中台。部分微服务的工程师倡导使用 API Gateway 的方式将领域服务的 API 直接暴露给端侧。实际上这种做法应用层并没有消失,编排领域服务 API 的职责被下放到端侧,在一些特殊的业务场景下没有问题,但是大多数场景下并不合适。业务逻辑容易造成碎片化,存在调用次数多,服务间最终一致性事务难以实现等问题。下面这张图表达了这种设计方式,但大多数情况下并不推荐。到此,领域层被垂直拆分,随之而来的就是我们熟知的各种分布式问题了,熔断、负载均衡等问题属于技术复杂度可以在业务无感知的情况下被解决,但下面几个问题需要侵入业务才能被良好的解决,因此还需要 DDD 的帮助。