第 2 章:分层架构的起源:从单体应用到分层

大家好,欢迎来到第二章。

这一章我们聊聊软件架构的演进。说白了,就是看看我们是怎么从“一锅乱炖”走到“分而治之”的。

我刚开始写代码那会儿,项目结构很简单。一个 .c 文件,或者一个 .java 文件,从头写到尾。数据库连接、业务逻辑、界面渲染,全揉在一起。那时候项目小,一个人能搞定。但后来项目大了,问题就来了。

嗯,这就是我们今天要聊的起点——单体架构。

2.1 单体架构:最初的“大泥球”

单体架构(Monolithic Architecture)是最早期的软件组织方式。所有功能模块、数据处理、用户界面,都打包在同一个可执行文件或部署单元里。

你想想看,一个典型的单体应用长什么样?

  • 一个 WAR 包或 EXE 文件,包含了所有代码
  • 一个数据库实例,所有表都在里面
  • 一个进程,处理所有请求
  • 团队所有人,都在同一个代码仓库里改代码

听起来简单粗暴,对吧?

我在 2010 年左右接手过一个电商后台系统,就是典型的单体架构。一个 Tomcat 实例,部署了一个 200 多 MB 的 WAR 包。里面订单、商品、用户、支付,全在一个工程里。刚开始还好,但后来每次发布都提心吊胆。改一行代码,整个系统都要重新编译、打包、部署。更可怕的是,一个模块出问题,整个系统都可能挂掉。

单体架构的核心痛点:
  • 耦合度高:模块之间直接调用,牵一发而动全身
  • 扩展性差:只能整体水平扩展,无法针对热点模块单独扩容
  • 维护成本高:代码量大了以后,新人上手极慢
  • 部署风险大:一次发布影响所有功能

为什么会这样?因为所有代码都在同一个地址空间里运行。没有边界,没有隔离。就像一个大厨房,所有厨师都在同一个灶台上炒菜,互相干扰。

2.2 分层的萌芽:从“分离关注点”开始

为了解决单体架构的问题,前辈们开始思考:能不能把不同职责的代码分开?

这就是“分离关注点”(Separation of Concerns)的思想。说白了,就是让每一层只关心自己的事。

我记得最早接触分层概念,是在读 Martin Fowler 的《企业应用架构模式》那本书。他提出了三层架构的经典模式:

  • 表现层(Presentation Layer):处理用户界面和交互
  • 业务逻辑层(Business Logic Layer):处理核心业务规则
  • 数据访问层(Data Access Layer):处理数据库操作

这个模式后来成了企业级应用的事实标准。直到今天,大部分 Web 应用还在沿用这个思路。

你可能会问:为什么是三层?不是两层或四层?

其实,层数不是固定的。关键在于:每一层只依赖它的下一层,不能跨层调用。这就是分层架构的核心约束。

我的经验: 刚开始做分层时,很容易犯“跨层调用”的毛病。比如在表现层直接写 SQL 查询。我曾经在一个项目里看到这样的代码:Controller 里直接调用 JDBC 连接数据库。嗯,那场面,简直灾难。后来我们花了整整两个迭代,才把这种“越层”调用全部清理掉。

2.3 分层架构的核心原则

分层架构不是简单地把代码放到不同文件夹里。它有严格的规则。

原则 说明 违反后果
单向依赖 上层只能依赖下层,不能反向依赖 循环依赖,编译失败
封装性 每层只暴露必要的接口,隐藏实现细节 层间耦合,修改困难
替换性 下层可以被替换,只要接口不变 技术栈锁定,无法升级
关注点分离 每层只处理自己的职责 代码混乱,职责不清

这些原则听起来简单,但实际落地时,坑很多。

我曾经在一个项目中,业务逻辑层直接返回了 DataSet 对象给表现层。表现层拿到 DataSet 后,直接遍历行和列来渲染表格。这看起来“方便”,但实际上破坏了封装性。后来数据库表结构一改,表现层代码全要跟着改。这就是典型的“层间泄漏”。

避坑指南: 我曾经在分层时犯过一个低级错误——在业务逻辑层里写了事务控制代码,同时又让数据访问层也做了事务。结果两层事务嵌套,导致死锁。记住:事务边界应该统一在业务逻辑层控制,数据访问层只负责原子操作。

2.4 分层架构的典型结构

我们来看一个标准的四层架构(在经典三层基础上增加了基础设施层):

┌─────────────────────────────────┐
│        表现层 (UI Layer)         │
│  - 控制器 (Controller)          │
│  - 视图 (View)                  │
│  - 视图模型 (ViewModel)         │
├─────────────────────────────────┤
│        应用层 (Application Layer)│
│  - 服务 (Service)               │
│  - 数据传输对象 (DTO)           │
│  - 应用事件 (Application Event) │
├─────────────────────────────────┤
│        领域层 (Domain Layer)     │
│  - 实体 (Entity)                │
│  - 值对象 (Value Object)        │
│  - 领域服务 (Domain Service)    │
│  - 仓储接口 (Repository Interface)│
├─────────────────────────────────┤
│      基础设施层 (Infrastructure) │
│  - 数据访问 (Data Access)       │
│  - 消息队列 (Message Queue)     │
│  - 外部服务 (External Service)  │
│  - 缓存 (Cache)                 │
└─────────────────────────────────┘

注意看,依赖方向是从上到下的。表现层依赖应用层,应用层依赖领域层,领域层依赖基础设施层。反过来不行。

这里有个关键点:领域层不依赖任何具体的技术实现。它只定义接口(比如 Repository 接口),具体的数据库操作由基础设施层实现。这就是“依赖倒置原则”(Dependency Inversion Principle)在分层架构中的应用。

2.5 分层架构的优缺点

任何架构模式都不是银弹。分层架构也有它的适用场景和局限性。

优点 缺点
结构清晰,职责明确 层数增加,性能有损耗
易于维护和扩展 小项目可能过度设计
支持并行开发 层间通信需要定义接口
技术栈可替换 学习曲线较陡
便于测试(可 mock 下层) 可能出现“胖服务层”

我个人习惯是:项目初期不要过度分层。先按三层架构搭起来,等业务复杂度上来了,再考虑是否要拆出独立的领域层或基础设施层。否则,你可能会陷入“为分层而分层”的陷阱。

2.6 分层架构的演进:从物理分层到逻辑分层

早期的分层是物理上的。表现层在一台服务器上,业务逻辑层在另一台,数据层在第三台。这叫“物理分层”。

但后来大家发现,物理分层带来了网络开销和部署复杂度。于是,逻辑分层成了主流。所有层都在同一个进程中,但通过接口和命名空间来隔离。

再后来,微服务架构兴起。它把分层的思想进一步推向了极端——每个服务都是一个独立的进程,有自己的分层结构。服务之间通过 API 通信。

但不管怎么变,分层的核心思想始终没变:通过定义清晰的边界和接口,降低系统的复杂度。

一句话总结: 分层架构的本质,就是用“层”作为边界,把大问题拆成小问题,然后定义好层与层之间的契约(接口),让每一层可以独立演化。

2.7 本章小结

我们从单体架构的痛点出发,看到了分层架构是如何一步步解决这些问题的。核心就三点:

  1. 分离关注点:每层只做一件事
  2. 单向依赖:上层依赖下层,不反向
  3. 接口定义:层与层之间通过接口通信,而不是直接调用实现

下一章,我们会深入探讨“接口定义”这个核心话题。毕竟,分层架构能不能落地,关键就看接口怎么设计。

嗯,今天就到这里。希望你能从这一章里,看到分层架构背后的设计哲学。它不只是技术,更是一种思维方式。