从零搭建三层架构:一个博客项目的架构演进实录

从零搭建三层架构:一个博客项目的架构演进实录

一、当代码开始“长胖”一年前,我接手了一个博客项目的维护工作。打开项目时,我看到了这样的场景:

一个 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 posts = new ArrayList<>();

private static Long idCounter = 1L;

@GetMapping

public List getAllPosts() {

return posts; // 直接返回数据,没有任何业务处理

}

@GetMapping("/{id}")

public ResponseEntity 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 deletePost(@PathVariable Long id) {

// 删除逻辑也写在 Controller 里

posts.removeIf(post -> post.getId().equals(id));

return ResponseEntity.noContent().build();

}

}这段代码的问题一目了然:

Controller 既管请求响应,又管数据存储业务逻辑(如创建文章时的校验)无处安放数据源切换(从内存到数据库)需要改 Controller 代码单元测试无从下手3.3 分层重构第一层:数据访问层(Repository)定义接口,抽象数据操作:

代码语言:javascript复制// 数据访问层接口

public interface PostRepository {

List findAll();

Optional findById(Long id);

Post save(Post post);

void deleteById(Long id);

}接口实现(内存版本,后续可无缝切换为 JPA):

代码语言:javascript复制// 内存实现

@Repository

public class InMemoryPostRepository implements PostRepository {

private final List posts = new ArrayList<>();

private Long idCounter = 1L;

@Override

public List findAll() {

return new ArrayList<>(posts);

}

@Override

public Optional 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)封装核心业务规则:

代码语言:javascript复制@Service

public class PostService {

private final PostRepository postRepository;

// 构造器注入

public PostService(PostRepository postRepository) {

this.postRepository = postRepository;

}

public List getAllPosts() {

return postRepository.findAll();

}

public Optional 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);

}

}第三层:表现层(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 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 deletePost(@PathVariable Long id) {

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 类,也会让代码变得更好一点。

相关任务

365bet网址开户 揭秘小米神秘SN码:揭开它的前世今生
365bet体育平台 [我爱世界杯]20180701 乌拉圭VS葡萄牙
365bet体育投注地 switch美区中文是指哪些国家

switch美区中文是指哪些国家

📅 08-08 👁️ 1585