1968 年,Dijkstra 在 THE 多道程序系统中第一次系统性地使用了分层(Layering)思想:把操作系统分成六层,每一层只依赖它下面的那一层。半个世纪过去了,微服务、事件驱动、Serverless 轮番登场,分层架构不但没有消失,反而成了几乎所有框架的默认组织方式。Spring Boot 项目的 Controller-Service-Repository 三层结构、Django 的 MTV 模式、甚至前端 React 应用里的 Component-Hook-API 分层,都是这个古老模式的变体。
为什么?因为分层架构解决的不是某个具体的技术问题,而是软件工程中最基本的矛盾:复杂性管理。人类大脑一次只能处理有限数量的概念,分层把系统切成几个认知边界清晰的切片,让开发者在任意时刻只需要关注一层的逻辑。
但分层架构也不是没有代价。过多的层会变成”千层面架构”(Lasagna Architecture),每层只是无脑透传;过少的层又退化成大泥球。严格分层和宽松分层的选择直接影响可测试性、性能和开发效率。依赖倒置(Dependency Inversion)的引入更是从根本上改变了”上层依赖下层”的传统方向。
本文从分层架构的核心价值出发,拆解 Layer 与 Tier 的本质区别,对比严格分层与宽松分层的真实影响,分析依赖倒置如何改变分层方向,并结合 Spring Boot 和 Django 的分层实现给出工程判断。
阅读建议:本文假设你已经读过 上一篇关于单体架构 的讨论。分层架构和单体架构经常同时出现——一个单体应用内部最常见的组织方式就是分层。
一、分层架构的核心价值
分层架构的核心价值可以归结为两个词:关注点分离(Separation of Concerns)和依赖管理(Dependency Management)。
关注点分离
Dijkstra 在 1974 年的论文 On the role of scientific thought 中提出了关注点分离(Separation of Concerns)的概念。他的原话是:把一个复杂问题分解成可以独立思考的部分,是人类控制复杂性的唯一有效手段。
分层架构把这个原则落地成一个简单的规则:把系统按职责切成水平切片,每一层只处理一类问题。表示层(Presentation Layer)负责用户交互,业务逻辑层(Business Logic Layer)负责领域规则,数据访问层(Data Access Layer)负责持久化。一个开发者在修改数据库查询语句时,不需要知道页面上的按钮长什么样。
这不是”良好实践”级别的建议,而是一个可以被量化的工程约束。当一层的修改不会扩散到其他层时,修改的影响范围(blast radius)就被限制住了。
依赖管理
分层架构的第二个价值是强制依赖方向单一化。在经典分层中,依赖只能从上往下流:表示层依赖业务逻辑层,业务逻辑层依赖数据访问层,反过来不允许。
这个约束看起来简单,但它解决了一个致命问题:循环依赖。一旦 A 依赖 B 且 B 依赖 A,修改任何一方都可能引发连锁反应。分层架构用一条简单规则——“只能向下依赖”——消除了循环依赖的可能性。
可替换性
当层与层之间通过接口(而不是具体实现)通信时,替换某一层的实现就变得可行。把 MySQL 换成 PostgreSQL?只改数据访问层。把 REST API 换成 gRPC?只改表示层。这种可替换性在理论上很美好,实际中能做到多少取决于接口设计的质量——但分层至少提供了做到这一点的结构基础。
二、Layer 与 Tier:逻辑分层和物理分层
Layer 和 Tier 这两个词在中文里都被翻译成”层”,但它们描述的是完全不同的东西。混淆它们是架构讨论中最常见的误解之一。
Layer:逻辑分层
Layer 是逻辑概念,指的是代码在职责上的划分。一个三层(3-Layer)应用可能全部运行在同一台机器的同一个进程里。Layer 关心的是代码组织:哪些类属于表示层,哪些类属于业务逻辑层,哪些类属于数据访问层。
Layer 之间的边界是命名空间、包、模块这些代码组织单元。跨层调用是普通的函数调用或方法调用,不涉及网络。
Tier:物理分层
Tier 是部署概念,指的是系统在物理上被部署到多少个独立的运行环境中。一个三层部署(3-Tier)的典型结构是:浏览器(客户端)、应用服务器、数据库服务器。Tier 之间的通信必须通过网络——HTTP、TCP、gRPC 或其他协议。
关键区别
| 维度 | Layer(逻辑层) | Tier(物理层) |
|---|---|---|
| 本质 | 代码职责划分 | 部署拓扑划分 |
| 边界 | 包、模块、命名空间 | 进程、机器、网络 |
| 通信方式 | 函数调用(进程内) | 网络调用(进程间) |
| 性能开销 | 几乎为零 | 网络延迟 + 序列化/反序列化 |
| 独立部署 | 不可以 | 可以 |
| 独立扩展 | 不可以 | 可以 |
| 技术异构 | 困难(同一运行时) | 容易(不同运行时) |
一个常见的错误是把 3-Layer 和 3-Tier 当成一回事。一个 Spring Boot 应用可能内部分成 Controller、Service、Repository 三个 Layer,但它们全部跑在同一个 JVM 进程里,部署上只是 1-Tier。只有当你把前端独立部署、应用服务器独立部署、数据库独立部署时,才构成 3-Tier。
这个区别之所以重要,是因为它直接决定了你需要操心的问题集。Layer 之间的调用是可靠的、同步的、低延迟的;Tier 之间的调用可能失败、可能超时、可能丢包。从 3-Layer 演变到 3-Tier,你的系统瞬间多出了网络分区、序列化兼容性、分布式事务等一堆新问题。
工程判断:在讨论架构时,先明确你说的”三层”是 Layer 还是 Tier。很多团队在技术方案评审中因为混淆这两个概念,高估了”分层”带来的隔离性——以为逻辑分层就能独立扩展,其实不能。
三、经典 N 层架构
经典的分层架构通常分为三层或四层。以最常见的三层架构为例:
┌─────────────────────────────────┐
│ 表示层 Presentation │ ← 用户界面、API 端点
├─────────────────────────────────┤
│ 业务逻辑层 Business Logic │ ← 领域规则、工作流
├─────────────────────────────────┤
│ 数据访问层 Data Access │ ← 数据库操作、外部数据源
└─────────────────────────────────┘
表示层(Presentation Layer)
负责接收用户输入并返回响应。在 Web 应用中,它处理 HTTP 请求和响应;在桌面应用中,它管理窗口和控件;在 API 服务中,它定义端点和序列化格式。
表示层不应该包含业务逻辑。一个典型的违规场景:在
Controller 里写
if (user.getAge() >= 18 && user.getBalance() > price)
这样的业务判断。这些规则应该在业务逻辑层。
业务逻辑层(Business Logic Layer)
系统的核心。所有的领域规则、业务流程、数据验证和计算都在这一层。业务逻辑层不应该知道数据是从 MySQL 还是 MongoDB 来的,也不应该知道请求是通过 REST 还是 gRPC 进来的。
这一层是最稳定的——业务规则的变化频率通常远低于 UI 和数据存储技术的变化频率。“用户必须年满 18 岁才能购买”这条规则,不管前端从 jQuery 换成 React,还是数据库从 MySQL 换成 PostgreSQL,都不应该改。
数据访问层(Data Access Layer)
封装所有与数据持久化相关的操作:数据库查询、文件读写、外部 API 调用、缓存操作。它对上层暴露与存储技术无关的接口,对下层处理具体的 SQL、ORM 映射或 API 调用。
四层架构变体
在三层基础上,常见的扩展是在表示层和业务逻辑层之间加一个应用服务层(Application Service Layer),或在底部加一个基础设施层(Infrastructure Layer):
┌─────────────────────────────────┐
│ 表示层 Presentation │
├─────────────────────────────────┤
│ 应用服务层 Application │ ← 用例编排、事务管理
├─────────────────────────────────┤
│ 领域层 Domain │ ← 纯业务规则
├─────────────────────────────────┤
│ 基础设施层 Infrastructure │ ← 数据库、消息队列、外部服务
└─────────────────────────────────┘
应用服务层的职责是编排:它调用领域层的对象来完成一个完整的用例,管理事务边界,协调多个领域对象之间的交互。领域层则只包含纯粹的业务规则,不依赖任何框架或基础设施。
这个四层结构已经非常接近 DDD(领域驱动设计)的分层模式。关于 DDD 的具体实践,后续系列会详细展开。
四、严格分层与宽松分层
分层架构有两种执行策略:严格分层(Strict Layering)和宽松分层(Relaxed Layering)。这个选择对系统的耦合度、可测试性和性能有直接影响。
严格分层
严格分层(也叫封闭分层,Closed Layering)的规则是:每一层只能调用它直接下方的那一层。表示层只能调用业务逻辑层,业务逻辑层只能调用数据访问层。表示层不能跳过业务逻辑层直接访问数据库。
graph TB
A[表示层 Presentation] --> B[业务逻辑层 Business Logic]
B --> C[数据访问层 Data Access]
A -. "禁止跨层调用" .-> C
style A fill:#388bfd,stroke:#388bfd,color:#fff
style B fill:#a371f7,stroke:#a371f7,color:#fff
style C fill:#3fb950,stroke:#3fb950,color:#fff
严格分层的好处是隔离性强。当你替换数据访问层的实现时,只有业务逻辑层需要适配,表示层完全不受影响。测试时,你可以 mock 业务逻辑层来单独测试表示层,mock 数据访问层来单独测试业务逻辑层。
代价是透传代码(pass-through code)。当表示层只需要一个简单的数据查询,没有任何业务逻辑需要执行时,业务逻辑层仍然需要提供一个方法,做的事情就是原样转发调用到数据访问层。这种代码没有逻辑价值,纯粹是为了满足分层约束。
宽松分层
宽松分层(也叫开放分层,Open Layering)允许跨层调用。表示层可以直接调用数据访问层获取简单数据,只在需要业务逻辑处理时才经过业务逻辑层。
graph TB
A[表示层 Presentation] --> B[业务逻辑层 Business Logic]
B --> C[数据访问层 Data Access]
A --> C
style A fill:#388bfd,stroke:#388bfd,color:#fff
style B fill:#a371f7,stroke:#a371f7,color:#fff
style C fill:#3fb950,stroke:#3fb950,color:#fff
宽松分层减少了透传代码,在简单场景下性能更好(少一层函数调用的开销在逻辑分层中几乎可以忽略,但在物理分层中会减少一次网络往返)。代价是依赖关系更复杂:表示层现在同时依赖业务逻辑层和数据访问层,替换数据访问层时需要检查两层的代码。
真实影响对比
下面这张表基于工程实践中的实际观察,而非理论推导:
| 维度 | 严格分层 | 宽松分层 |
|---|---|---|
| 耦合度 | 低。每层只耦合相邻层 | 中。上层可能耦合多个下层 |
| 可测试性 | 高。mock 点清晰,每层独立可测 | 中。跨层依赖导致 mock 组合增多 |
| 可替换性 | 高。替换一层只影响相邻层 | 低。替换下层需检查所有依赖它的上层 |
| 透传代码 | 多。简单操作也要经过中间层 | 少。简单操作可以跳过中间层 |
| 开发效率 | 初期慢。需要在每层定义接口 | 初期快。可以走捷径 |
| 维护成本 | 长期低。修改影响范围可控 | 长期高。跨层依赖会随时间扩散 |
| 性能(逻辑分层) | 几乎无差异 | 几乎无差异 |
| 性能(物理分层) | 多一次网络往返 | 少一次网络往返 |
| 适用规模 | 中大型项目、长期维护系统 | 小型项目、原型、生命周期短的系统 |
工程判断:对于预期生命周期超过两年的系统,严格分层的长期收益通常大于宽松分层的短期便利。透传代码虽然看着多余,但它是一个”结构占位符”——当后续需要在中间层加入缓存、权限检查、审计日志等逻辑时,代码插入点已经存在。宽松分层在原型验证和小型工具中更合理,它的问题不是技术上的,而是组织上的:一旦允许跨层调用,很难阻止团队成员在”方便”的驱动下不断扩大跨层调用的范围。
五、依赖倒置如何改变分层方向
传统分层架构有一个根本性的问题:业务逻辑层依赖数据访问层。这意味着你的核心业务规则——系统中最有价值、最应该稳定的部分——反而依赖于数据库这种基础设施细节。数据库换了,业务逻辑层就得改。
依赖倒置原则(Dependency Inversion Principle, DIP)彻底反转了这个关系。
传统分层的依赖方向
在传统分层中,源码依赖和控制流方向一致,都是从上到下:
表示层 ──依赖──→ 业务逻辑层 ──依赖──→ 数据访问层 ──依赖──→ 数据库
业务逻辑层直接引用数据访问层的具体类。例如:
// 业务逻辑层直接依赖数据访问层的具体实现
public class OrderService {
private final OrderDao orderDao = new MySqlOrderDao();
public void placeOrder(Order order) {
// 业务规则
if (order.getTotal() <= 0) {
throw new IllegalArgumentException("订单金额必须大于零");
}
orderDao.save(order);
}
}问题显而易见:OrderService 直接依赖
MySqlOrderDao。要换数据库,得改
OrderService;要单元测试
OrderService,得启动 MySQL。
依赖倒置后的分层
依赖倒置的核心思想是:高层模块不应该依赖低层模块,二者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。(Robert C. Martin, Agile Software Development, 2002)
具体做法是:在业务逻辑层定义接口(抽象),数据访问层实现这个接口。源码依赖方向从”业务逻辑→数据访问”反转为”数据访问→业务逻辑”:
// 接口定义在业务逻辑层
public interface OrderRepository {
void save(Order order);
Order findById(String orderId);
}
// 业务逻辑层只依赖自己定义的接口
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public void placeOrder(Order order) {
if (order.getTotal() <= 0) {
throw new IllegalArgumentException("订单金额必须大于零");
}
orderRepository.save(order);
}
}// 数据访问层实现业务逻辑层定义的接口
public class MySqlOrderRepository implements OrderRepository {
@Override
public void save(Order order) {
// MySQL 具体实现
}
@Override
public Order findById(String orderId) {
// MySQL 具体实现
return null;
}
}现在依赖方向变了:
┌──────────────────────┐
│ 业务逻辑层 │
│ OrderService │
│ OrderRepository(接口)│
└────────▲─────────────┘
│ 实现(依赖方向向上)
┌────────┴─────────────┐
│ 数据访问层 │
│ MySqlOrderRepository│
└──────────────────────┘
OrderService 不再知道 MySQL
的存在。测试时传入一个内存实现的
OrderRepository,不需要任何数据库。换数据库只需要写一个新的
OrderRepository
实现,业务逻辑层一行代码不用改。
传统分层与依赖倒置分层的对比
下面这张 Mermaid 图展示了两种分层模式的依赖方向差异:
graph TB
subgraph 传统分层
direction TB
P1[表示层] -->|依赖| B1[业务逻辑层]
B1 -->|依赖| D1[数据访问层]
D1 -->|依赖| DB1[(数据库)]
end
subgraph 依赖倒置分层
direction TB
P2[表示层] -->|依赖| B2[业务逻辑层<br/>定义接口]
D2[基础设施层<br/>实现接口] -->|依赖| B2
D2 -->|使用| DB2[(数据库)]
end
style P1 fill:#388bfd,stroke:#388bfd,color:#fff
style B1 fill:#a371f7,stroke:#a371f7,color:#fff
style D1 fill:#3fb950,stroke:#3fb950,color:#fff
style DB1 fill:#636e7b,stroke:#636e7b,color:#fff
style P2 fill:#388bfd,stroke:#388bfd,color:#fff
style B2 fill:#a371f7,stroke:#a371f7,color:#fff
style D2 fill:#3fb950,stroke:#3fb950,color:#fff
style DB2 fill:#636e7b,stroke:#636e7b,color:#fff
左边是传统分层:所有箭头向下,业务逻辑层依赖数据访问层。右边是依赖倒置分层:基础设施层的箭头向上指向业务逻辑层,因为基础设施层实现的是业务逻辑层定义的接口。
这个方向反转的意义在于:业务逻辑层不再依赖任何外部细节,它成了整个系统的核心,所有其他层都依赖它。这正是 Clean Architecture(整洁架构)和六边形架构(Hexagonal Architecture)的核心思想。
从分层到整洁架构
依赖倒置把传统的”从上到下”的依赖链改成了”从外到内”的依赖圈:
┌──────────────────────────────────────┐
│ 基础设施(框架、数据库、外部服务) │
│ ┌──────────────────────────────┐ │
│ │ 接口适配器(Controller、DAO)│ │
│ │ ┌──────────────────────┐ │ │
│ │ │ 应用服务(用例) │ │ │
│ │ │ ┌──────────────┐ │ │ │
│ │ │ │ 领域模型 │ │ │ │
│ │ │ └──────────────┘ │ │ │
│ │ └──────────────────────┘ │ │
│ └──────────────────────────────┘ │
└──────────────────────────────────────┘
依赖方向:由外向内 →
最内层是领域模型,纯业务规则,不依赖任何东西。往外每一层都可以依赖它内部的层,但不能依赖它外部的层。这就是 Robert C. Martin 的依赖规则(Dependency Rule)。
分层架构和整洁架构不是互斥的关系。整洁架构是分层架构在依赖倒置原则指导下的自然演化。关于六边形架构和整洁架构的完整讨论,参见 六边形架构。
六、框架中的分层实现
分层架构之所以长盛不衰,一个重要原因是主流框架直接把分层约定编码进了自己的结构中。开发者不需要从头设计分层——框架已经替你做好了选择。
Spring Boot:Controller-Service-Repository
Spring Boot 是 Java 生态中分层架构最典型的代表。它的约定(Convention)几乎是教科书式的三层结构:
src/main/java/com/example/order/
├── controller/ ← 表示层
│ └── OrderController.java
├── service/ ← 业务逻辑层
│ ├── OrderService.java (接口)
│ └── OrderServiceImpl.java
├── repository/ ← 数据访问层
│ └── OrderRepository.java
├── model/ ← 领域模型(跨层共享)
│ └── Order.java
└── dto/ ← 数据传输对象
└── OrderRequest.java
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping
public ResponseEntity<Order> createOrder(@RequestBody OrderRequest request) {
Order order = orderService.placeOrder(request);
return ResponseEntity.status(HttpStatus.CREATED).body(order);
}
}@Service
public class OrderServiceImpl implements OrderService {
private final OrderRepository orderRepository;
public OrderServiceImpl(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Override
@Transactional
public Order placeOrder(OrderRequest request) {
Order order = new Order();
order.setItems(request.getItems());
order.setTotal(calculateTotal(request.getItems()));
if (order.getTotal() <= BigDecimal.ZERO) {
throw new InvalidOrderException("订单金额必须大于零");
}
return orderRepository.save(order);
}
}public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByCustomerId(Long customerId);
}Spring 的分层有几个值得注意的设计决策:
1. Spring Data JPA 的 Repository
接口本身就是依赖倒置的体现。OrderRepository
接口定义在你的代码里,但实现由 Spring
框架在运行时动态生成。业务逻辑层永远不会直接接触 JDBC 或
Hibernate 的细节。
2. 事务管理在 Service
层。@Transactional 注解放在 Service
方法上,而不是 Repository 或 Controller
上。这是因为一个业务操作可能涉及多个 Repository
调用,事务边界应该和业务操作边界一致。
3.
模型对象的跨层传递是一个争议点。上面的例子中
Order 实体从 Repository 一路传到
Controller。严格分层的做法是每层用不同的数据对象(Entity、Domain
Object、DTO),但这会产生大量映射代码。实际工程中,很多团队选择在
Controller 和 Service 之间做 DTO 转换,但允许 Service 和
Repository 共享 Entity。
Django:MTV 的分层变体
Django 的架构模式叫 MTV:Model-Template-View。它和经典 MVC 的名称对应关系容易混淆:
| Django 术语 | 经典 MVC 对应 | 职责 |
|---|---|---|
| Model | Model | 数据结构、业务规则、数据库交互 |
| Template | View | 页面渲染 |
| View | Controller | 请求处理、业务逻辑编排 |
Django 的目录结构:
order/
├── models.py ← Model 层:ORM 模型 + 业务逻辑
├── views.py ← View 层:请求处理
├── urls.py ← URL 路由
├── templates/ ← Template 层:HTML 渲染
│ └── order/
│ └── detail.html
├── serializers.py ← DRF 序列化(API 场景)
└── admin.py ← 管理后台
# models.py
from django.db import models
class Order(models.Model):
customer_name = models.CharField(max_length=200)
total = models.DecimalField(max_digits=10, decimal_places=2)
created_at = models.DateTimeField(auto_now_add=True)
def is_valid(self):
"""业务规则直接写在 Model 上"""
return self.total > 0
class Meta:
ordering = ['-created_at']# views.py
from django.http import JsonResponse
from django.views import View
from .models import Order
class OrderCreateView(View):
def post(self, request):
order = Order(
customer_name=request.POST['customer_name'],
total=request.POST['total'],
)
if not order.is_valid():
return JsonResponse({'error': '订单金额必须大于零'}, status=400)
order.save()
return JsonResponse({'id': order.id}, status=201)Django 的分层和 Spring Boot
有一个本质差异:Django 的 Model
层同时承担了业务逻辑和数据访问两个职责。Order
既定义了业务规则(is_valid),又直接和数据库交互(save、objects.filter)。Django
的 ORM 不鼓励你在 Model 和数据库之间插入一个独立的
Repository 层。
这是 Django 的有意设计。Django 的哲学是”约定优于配置”(Convention over Configuration)和”不要重复自己”(DRY)。额外的 Repository 层在 Django 看来是不必要的间接性。对于大多数 CRUD 应用,这种简化是合理的——Model 直接和 ORM 绑定减少了代码量,加快了开发速度。
但当业务逻辑变得复杂时,Django 的 Fat Model 模式(把大量逻辑塞进 Model)会导致 Model 类膨胀到几千行。这时候 Django 社区的常见做法是引入 Service 层:
# services.py
from .models import Order, Inventory
class OrderService:
@staticmethod
def place_order(customer_name, items):
total = sum(item.price * item.quantity for item in items)
if total <= 0:
raise ValueError("订单金额必须大于零")
for item in items:
inventory = Inventory.objects.get(product_id=item.product_id)
if inventory.quantity < item.quantity:
raise ValueError(f"库存不足: {item.product_id}")
inventory.quantity -= item.quantity
inventory.save()
order = Order.objects.create(
customer_name=customer_name,
total=total,
)
return order这个 Service 层并非 Django 框架的内置约定,而是工程实践中演化出来的。它在 Django 社区中既不是强制的也不是官方推荐的,但在中大型项目中被广泛使用。
框架分层决策的背后逻辑
Spring Boot 和 Django 对分层的不同选择,反映了两种不同的设计哲学:
| 维度 | Spring Boot | Django |
|---|---|---|
| 默认分层 | 三层(Controller-Service-Repository) | 两层(View-Model) |
| 依赖倒置 | 框架内置(Spring Data JPA) | 需自行实现 |
| 业务逻辑位置 | Service 层 | Model 层(默认) |
| 数据访问抽象 | Repository 接口 | ORM 直接嵌入 Model |
| 扩展分层 | 已经是三层,扩展到四层 | 从两层扩展到三层(加 Service) |
| 适用场景 | 企业级应用、长期维护系统 | 快速开发、内容型应用 |
没有哪种分层策略绝对更好。Spring 的三层结构在系统复杂度高时提供了更好的隔离性;Django 的两层结构在系统简单时减少了不必要的间接性。关键是理解你的框架做了什么假设,以及这些假设在什么条件下会失效。
七、分层架构的反模式
分层架构的概念简单,但实践中有几个反复出现的反模式值得警惕。
千层面架构(Lasagna Architecture)
层太多,每层都薄得几乎没有逻辑。请求从进入系统到触及数据库,中间经过七八层,每层只做一件事:调用下一层。
一个夸张但不少见的例子:
Controller → Facade → Service → Manager → Handler → Processor → Repository → DAO
每一层的代码几乎一样:
// Facade
public Order getOrder(Long id) {
return orderService.getOrder(id);
}
// Service
public Order getOrder(Long id) {
return orderManager.getOrder(id);
}
// Manager
public Order getOrder(Long id) {
return orderHandler.getOrder(id);
}这种代码的问题不是性能——函数调用的开销微乎其微。问题是认知负荷:当你需要理解一个 bug 时,你得跳七八个文件才能找到真正执行逻辑的地方。调试堆栈变成了一长串相同方法名的调用链。
诊断方法:如果一个层的方法中超过 80% 都只是调用下一层的同名方法,这个层大概率不应该存在。
跨层泄漏
下层的实现细节泄漏到上层。最常见的情况是数据库异常(SQLException)从数据访问层一路抛到表示层,或者
HTTP
相关的概念(HttpServletRequest)出现在业务逻辑层。
// 反模式:业务逻辑层感知了 HTTP 概念
public class OrderService {
public void placeOrder(HttpServletRequest request) {
String customerId = request.getParameter("customerId");
// ...
}
}正确做法是每层用自己的数据模型。表示层把 HTTP 请求解析成 DTO,传给业务逻辑层;业务逻辑层用领域对象处理逻辑;数据访问层把领域对象映射成数据库记录。
横切关注点的困境
日志(Logging)、安全(Security)、事务(Transaction)、监控(Monitoring)——这些关注点不属于任何单独一层,而是贯穿所有层。分层架构对横切关注点(Cross-cutting Concerns)没有好的原生解决方案。
常见的应对方式:
- AOP(面向切面编程):Spring 的
@Transactional、@Cacheable就是这个思路。把横切逻辑从业务代码中抽出来,通过切面自动织入。 - 中间件/过滤器链:Django 的 Middleware、Express 的中间件。在请求进入分层结构之前统一处理认证、日志等横切逻辑。
- 装饰器模式:用装饰器包装每层的接口,在不修改层内代码的情况下添加横切逻辑。
这些方案都不完美——AOP 增加了调试难度(堆栈里会多出代理类),中间件只能处理请求级别的横切,装饰器会导致类数量爆炸。但它们是目前工程实践中最可行的方案。
底层逻辑向上蔓延
有时候团队为了”方便”,让数据访问层的逻辑逐渐侵入业务逻辑层。典型症状是 Service 层里出现大量 SQL 片段或 ORM 查询构建逻辑:
// 反模式:Service 层直接构建查询
public class OrderService {
@PersistenceContext
private EntityManager em;
public List<Order> findOrders(String status, LocalDate from, LocalDate to) {
String jpql = "SELECT o FROM Order o WHERE o.status = :status " +
"AND o.createdAt BETWEEN :from AND :to";
return em.createQuery(jpql, Order.class)
.setParameter("status", status)
.setParameter("from", from)
.setParameter("to", to)
.getResultList();
}
}查询构建属于数据访问层。Service 层应该调用
orderRepository.findByStatusAndDateRange(status, from, to),把查询的具体实现封装在
Repository 里。
八、实战案例:用依赖倒置重构三层应用
以一个订单系统为例,演示如何把传统三层架构重构为依赖倒置的分层架构。
重构前:传统三层
controller/
├── OrderController.java → 依赖 OrderService
service/
├── OrderService.java → 依赖 OrderDao(具体类)
dao/
├── OrderDao.java → 依赖 JDBC / MySQL Driver
OrderService 直接依赖 OrderDao
的具体实现:
public class OrderService {
private final OrderDao orderDao;
public OrderService() {
this.orderDao = new OrderDao(); // 直接实例化具体类
}
public Order getOrder(Long id) {
Order order = orderDao.findById(id);
if (order == null) {
throw new OrderNotFoundException(id);
}
return order;
}
public void cancelOrder(Long id) {
Order order = orderDao.findById(id);
if (!order.isCancellable()) {
throw new IllegalStateException("该订单不可取消");
}
order.setStatus(OrderStatus.CANCELLED);
orderDao.update(order);
}
}这段代码的问题:
OrderService直接new OrderDao(),无法替换实现- 单元测试必须连接真实数据库
- 如果要从 MySQL 迁移到 PostgreSQL,需要修改
OrderService
重构步骤
第一步:在业务逻辑层定义接口
// domain/repository/OrderRepository.java
// 这个接口定义在领域层,不在数据访问层
public interface OrderRepository {
Order findById(Long id);
void save(Order order);
void update(Order order);
List<Order> findByCustomerId(Long customerId);
}第二步:数据访问层实现接口
// infrastructure/persistence/JpaOrderRepository.java
@Repository
public class JpaOrderRepository implements OrderRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
public Order findById(Long id) {
return entityManager.find(Order.class, id);
}
@Override
public void save(Order order) {
entityManager.persist(order);
}
@Override
public void update(Order order) {
entityManager.merge(order);
}
@Override
public List<Order> findByCustomerId(Long customerId) {
return entityManager.createQuery(
"SELECT o FROM Order o WHERE o.customerId = :customerId", Order.class)
.setParameter("customerId", customerId)
.getResultList();
}
}第三步:业务逻辑层通过构造器注入依赖
// domain/service/OrderService.java
@Service
public class OrderService {
private final OrderRepository orderRepository;
// 通过构造器注入,依赖的是接口而非具体类
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public Order getOrder(Long id) {
Order order = orderRepository.findById(id);
if (order == null) {
throw new OrderNotFoundException(id);
}
return order;
}
public void cancelOrder(Long id) {
Order order = orderRepository.findById(id);
if (!order.isCancellable()) {
throw new IllegalStateException("该订单不可取消");
}
order.setStatus(OrderStatus.CANCELLED);
orderRepository.update(order);
}
}第四步:调整包结构反映依赖方向
com.example.order/
├── domain/ ← 核心:不依赖任何外部层
│ ├── model/
│ │ ├── Order.java
│ │ └── OrderStatus.java
│ ├── repository/
│ │ └── OrderRepository.java ← 接口定义在领域层
│ └── service/
│ └── OrderService.java
├── infrastructure/ ← 实现层:依赖领域层
│ └── persistence/
│ └── JpaOrderRepository.java
└── api/ ← 表示层:依赖领域层
├── controller/
│ └── OrderController.java
└── dto/
└── OrderRequest.java
重构后的效果
重构前后的依赖方向对比:
重构前:Controller → Service → Dao(全部向下)
重构后:Controller → Domain ← Infrastructure(领域层成为核心)
具体改善:
- 可测试性:
OrderService的单元测试不再需要数据库。传入一个内存实现的OrderRepository即可:
@Test
void cancelOrder_shouldThrowWhenNotCancellable() {
// 用内存实现替代数据库
OrderRepository inMemoryRepo = new InMemoryOrderRepository();
inMemoryRepo.save(new Order(1L, OrderStatus.SHIPPED));
OrderService service = new OrderService(inMemoryRepo);
assertThrows(IllegalStateException.class, () -> service.cancelOrder(1L));
}可替换性:从 JPA 换成 MyBatis,只需要写一个
MyBatisOrderRepository implements OrderRepository,领域层和表示层零修改。领域模型独立性:
Order、OrderStatus、OrderRepository接口都在domain包里,不依赖 Spring、JPA 或任何框架注解。这些类可以直接搬到另一个项目里使用。
九、分层架构的权衡总览
严格分层 vs 宽松分层:多维度对比
| 评估维度 | 严格分层 | 宽松分层 | 评估标准 |
|---|---|---|---|
| 关注点分离 | 强。每层职责清晰,边界硬性约束 | 弱。跨层调用模糊了层间边界 | 修改一层时需要变动多少其他层的代码 |
| 耦合度 | 低。只耦合相邻层 | 中到高。跨层依赖随时间积累 | 用静态分析工具(如 ArchUnit)度量跨层引用数 |
| 可测试性 | 高。Mock 点明确:每层只需 mock 相邻层 | 中。Mock 组合爆炸:一层可能依赖多个下层 | 编写一个层的单元测试需要多少个 mock 对象 |
| 可替换性 | 高。替换一层只影响相邻层 | 低。替换底层需排查所有直接引用的上层 | 替换数据库层时需修改的文件数量 |
| 开发速度(初期) | 慢。需为每层定义接口和契约 | 快。可直接跨层调用,省去中间抽象 | 实现一个简单 CRUD 功能需要创建多少个文件 |
| 维护成本(长期) | 低。变更影响范围可控可预测 | 高。跨层依赖导致变更难以评估影响 | 修改一个数据表结构时需要改动的文件数 |
| 代码量 | 多。中间层的透传代码不可避免 | 少。简单操作省去中间环节 | 项目中无实际逻辑的透传方法占比 |
| 调试体验 | 深。调用栈更长,跳转更多层 | 浅。调用链更短,定位更直接 | 从 HTTP 请求到数据库操作的调用栈深度 |
| 团队协作 | 好。层间接口即契约,可以并行开发 | 差。没有明确接口,容易产生集成冲突 | 多人同时修改同一功能时的合并冲突频率 |
| 性能(进程内) | 无显著差异 | 无显著差异 | 额外函数调用开销在纳秒级 |
| 性能(跨进程) | 额外网络往返 | 减少网络往返 | 每次请求的网络调用次数 |
何时选择哪种方式
严格分层适合:核心业务系统、金融交易系统、需要长期维护(3 年以上)的系统、多团队协作的大型项目。这些场景下,前期多写的接口和透传代码,会在后续维护中节省大量排查和重构时间。
宽松分层适合:内部工具、原型验证、生命周期明确且短(1 年以内)的项目、团队规模很小(1-3 人)的项目。这些场景下,快速交付比长期可维护性更重要。
依赖倒置适合:当你需要对核心业务逻辑进行严格单元测试、当你预期数据存储或外部服务可能更换、当系统复杂度高到需要独立的领域模型时。依赖倒置不是必须的——对于简单 CRUD 应用,传统三层已经够用。引入依赖倒置是有成本的(更多接口、更复杂的包结构、更陡的学习曲线),只有当收益大于成本时才值得做。
十、结论
分层架构存活了五十多年,不是因为它完美,而是因为它直击软件工程最根本的问题:如何让人类大脑应对不断增长的系统复杂性。把系统切成几个职责清晰的水平切片,约束依赖方向为单向流动——这个思路简单到几乎不需要解释,这正是它的力量所在。
但”简单”不等于”随便用”。Layer 和 Tier 的区别决定了你需要操心的问题集完全不同;严格分层和宽松分层的选择直接影响系统的长期维护成本;依赖倒置从根本上改变了层间依赖方向,让业务逻辑从依赖基础设施变成被基础设施依赖。
分层架构最大的风险不是技术问题,而是组织问题。Conway 定律(Conway’s Law)告诉我们,系统的架构会反映组织的沟通结构。如果一个团队按分层组织——前端组、后端组、DBA 组——每一层内部可能很干净,但层间接口会成为沟通瓶颈和互相推诿的温床。这也是为什么微服务运动倾向于按业务能力而非技术层来划分团队。关于微服务的完整讨论,参见 下一篇。
上一篇:单体架构:被严重低估的选择
下一篇:微服务架构深度审视
参考资料
论文与书籍
- Dijkstra, E.W. “The Structure of the ‘THE’-Multiprogramming System.” Communications of the ACM, 1968. 分层思想在操作系统设计中的首次系统性应用。
- Dijkstra, E.W. “On the role of scientific thought.” 1974. 关注点分离(Separation of Concerns)概念的提出。
- Martin, Robert C. Agile Software Development, Principles, Patterns, and Practices. Prentice Hall, 2002. 依赖倒置原则(DIP)的系统阐述。
- Martin, Robert C. Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall, 2017. 整洁架构与依赖规则的完整论述。
- Buschmann, Frank et al. Pattern-Oriented Software Architecture, Volume 1: A System of Patterns. Wiley, 1996. Layers 模式的经典定义,严格分层与宽松分层的形式化描述。
- Richards, Mark. Software Architecture Patterns. O’Reilly, 2015. 分层架构作为最常见架构模式的分析,包括反模式的讨论。
- Evans, Eric. Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley, 2003. DDD 四层架构(UI、Application、Domain、Infrastructure)的定义。
框架与文档
- Spring Framework Reference Documentation, “Web MVC framework” 章节。Controller-Service-Repository 分层约定的官方说明。
- Django Documentation, “Design philosophies” 章节。MTV 模式的设计哲学。
- Spring Data JPA Reference Documentation。Repository 抽象与依赖倒置的框架级实现。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【系统架构设计百科】架构质量属性:不只是"高可用高性能"
需求评审时写下的'高可用、高性能、高并发',到了架构设计阶段几乎无法落地——因为它们不是可执行的需求。本文从 SEI/CMU 的质量属性理论出发,用 stimulus-response 场景模型把模糊需求变成可量化、可验证的架构约束,并拆解属性之间的冲突与联动关系。
【系统架构设计百科】告警策略:如何避免"狼来了"
大多数团队的告警系统都在制造噪声而不是传递信号。阈值告警看似直观,实则产生大量误报和漏报,值班工程师在凌晨三点被叫醒,却发现只是一次无害的毛刺。本文从告警疲劳的工业数据出发,拆解基于 SLO 的多窗口燃烧率告警算法,深入 Alertmanager 的路由、抑制与分组机制,结合 PagerDuty 的告警疲劳研究和真实工程案例,给出一套可落地的告警策略设计方法。
【系统架构设计百科】复杂性管理:架构的核心战场
系统复杂性是架构腐化的根源——本文从 Brooks 的本质复杂性与偶然复杂性划分出发,结合认知负荷理论与 Parnas 的信息隐藏原则,系统阐述复杂性的来源、度量与控制手段,并给出可操作的架构策略
【系统架构设计百科】微服务架构深度审视:优势、代价与适用边界
微服务不是免费的午餐。本文从分布式系统八大谬误出发,拆解微服务真正解决的问题与引入的代价,梳理服务边界划分的工程方法论,还原 Amazon 和 Netflix 从单体到微服务的真实演进时间线,给出微服务适用与不适用的判断框架。