经典三层架构:表现层、业务逻辑层、数据访问层

说到软件分层,三层架构是绕不开的经典。我入行那会儿,第一个参与的企业项目就是基于这个模式。说实话,当时觉得不就是分三个文件夹嘛,后来被现实狠狠教育了一顿——分层的核心不是分文件夹,而是划清职责边界。

这一章,咱们就深入聊聊每一层到底该干什么、不该干什么,以及层与层之间怎么协作才不拧巴。

3.1 表现层(UI Layer)——只做“翻译”

表现层的职责很纯粹:把用户的操作转成请求,把后端的数据转成界面。它不应该知道业务规则,也不该直接操作数据库。

核心原则:表现层只关心“怎么展示”和“怎么交互”,不关心“业务逻辑是什么”。

我在项目中见过最典型的反例:一个 Controller 里写了 200 行 SQL 拼接逻辑。嗯,那代码后来谁都不敢动。表现层一旦掺和业务,维护成本直接翻倍。

表现层该有的样子

  • 接收输入:表单提交、点击事件、API 请求参数
  • 调用业务层:把参数传给 Service,等结果回来
  • 渲染输出:把 Service 返回的数据转成 JSON、HTML 或视图模型
  • 简单校验:比如字段非空、格式检查(但别做业务校验)
我的习惯:表现层只定义 DTO(数据传输对象),绝不引用领域实体。DTO 是给前端看的,实体是给业务层用的,混在一起迟早出事。

3.2 业务逻辑层(Business Logic Layer)——核心战场

这一层是系统的灵魂。所有业务规则、计算、状态流转、权限判断,都应该在这里完成。我把它叫做“不可质疑的决策层”——表现层不能替它做决定,数据层不能替它做判断。

你想想看,如果业务逻辑散落在各个 Controller 或者 SQL 存储过程里,将来改需求的时候,你根本不知道要去哪里改。我曾经接手过一个项目,一个订单折扣规则分散在 7 个地方……那感觉,就像在迷宫里找出口。

业务逻辑层的职责清单

职责 说明 反例
业务规则 比如“VIP 用户满 100 减 20” 在 SQL 里写 CASE WHEN
状态流转 比如“订单已支付 → 已发货” 在前端直接改状态字段
权限校验 比如“只有管理员能删除” 在 DAO 层判断角色
事务管理 保证多个操作要么全成功要么全失败 在表现层开事务
避坑指南:我曾经见过业务层直接返回 Entity 给表现层,结果前端不小心调用了 entity.setXxx(),数据就被污染了。业务层应该返回不可变对象或 DTO,别把“枪”交到外面。

3.3 数据访问层(Data Access Layer)——只做“搬运”

数据访问层,说白了就是跟数据库打交道的。它的任务很简单:CRUD。但简单不代表可以乱来。这一层的核心约束是——不包含任何业务逻辑。

我见过很多项目在 DAO 层里写 if-else 判断用户类型,然后决定查哪张表。这其实已经越界了。数据访问层应该像一台自动售货机:你投币(传参),它出货(返回数据),它不管你是谁、为什么买。

数据访问层的正确姿势

  • 只做数据映射:把数据库表映射成对象,或者把对象写回数据库
  • 提供查询接口:比如 findById、save、delete
  • 不写业务判断:哪怕是一个简单的 if,也要往上提到业务层
  • 不跨表做复杂关联:复杂查询应该在业务层组合,或者在数据库视图里处理
我的习惯:数据访问层的方法名尽量用“数据库语言”,比如 findByName、updateStatus。别用 createOrder 这种业务名——那是业务层该干的事。

3.4 层间依赖关系——谁依赖谁?

三层架构的依赖方向是单向的、自上而下的:

表现层 → 业务逻辑层 → 数据访问层

每一层只依赖它的下一层,绝不反向依赖。更关键的是:每一层都通过接口依赖,而不是具体实现。这样你换数据库、换前端框架,都不会影响其他层。

依赖倒置原则:业务逻辑层定义接口,数据访问层实现接口。这样业务层就不依赖具体的数据源,想换 MySQL 还是 MongoDB,业务层代码一行都不用改。

我早期做项目时,业务层直接 new 了一个 DAO 实现类。后来要加缓存,得改业务层代码。改成接口注入后,加缓存只需要新增一个实现类,业务层完全不知情——这才是分层该有的样子。

3.5 一张图看懂三层架构

下面这张图展示了三层之间的调用关系和边界。注意看箭头方向和数据流动:

经典三层架构关系图 经典三层架构:职责边界与依赖关系 表现层(UI Layer) 职责:接收请求、渲染视图、简单校验 边界:不写业务逻辑、不直接操作数据库 调用 Service 接口 业务逻辑层(BLL Layer) 职责:业务规则、状态流转、权限校验 边界:不依赖具体数据源、不处理视图 调用 DAO 接口 数据访问层(DAL Layer) 职责:CRUD、数据映射、查询接口 边界:不包含业务判断、不跨表复杂关联 依赖方向:自上而下,单向调用。每层通过接口依赖,不依赖具体实现。

3.6 实战中的常见误区

理论说完了,聊聊我踩过的坑。这些误区几乎每个团队都会遇到:

误区 表现 后果 正确做法
层间泄漏 表现层直接调用 DAO 业务逻辑分散,改一处崩一片 所有请求必须经过业务层
贫血业务层 业务层只是 DAO 的传话筒 业务规则散落在各处 业务层要有自己的判断和计算
胖表现层 Controller 里写大量 if-else 难以测试,难以复用 把逻辑下沉到业务层
数据层越界 DAO 里做权限判断 换数据库时业务逻辑丢失 权限判断只放在业务层
我曾经踩过的坑:一个项目里,表现层为了“性能优化”,直接调用了 DAO 的批量查询接口,跳过了业务层的缓存逻辑。结果数据一致性出了问题,用户看到的是脏数据。从那以后,我定了一条铁律:表现层只能调用业务层,不允许跨层调用。

3.7 接口定义在分层中的角色

三层架构能跑得顺,接口是关键。每一层之间的契约,就是接口。接口定义得好,层与层之间就像齿轮一样咬合紧密;定义得不好,改一个接口,上下游全得跟着改。

我个人习惯在业务层先定义接口,再写实现。这样数据访问层和表现层都可以并行开发——只要接口不变,大家各干各的。这也是《接口定义与软件分层协同设计实战》这门课的核心思想:先定契约,再写代码。

一个小技巧:接口的方法名要体现“做什么”,而不是“怎么做”。比如 createOrder 比 insertOrderIntoDB 好得多——前者是业务语义,后者是实现细节。

好了,三层架构的核心内容就这些。记住一句话:每一层都有自己的战场,别越界。表现层管好界面,业务层管好规则,数据层管好存储。各司其职,系统才能健康。