一年前,我接手了一个博客项目的维护工作。打开项目时,我看到了这样的场景:
一个 Controller 文件里塞了 800 多行代码,包含数据库操作、业务逻辑、参数校验、异常处理——所有事情都在同一个文件里解决。每次加一个新功能,都要在同一个文件里找到合适的位置“塞进去”,代码像滚雪球一样越滚越大。
这就是典型的集中式架构问题:功能优先,架构靠后,所有逻辑堆在一起,状态管理混乱,维护成本随着需求迭代指数级上升。
当时我面临的核心问题是:如何让代码结构清晰到每个新人半天就能上手?
答案是:三层架构。
三层架构(Three-Tier Architecture)是一种经典的软件架构模式,将应用程序按职责划分为三个层次:
┌─────────────────────────────────────────────────────┐
│ 表现层(Presentation Layer / Controller) │
│ 职责:接收用户请求,返回响应,不包含业务逻辑 │
└────────────────────┬────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ 业务逻辑层(Business Logic Layer / Service) │
│ 职责:执行核心业务规则、流程编排、事务管理 │
└────────────────────┬────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ 数据访问层(Data Access Layer / Repository) │
│ 职责:与数据库交互,封装 CRUD 操作 │
└─────────────────────────────────────────────────────┘三层架构的核心价值是职责分离:每一层只关注自己该做的事,层与层之间通过接口通信,互不干扰。
这是一个简易博客系统,支持:
重构前,所有逻辑集中在 Controller 中:
// 重构前:BlogController.java - 把所有事情塞在一起
@RestController
@RequestMapping("/api/posts")
public class BlogController {
// 硬编码数据存储——用 static List 代替数据库
private static List<Post> posts = new ArrayList<>();
private static Long idCounter = 1L;
@GetMapping
public List<Post> getAllPosts() {
return posts; // 直接返回数据,没有任何业务处理
}
@GetMapping("/{id}")
public ResponseEntity<Post> getPostById(@PathVariable Long id) {
// 查找逻辑直接写在 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<Void> deletePost(@PathVariable Long id) {
// 删除逻辑也写在 Controller 里
posts.removeIf(post -> post.getId().equals(id));
return ResponseEntity.noContent().build();
}
}这段代码的问题一目了然:
定义接口,抽象数据操作:
// 数据访问层接口
public interface PostRepository {
List<Post> findAll();
Optional<Post> findById(Long id);
Post save(Post post);
void deleteById(Long id);
}接口实现(内存版本,后续可无缝切换为 JPA):
// 内存实现
@Repository
public class InMemoryPostRepository implements PostRepository {
private final List<Post> posts = new ArrayList<>();
private Long idCounter = 1L;
@Override
public List<Post> findAll() {
return new ArrayList<>(posts);
}
@Override
public Optional<Post> findById(Long id) {
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
public class PostService {
private final PostRepository postRepository;
// 构造器注入
public PostService(PostRepository postRepository) {
this.postRepository = postRepository;
}
public List<Post> getAllPosts() {
return postRepository.findAll();
}
public Optional<Post> getPostById(Long id) {
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);
}
}只负责 HTTP 请求响应,不包含任何业务逻辑:
@RestController
@RequestMapping("/api/posts")
public class PostController {
private final PostService postService;
// 构造器注入 Service
public PostController(PostService postService) {
this.postService = postService;
}
@GetMapping
public ResponseEntity<List<Post>> getAllPosts() {
return ResponseEntity.ok(postService.getAllPosts());
}
@GetMapping("/{id}")
public ResponseEntity<Post> getPostById(@PathVariable Long id) {
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<Void> deletePost(@PathVariable Long id) {
try {
postService.deletePost(id);
return ResponseEntity.noContent().build();
} catch (IllegalArgumentException e) {
return ResponseEntity.notFound().build();
}
}
}重构后的项目结构清晰了很多:
src/main/java/com/blog/
├── controller/
│ └── PostController.java # 表现层
├── service/
│ ├── PostService.java # 业务逻辑层(接口)
│ └── impl/
│ └── PostServiceImpl.java # 业务逻辑层(实现)
├── repository/
│ └── PostRepository.java # 数据访问层
├── entity/
│ └── Post.java # 实体类
└── dto/
└── PostRequest.java # 请求/响应 DTO维度 | 重构前 | 重构后 |
|---|---|---|
Controller 行数 | 80+ 行 | ~50 行 |
单一职责 | ❌ Controller 管所有 | ✅ 每层各司其职 |
业务逻辑复用 | ❌ 无法复用 | ✅ Service 可被多处调用 |
数据源切换 | ❌ 需改 Controller | ✅ 改 Repository 实现即可 |
单元测试难度 | ❌ 高(需启动 Web 容器) | ✅ 低(可独立测试 Service) |
重构后,测试 Service 层只需要 mock Repository:
@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);
}
}如果项目规模继续扩大,可以引入更细的分层:
┌─────────────────────────────────────────────────────┐
│ 表现层:Controller / API Gateway │
└────────────────────┬────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ 应用层:Application Service(编排多个领域服务) │
└────────────────────┬────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ 领域层:Domain Service(核心业务逻辑) │
└────────────────────┬────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ 基础设施层:Repository / MQ / Cache │
└─────────────────────────────────────────────────────┘这种分层在 DDD(领域驱动设计)中更为常见,适合复杂业务场景。
三层架构的核心不是“分三个文件夹”,而是通过职责分离来降低认知负荷,让每段代码都有明确的归属。当你在写代码时,不妨多问一句:“如果三个月后有人接手这段代码,他能轻松理解并修改吗?”答案就是你架构演进的方向。
对于中小型项目,三层架构已经足够应对 80% 的业务场景。它能带来的收益是实实在在的:降低维护成本、提升可测试性、让新人更快上手。
如果你正在维护一个“能跑就行”的项目,不妨从今天开始,试试用三层架构的思路进行一次小重构——哪怕只抽离一个 Service 类,也会让代码变得更好一点。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。