土法炼钢兴趣小组的算法知识备份

【系统架构设计百科】分层架构:最古老的模式为何仍然有效

文章导航

分类入口
architecture
标签入口
#layered-architecture#separation-of-concerns#dependency-inversion#Spring#Django

目录

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),又直接和数据库交互(saveobjects.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 增加了调试难度(堆栈里会多出代理类),中间件只能处理请求级别的横切,装饰器会导致类数量爆炸。但它们是目前工程实践中最可行的方案。

底层逻辑向上蔓延

有时候团队为了”方便”,让数据访问层的逻辑逐渐侵入业务逻辑层。典型症状是 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);
    }
}

这段代码的问题:

  1. OrderService 直接 new OrderDao(),无法替换实现
  2. 单元测试必须连接真实数据库
  3. 如果要从 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(领域层成为核心)

具体改善:

  1. 可测试性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));
}
  1. 可替换性:从 JPA 换成 MyBatis,只需要写一个 MyBatisOrderRepository implements OrderRepository,领域层和表示层零修改。

  2. 领域模型独立性OrderOrderStatusOrderRepository 接口都在 domain 包里,不依赖 Spring、JPA 或任何框架注解。这些类可以直接搬到另一个项目里使用。


九、分层架构的权衡总览

严格分层 vs 宽松分层:多维度对比

评估维度 严格分层 宽松分层 评估标准
关注点分离 强。每层职责清晰,边界硬性约束 弱。跨层调用模糊了层间边界 修改一层时需要变动多少其他层的代码
耦合度 低。只耦合相邻层 中到高。跨层依赖随时间积累 用静态分析工具(如 ArchUnit)度量跨层引用数
可测试性 高。Mock 点明确:每层只需 mock 相邻层 中。Mock 组合爆炸:一层可能依赖多个下层 编写一个层的单元测试需要多少个 mock 对象
可替换性 高。替换一层只影响相邻层 低。替换底层需排查所有直接引用的上层 替换数据库层时需修改的文件数量
开发速度(初期) 慢。需为每层定义接口和契约 快。可直接跨层调用,省去中间抽象 实现一个简单 CRUD 功能需要创建多少个文件
维护成本(长期) 低。变更影响范围可控可预测 高。跨层依赖导致变更难以评估影响 修改一个数据表结构时需要改动的文件数
代码量 多。中间层的透传代码不可避免 少。简单操作省去中间环节 项目中无实际逻辑的透传方法占比
调试体验 深。调用栈更长,跳转更多层 浅。调用链更短,定位更直接 从 HTTP 请求到数据库操作的调用栈深度
团队协作 好。层间接口即契约,可以并行开发 差。没有明确接口,容易产生集成冲突 多人同时修改同一功能时的合并冲突频率
性能(进程内) 无显著差异 无显著差异 额外函数调用开销在纳秒级
性能(跨进程) 额外网络往返 减少网络往返 每次请求的网络调用次数

何时选择哪种方式

严格分层适合:核心业务系统、金融交易系统、需要长期维护(3 年以上)的系统、多团队协作的大型项目。这些场景下,前期多写的接口和透传代码,会在后续维护中节省大量排查和重构时间。

宽松分层适合:内部工具、原型验证、生命周期明确且短(1 年以内)的项目、团队规模很小(1-3 人)的项目。这些场景下,快速交付比长期可维护性更重要。

依赖倒置适合:当你需要对核心业务逻辑进行严格单元测试、当你预期数据存储或外部服务可能更换、当系统复杂度高到需要独立的领域模型时。依赖倒置不是必须的——对于简单 CRUD 应用,传统三层已经够用。引入依赖倒置是有成本的(更多接口、更复杂的包结构、更陡的学习曲线),只有当收益大于成本时才值得做。


十、结论

分层架构存活了五十多年,不是因为它完美,而是因为它直击软件工程最根本的问题:如何让人类大脑应对不断增长的系统复杂性。把系统切成几个职责清晰的水平切片,约束依赖方向为单向流动——这个思路简单到几乎不需要解释,这正是它的力量所在。

但”简单”不等于”随便用”。Layer 和 Tier 的区别决定了你需要操心的问题集完全不同;严格分层和宽松分层的选择直接影响系统的长期维护成本;依赖倒置从根本上改变了层间依赖方向,让业务逻辑从依赖基础设施变成被基础设施依赖。

分层架构最大的风险不是技术问题,而是组织问题。Conway 定律(Conway’s Law)告诉我们,系统的架构会反映组织的沟通结构。如果一个团队按分层组织——前端组、后端组、DBA 组——每一层内部可能很干净,但层间接口会成为沟通瓶颈和互相推诿的温床。这也是为什么微服务运动倾向于按业务能力而非技术层来划分团队。关于微服务的完整讨论,参见 下一篇


上一篇:单体架构:被严重低估的选择

下一篇:微服务架构深度审视


参考资料

论文与书籍

框架与文档

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-13 · architecture

【系统架构设计百科】架构质量属性:不只是"高可用高性能"

需求评审时写下的'高可用、高性能、高并发',到了架构设计阶段几乎无法落地——因为它们不是可执行的需求。本文从 SEI/CMU 的质量属性理论出发,用 stimulus-response 场景模型把模糊需求变成可量化、可验证的架构约束,并拆解属性之间的冲突与联动关系。

2026-04-13 · architecture

【系统架构设计百科】告警策略:如何避免"狼来了"

大多数团队的告警系统都在制造噪声而不是传递信号。阈值告警看似直观,实则产生大量误报和漏报,值班工程师在凌晨三点被叫醒,却发现只是一次无害的毛刺。本文从告警疲劳的工业数据出发,拆解基于 SLO 的多窗口燃烧率告警算法,深入 Alertmanager 的路由、抑制与分组机制,结合 PagerDuty 的告警疲劳研究和真实工程案例,给出一套可落地的告警策略设计方法。

2026-04-13 · architecture

【系统架构设计百科】复杂性管理:架构的核心战场

系统复杂性是架构腐化的根源——本文从 Brooks 的本质复杂性与偶然复杂性划分出发,结合认知负荷理论与 Parnas 的信息隐藏原则,系统阐述复杂性的来源、度量与控制手段,并给出可操作的架构策略


By .