一、当代码开始“长胖”一年前,我接手了一个博客项目的维护工作。打开项目时,我看到了这样的场景:
一个 Controller 文件里塞了 800 多行代码,包含数据库操作、业务逻辑、参数校验、异常处理——所有事情都在同一个文件里解决。每次加一个新功能,都要在同一个文件里找到合适的位置“塞进去”,代码像滚雪球一样越滚越大。
这就是典型的集中式架构问题:功能优先,架构靠后,所有逻辑堆在一起,状态管理混乱,维护成本随着需求迭代指数级上升。
当时我面临的核心问题是:如何让代码结构清晰到每个新人半天就能上手?
答案是:三层架构。
二、什么是三层架构三层架构(Three-Tier Architecture)是一种经典的软件架构模式,将应用程序按职责划分为三个层次:
代码语言:javascript复制┌─────────────────────────────────────────────────────┐
│ 表现层(Presentation Layer / Controller) │
│ 职责:接收用户请求,返回响应,不包含业务逻辑 │
└────────────────────┬────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ 业务逻辑层(Business Logic Layer / Service) │
│ 职责:执行核心业务规则、流程编排、事务管理 │
└────────────────────┬────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ 数据访问层(Data Access Layer / Repository) │
│ 职责:与数据库交互,封装 CRUD 操作 │
└─────────────────────────────────────────────────────┘三层架构的核心价值是职责分离:每一层只关注自己该做的事,层与层之间通过接口通信,互不干扰。
三、重构实战:三层架构落地3.1 项目背景这是一个简易博客系统,支持:
发布文章查看文章列表根据 ID 查看单篇文章删除文章3.2 重构前的代码(反面教材)重构前,所有逻辑集中在 Controller 中:
代码语言:javascript复制// 重构前:BlogController.java - 把所有事情塞在一起
@RestController
@RequestMapping("/api/posts")
public class BlogController {
// 硬编码数据存储——用 static List 代替数据库
private static List
private static Long idCounter = 1L;
@GetMapping
public List
return posts; // 直接返回数据,没有任何业务处理
}
@GetMapping("/{id}")
public ResponseEntity
// 查找逻辑直接写在 Controller 里
for (Post post : posts) {
if (post.getId().equals(id)) {
return ResponseEntity.ok(post);
}
}
return ResponseEntity.notFound().build();
}
@PostMapping
public Post createPost(@RequestBody Post newPost) {
// 创建逻辑也写在 Controller 里
newPost.setId(idCounter++);
posts.add(newPost);
return newPost;
}
@DeleteMapping("/{id}")
public ResponseEntity
// 删除逻辑也写在 Controller 里
posts.removeIf(post -> post.getId().equals(id));
return ResponseEntity.noContent().build();
}
}这段代码的问题一目了然:
Controller 既管请求响应,又管数据存储业务逻辑(如创建文章时的校验)无处安放数据源切换(从内存到数据库)需要改 Controller 代码单元测试无从下手3.3 分层重构第一层:数据访问层(Repository)定义接口,抽象数据操作:
代码语言:javascript复制// 数据访问层接口
public interface PostRepository {
List
Optional
Post save(Post post);
void deleteById(Long id);
}接口实现(内存版本,后续可无缝切换为 JPA):
代码语言:javascript复制// 内存实现
@Repository
public class InMemoryPostRepository implements PostRepository {
private final List
private Long idCounter = 1L;
@Override
public List
return new ArrayList<>(posts);
}
@Override
public Optional
return posts.stream()
.filter(post -> post.getId().equals(id))
.findFirst();
}
@Override
public Post save(Post post) {
if (post.getId() == null) {
post.setId(idCounter++);
posts.add(post);
} else {
// 更新逻辑
deleteById(post.getId());
posts.add(post);
}
return post;
}
@Override
public void deleteById(Long id) {
posts.removeIf(post -> post.getId().equals(id));
}
}第二层:业务逻辑层(Service)封装核心业务规则:
代码语言:javascript复制@Service
public class PostService {
private final PostRepository postRepository;
// 构造器注入
public PostService(PostRepository postRepository) {
this.postRepository = postRepository;
}
public List
return postRepository.findAll();
}
public Optional
return postRepository.findById(id);
}
public Post createPost(Post post) {
// 业务规则:标题不能为空
if (post.getTitle() == null || post.getTitle().trim().isEmpty()) {
throw new IllegalArgumentException("文章标题不能为空");
}
// 业务规则:内容不能为空
if (post.getContent() == null || post.getContent().trim().isEmpty()) {
throw new IllegalArgumentException("文章内容不能为空");
}
// 业务规则:限制敏感词(示例)
if (post.getContent().contains("敏感词")) {
throw new IllegalArgumentException("内容包含违规词汇");
}
return postRepository.save(post);
}
public void deletePost(Long id) {
// 业务规则:检查文章是否存在
if (!postRepository.findById(id).isPresent()) {
throw new IllegalArgumentException("文章不存在");
}
postRepository.deleteById(id);
}
}第三层:表现层(Controller)只负责 HTTP 请求响应,不包含任何业务逻辑:
代码语言:javascript复制@RestController
@RequestMapping("/api/posts")
public class PostController {
private final PostService postService;
// 构造器注入 Service
public PostController(PostService postService) {
this.postService = postService;
}
@GetMapping
public ResponseEntity> getAllPosts() {
return ResponseEntity.ok(postService.getAllPosts());
}
@GetMapping("/{id}")
public ResponseEntity
return postService.getPostById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity> createPost(@RequestBody Post newPost) {
try {
Post saved = postService.createPost(newPost);
return ResponseEntity.status(HttpStatus.CREATED).body(saved);
} catch (IllegalArgumentException e) {
// 业务层抛出的异常在这里被捕获并返回友好信息
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@DeleteMapping("/{id}")
public ResponseEntity
try {
postService.deletePost(id);
return ResponseEntity.noContent().build();
} catch (IllegalArgumentException e) {
return ResponseEntity.notFound().build();
}
}
}3.4 目录结构(按功能组织)重构后的项目结构清晰了很多:
代码语言:javascript复制src/main/java/com/blog/
├── controller/
│ └── PostController.java # 表现层
├── service/
│ ├── PostService.java # 业务逻辑层(接口)
│ └── impl/
│ └── PostServiceImpl.java # 业务逻辑层(实现)
├── repository/
│ └── PostRepository.java # 数据访问层
├── entity/
│ └── Post.java # 实体类
└── dto/
└── PostRequest.java # 请求/响应 DTO四、重构后的收益4.1 对比表维度
重构前
重构后
Controller 行数
80+ 行
~50 行
单一职责
❌ Controller 管所有
✅ 每层各司其职
业务逻辑复用
❌ 无法复用
✅ Service 可被多处调用
数据源切换
❌ 需改 Controller
✅ 改 Repository 实现即可
单元测试难度
❌ 高(需启动 Web 容器)
✅ 低(可独立测试 Service)
4.2 单元测试变得简单重构后,测试 Service 层只需要 mock Repository:
代码语言:javascript复制@ExtndWith(MockitoExtension.class)
class PostServiceTest {
@Mock
private PostRepository repository;
@InjectMocks
private PostService service;
@Test
void createPost_shouldThrowWhenTitleEmpty() {
Post invalidPost = new Post();
invalidPost.setTitle("");
invalidPost.setContent("valid content");
assertThrows(IllegalArgumentException.class,
() -> service.createPost(invalidPost));
}
@Test
void createPost_shouldSaveWhenValid() {
Post validPost = new Post();
validPost.setTitle("Valid Title");
validPost.setContent("Valid Content");
when(repository.save(any(Post.class))).thenReturn(validPost);
Post saved = service.createPost(validPost);
assertNotNull(saved);
verify(repository, times(1)).save(validPost);
}
}五、更进一步的架构分层如果项目规模继续扩大,可以引入更细的分层:
代码语言:javascript复制┌─────────────────────────────────────────────────────┐
│ 表现层:Controller / API Gateway │
└────────────────────┬────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ 应用层:Application Service(编排多个领域服务) │
└────────────────────┬────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ 领域层:Domain Service(核心业务逻辑) │
└────────────────────┬────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ 基础设施层:Repository / MQ / Cache │
└─────────────────────────────────────────────────────┘这种分层在 DDD(领域驱动设计)中更为常见,适合复杂业务场景。
六、总结三层架构的核心不是“分三个文件夹”,而是通过职责分离来降低认知负荷,让每段代码都有明确的归属。当你在写代码时,不妨多问一句:“如果三个月后有人接手这段代码,他能轻松理解并修改吗?”答案就是你架构演进的方向。
对于中小型项目,三层架构已经足够应对 80% 的业务场景。它能带来的收益是实实在在的:降低维护成本、提升可测试性、让新人更快上手。
如果你正在维护一个“能跑就行”的项目,不妨从今天开始,试试用三层架构的思路进行一次小重构——哪怕只抽离一个 Service 类,也会让代码变得更好一点。