分层架构基础:从混沌到秩序的演进之路
说起分层架构,我得先坦白一件事。我入行那会儿,写代码基本就是「一个文件干到底」。业务逻辑、数据库查询、界面渲染,全揉在一起。那时候的项目,说白了就是一堆意大利面条——看着能跑,改起来要命。
后来我慢慢意识到,分层不是一种选择,而是一种必然。当系统复杂度超过某个临界点,不分层,你就等着被自己写的代码反噬吧。
分层架构的演进历史:从混沌到分层
最早期的软件,其实没什么架构可言。我记得2000年初那会儿,很多项目还是C/S架构,客户端直接连数据库。你想想看,那意味着什么?每个用户的电脑上都装着数据库连接字符串,安全性和可维护性几乎为零。
后来大家发现这样不行,于是开始尝试分层。这个演进过程,我把它总结为三个阶段:
- 单体无分层阶段:所有代码堆在一起,修改一个地方可能引发连锁反应。我在一个老项目中见过一个JSP页面写了3000行代码,里面混杂着SQL、HTML和Java逻辑——嗯,那真是噩梦。
- 初步分层阶段:开始把界面和业务逻辑分开,但分层不彻底。比如经典的MVC模式,很多人以为分了Model、View、Controller就是分层了,其实Controller里经常塞满了业务代码。
- 严格分层阶段:明确每一层的职责,层与层之间通过接口通信。这就是我们今天要讲的三层架构的成熟形态。
核心观点:分层架构的本质,是用「层」来隔离变化。每一层只关心自己的事,不越界、不越权。
经典三层架构:表现层、业务层、数据层
经典三层架构,说白了就是把系统切成三块:
| 层次 | 职责 | 常见技术 |
|---|---|---|
| 表现层(UI层) | 用户交互、数据展示、输入校验 | Vue/React、HTML/CSS、Android XML |
| 业务层(Service层) | 业务逻辑、流程编排、规则校验 | Spring Service、领域模型、工作流引擎 |
| 数据层(DAO层) | 数据持久化、缓存、事务管理 | MyBatis、JPA、Redis、JDBC |
这三层的关系,我习惯用一个比喻:表现层是前台接待,业务层是后台经理,数据层是仓库管理员。前台只负责接待客户,不负责审批;经理只负责决策,不负责搬货;仓库管理员只负责存取货物,不负责跟客户聊天。
每一层都只跟相邻层通信。表现层调用业务层,业务层调用数据层。反过来不行——数据层不能直接调用表现层,业务层也不能跳过数据层去操作数据库。
我的经验:我在项目中见过有人把业务逻辑写在Controller里,理由是「这样改起来快」。结果三个月后,同样的逻辑在三个地方出现了不同的实现,bug修都修不完。分层不是束缚,是保护。
分层原则与职责划分:边界就是生命线
分层架构好不好用,关键看边界划得清不清楚。我总结了四条核心原则:
- 单向依赖原则:上层依赖下层,下层不依赖上层。表现层知道业务层的存在,但业务层不应该知道表现层用什么框架。
- 封装原则:每一层的内部实现对外部是黑盒。数据层用MyBatis还是JPA,业务层不需要关心。
- 职责单一原则:每一层只做一件事。表现层只做界面展示和输入校验,不做业务判断。
- 接口隔离原则:层与层之间通过接口通信,而不是直接依赖具体实现。
举个例子,我曾经接手过一个项目,表现层直接调用了数据层的DAO方法。你猜怎么着?后来数据库从MySQL换成PostgreSQL,表现层代码改了一大片。这就是典型的「越界」问题。
避坑指南:我曾经在一个电商项目中,看到业务层直接返回了Entity对象给表现层。结果前端需要展示用户昵称,但Entity里只有userId,表现层不得不又去查一次数据库。正确的做法是:业务层返回DTO(数据传输对象),只包含前端需要的数据。
代码示例:一个简单的三层架构实现
光说不练假把式。我写个简单的用户注册功能,你看看三层是怎么协作的:
// 表现层 - UserController.java
@RestController
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/register")
public Result register(@RequestBody RegisterRequest request) {
// 表现层只做输入校验
if (StringUtils.isBlank(request.getUsername())) {
return Result.error("用户名不能为空");
}
// 调用业务层
return userService.register(request);
}
}
// 业务层 - UserServiceImpl.java
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public Result register(RegisterRequest request) {
// 业务层做业务逻辑判断
if (userDao.existsByUsername(request.getUsername())) {
return Result.error("用户名已存在");
}
// 创建用户对象,调用数据层
User user = new User(request.getUsername(), encryptPassword(request.getPassword()));
userDao.save(user);
return Result.success("注册成功");
}
}
// 数据层 - UserDaoImpl.java
@Repository
public class UserDaoImpl implements UserDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public boolean existsByUsername(String username) {
// 数据层只做数据操作
String sql = "SELECT COUNT(*) FROM users WHERE username = ?";
Integer count = jdbcTemplate.queryForObject(sql, Integer.class, username);
return count > 0;
}
@Override
public void save(User user) {
String sql = "INSERT INTO users (username, password) VALUES (?, ?)";
jdbcTemplate.update(sql, user.getUsername(), user.getPassword());
}
}
你看,每一层的职责都很清晰。表现层不碰数据库,业务层不写SQL,数据层不关心HTTP请求。这就是分层架构的魅力——各司其职,互不干扰。
分层架构的代价与取舍
当然,分层架构不是银弹。它也有代价:
- 代码量增加:每层都要写接口、实现类、DTO,代码量至少翻倍。
- 性能损耗:层与层之间的调用有开销,特别是远程调用时更明显。
- 学习成本:新人需要理解分层思想,否则容易写出「跨层调用」的代码。
但说实话,对于中大型项目来说,这些代价是值得的。我见过太多项目因为不分层,最后重构成本高到直接重写。你想想看,是前期多花点时间分层,还是后期花几倍时间重构?
我的建议:小项目(比如几百行代码的工具类)可以不分层。但一旦项目超过1万行代码,或者有2个以上开发人员参与,请立刻开始分层。这是过来人的血泪教训。
好了,这一章我们聊了分层架构的演进历史、经典三层架构、以及分层原则。下一章我会深入讲状态管理在分层架构中的具体落地——说白了,就是数据怎么在层与层之间流动,又不把代码搞乱。到时候见。