本文简单回顾了软件架构的演进历史,并梳理了几种常见的架构风格。
软件架构演进史
软件架构并不是一开始就作为一门方法论存在的,其本质是软件规模不断扩大后被倒逼出来的结果。
软件危机
1968 年,北约在德国召开的软件工程会议把当时行业里普遍存在的问题集中揭示了出来:项目超期、超预算、质量不稳定、维护困难、复杂度失控。这些问题后来被统称为“软件危机”[1]。
当时的很多系统还不需要今天意义上的完整架构方法,大家更关注的是算法、数据结构和把功能先做出来。但随着系统规模扩大,工程师很快发现,真正让项目失控的往往不是某个函数写错了,而是整体结构没有被认真设计。
也正是在这种背景下,模块化、信息隐藏、分层、关注点分离这些思想开始变得越来越重要。软件设计开始从“怎么写代码”逐渐走向“怎么组织系统”。
单体架构与代码分层
在较长一段时间里,软件系统的主流形态其实就是单体应用。所有功能放在一个系统里,围绕一套代码、一套部署物、一套数据库展开。这种方式在业务简单、团队规模不大时非常高效。
为了管理不断膨胀的复杂度,开发者逐渐引入分层结构,例如表现层、业务层、数据访问层,这是软件工程从混乱走向秩序的一次重要进步。
部署分层
进入 1980 年代到 1990 年代,企业信息系统和网络应用快速发展,软件不再只是单机程序,而是越来越多地以 Client/Server 形式运行。客户端负责交互,服务器负责数据和业务处理[2]。
随后,三层架构进一步流行起来。界面、业务逻辑、数据库被拆到不同层甚至不同机器上,系统开始具备更明确的部署边界和扩展边界。这一阶段的软件架构,重点是把“功能组织”进一步演进成“逻辑分层 + 物理分布”[3]。
面向服务架构(SOA)
到了 2000 年代,企业系统集成需求越来越强,不同业务系统之间需要互通、复用和编排。SOA 因此兴起[4]。
SOA 的核心目标,是把能力抽象成服务,让不同系统可以通过统一协议协作。它解决的是大型企业内部多个系统之间的整合问题,所以经常伴随 ESB、Web Service、集中治理这类配套机制出现。
这一时期典型的软件形态,是大型企业的 ERP、CRM、HR、财务、供应链系统通过 SOAP / WSDL 接入 ESB,运行在 Java EE 或 .NET 应用服务器(WebLogic、WebSphere、JBoss)上,XML 几乎承担了所有数据交换格式,BPEL 用来描述跨服务流程,整套体系强调契约先行、集中治理。
也正是在这个时期,Roy Fielding 在 2000 年的博士论文里提出了 REST 架构风格[5]。它选择了一条更轻量的路:直接复用 HTTP 语义、用 JSON 替代 XML、面向资源而不是面向接口,被视为对 SOAP/WS-* 重型体系的一种替代。随着 Web 2.0、移动端和前后端分离的兴起,REST 逐步取代 SOAP 成为公共 API 的事实标准,也为后来的微服务铺好了通信路径。
微服务
再往后,互联网业务、持续交付、云平台和容器化技术逐渐成熟,架构目标开始从“复用服务”转向“让系统可以更快演进”。这就是微服务兴起的背景[6]。
微服务继承了 SOA 的服务化思想,但更强调:
- 服务围绕业务能力拆分
- 每个服务独立部署、独立扩缩容
- 使用轻量通信机制
它更适合变化快、团队多、发布频繁的系统,但也把分布式复杂度带了进来。因此它不是简单地“替代”单体或 SOA,而是在新的工程环境下形成的一种取舍。
前后端分离
差不多和微服务同期,浏览器侧也发生了一次大变化。Angular(2010)、React(2013)、Vue(2014)等 SPA 框架陆续成熟,前端有了真正完整的工程体系——组件化、状态管理、构建工具、路由都齐了。”前端”开始作为独立项目存在,部署成静态资源(HTML / JS / CSS)发布到 CDN,后端则收敛为纯 API 服务,两边只通过 HTTP + JSON 交互。
这其实是”部署分层”在 Web 时代的延续:把原本由服务端承担的 UI 渲染彻底交还给客户端。它和后端微服务化是天然搭档——前端独立部署、后端拆成多个服务,整个系统的边界变得更清晰。
云原生
近些年,软件架构又继续向云原生方向演进。容器、Kubernetes、DevOps、可观测性、服务网格等技术,让系统能自动部署、自动恢复、按需扩缩[7]。
与此同时,事件驱动架构也越来越常见。很多系统不再要求所有动作都同步串行完成,而是通过事件总线、消息队列和异步处理来提高解耦程度、吞吐能力和业务响应速度。
常见架构风格
架构风格描述了系统的组织方式——构件如何划分、如何连接、如何协作。
微服务架构
微服务的核心思想,是把一个大型系统拆成多个小型自治服务。每个服务围绕一块相对独立的业务能力构建,独立开发、独立部署、独立扩缩容,通过轻量通信机制协作。
VS 单体架构
单体架构通常把所有功能放在一个进程里,开发初期效率很高,部署也简单。但随着业务变大,代码、构建、发布、协作和故障影响范围会一起膨胀。
微服务的优势在于:
- 可以围绕业务能力拆分职责,降低单个服务的复杂度
- 支持独立部署和独立扩缩容,热点模块可以单独处理
- 更适合多人协作,不同团队可以并行维护不同服务
- 技术选型更灵活,不同服务可以根据场景使用不同技术栈
- 故障隔离能力更强,单点问题不一定拖垮整个系统
拥有这些优势的同时也付出了代价:
- 本地调用变成网络调用,必须面对超时、重试、熔断、幂等和序列化问题
- 运维复杂度显著上升,需要处理注册发现、配置中心、链路追踪、监控告警和容器编排
- 数据一致性更难保证,传统 ACID 思路往往不再适用
- 测试和排障难度增加,很多问题只能在集成环境或真实流量下暴露
所以微服务是一种用复杂度换扩展性的选择。系统还很小的时候,过早微服务往往是负收益。
几种常见的微服务组织方式
聚合器模式
客户端请求先进入 API 网关,再由聚合器统一调用多个后端服务,把结果组装后一次性返回给前端。这种模式常见于首页、控制台、用户画像这类需要拼装多份数据的场景。
flowchart LR
Client[客户端] --> Gateway[API 网关]
Gateway --> Aggregator[聚合器服务]
Aggregator --> A[微服务 A]
Aggregator --> B[微服务 B]
Aggregator --> C[微服务 C]
它的优点是接口聚合清晰,前端不用感知多个服务的细节;缺点是聚合器很容易变成新的中心节点,一旦职责失控,就会演变成“另一个单体”。
链式调用模式
请求进入系统后,由一个服务继续调用下一个服务,逐步形成调用链。这种方式在流程型业务里很常见,比如订单、支付、库存、物流一层层向后推进。
flowchart LR
Client[客户端] --> Gateway[API 网关]
Gateway --> A[微服务 A]
A --> B[微服务 B]
B --> C[微服务 C]
它的好处是流程表达自然,职责也比较直观;但调用链一长,延迟、失败传播和排障成本都会快速上升。一个节点变慢可能会拖住整条链。
数据共享模式
多个微服务共同读写同一个数据库。它在很多微服务的改造初期很常见,因为实现快、迁移成本低,也容易维持数据一致性。
flowchart LR
A[微服务 A] --> DB[(共享数据库)]
B[微服务 B] --> DB
C[微服务 C] --> DB
这种模式的结构性问题很明显:数据库模式一改,多个服务都会受影响。
异步消息模式
服务之间不直接同步调用,而是通过消息队列解耦。一方负责发送事件或消息,另一方异步消费。
flowchart LR
A[微服务 A] --> MQ[消息队列
Kafka / RabbitMQ / RocketMQ]
MQ --> B[微服务 B]
这种模式非常适合跨系统解耦,但也会带来新的工程问题:消息重复、乱序、积压、重试和最终一致性。
分层架构
分层架构是最常见、也最容易落地的一种结构方式。它把系统按职责分成若干层,每层只处理自己负责的事情,并尽量只和相邻层交互。
很多业务系统即便外层用了微服务,单个服务内部仍然会采用分层架构,因为它足够稳定,也便于团队协作。
传统分层通常可以简化为三层:
| 层级 | 主要职责 | 常见实现 |
|---|---|---|
| 表现层 | 接收请求、参数校验、协议转换、响应封装 | Controller、REST API、GraphQL Resolver |
| 业务层 | 承载业务逻辑、流程控制、事务处理 | Service、Manager |
| 持久层 | 负责数据访问和持久化细节封装 | DAO、Repository、Mapper |
这种结构的优点很明确:
- 结构清晰,容易理解
- 职责边界明确,适合大多数 CRUD 和中后台系统
- 修改影响范围相对可控,便于新人接手
它的问题也同样典型:
- 简单逻辑也可能被迫层层透传,样板代码偏多
- 如果设计不到位,每一层都只是在机械转发,实际没有承担清晰职责
- 业务复杂后,所谓“业务层”容易无限膨胀,最后变成巨型 Service
分层架构不是落后,而是基础。很多架构风格说到底,都是在分层的基础上继续细化职责边界。
DDD 分层:当业务复杂到不能只靠 Service 硬扛
当系统规则很多、状态变化复杂、术语容易混乱时,传统“Controller + Service + DAO”往往会开始失控。业务规则会不断堆进 Service,最后形成一批难读、难测、难复用的流程脚本。
DDD(领域驱动设计)试图解决的正是这个问题。它强调让代码结构贴近业务模型,把真正核心的业务规则沉淀到领域层,而不是全部塞进应用服务里。
DDD 常见的分层方式如下:
| 层级 | 主要职责 | 常见实现 |
|---|---|---|
| 接口层 | 对外暴露访问入口,完成协议转换和基础校验 | Controller、RPC Handler、MQ Consumer |
| 应用层 | 编排用例流程,控制事务,协调领域对象和外部系统 | Application Service、Command Handler |
| 领域层 | 承载核心业务规则和业务模型 | Entity、Value Object、Aggregate、Domain Service |
| 基础设施层 | 提供数据库、缓存、消息队列、第三方系统等技术实现 | Repository Impl、DAO、ORM、Redis Client |
传统写法里,业务规则往往直接塞进 Service:
1 | class OrderService { |
DDD 的思路则是把“订单如何下单”这类规则放回领域对象本身,让应用层只负责组织流程:
1 | class OrderApplicationService { |
这样做的价值在于:
- 代码结构更贴近业务语义,复杂规则不容易散落
- 团队可以围绕统一领域语言协作,减少“同一个概念多种叫法”
- 领域层尽量不依赖数据库和框架,替换基础设施的成本更低
当然,DDD 的学习成本和建模成本高,如果业务本身并不复杂,上 DDD 会把简单问题复杂化。
事件驱动架构
事件驱动架构(EDA)的核心不是“函数互相调用”,而是“构件围绕事件协作”。某个动作发生后,系统发布一个事件,其他感兴趣的模块再决定是否订阅和处理。
flowchart LR
Producer[事件产生方] -->|发布事件| Bus[事件总线 / 事件管理机制]
Bus -->|通知处理| ConsumerA[处理器 A]
Bus -->|通知处理| ConsumerB[处理器 B]
Bus -->|通知处理| ConsumerC[处理器 C]
这种模式在 GUI 编程、消息系统、业务通知、审计日志、异步任务和微服务协作里都非常常见。和传统调用返回模型相比,它最大的特点是发布方不需要知道接收方是谁,也不需要等待所有处理过程完成。
它的优势主要体现在:
- 松耦合,新增处理逻辑通常不需要改动事件源
- 扩展性强,很适合一对多通知场景
- 天然适合异步化和并发处理
- 某些横切能力,比如审计、埋点、通知,可以更自然地挂接到业务流程上
但事件驱动的问题也很典型:
- 流程不再线性可见,调用链和时序关系更难追踪
- 数据传递和事件定义需要更严格的约束
- 执行顺序可能不稳定,容易引入竞态和重复消费问题
- 一旦缺少完善的日志、追踪和监控,排障体验会非常差
所以事件驱动很适合作为“解耦机制”,但不应该被神化。系统里哪些流程适合同步调用,哪些适合转成事件,通常需要结合一致性要求和时效要求来判断。
结语
真正成熟的系统,通常也不会只使用一种风格。更常见的是:外层是微服务,服务内部是分层架构,某些模块使用 DDD,跨服务协作再配合事件驱动。这种组合是工程上的常态,让系统在功能、维护性和扩展性之间取得平衡。