“Controller→Service→DAO”三层架构

前言

后端架构中Controller-Service-DAO三层架构非常常见,因为之前学的C++,没有学Java的Spring Boot等框架,所以还不太了解。现在看了项目里很多代码都是基于这种架构,特地来学习一下。

各层核心职责和作用

这种架构基于单一职责原则设计,核心目标是拆分请求处理,业务逻辑和数据访问的职责。好处是降低代码耦合度,提升可维护性和可测试性。整个三层严格遵循 "单向依赖"(Controller 仅调用 Service,Service 仅调用 DAO,禁止反向或跨层调用)

Controller层

核心职责

接收前端请求,参数校验,调用Service,返回响应

作用

作为前端请求和后端业务逻辑的桥梁,不负责任何业务逻辑,只负责需求转发

价值

  1. 过滤无效参数,拦截无效请求,从而避免消耗Service层资源,减少不必要业务逻辑代码执行;
  2. 统一请求入口,减少前后端交互的复杂性
  3. 统一响应格式,且可以用用户友好度高的方式返回错误处理,避免暴露后端堆栈错误信息或者SQL错误信息等
  4. 防止xss攻击

Service层

核心职责

封装所有的业务逻辑,协调多个DAO

作用

后端的"大脑",是业务规则的实现地,但不直接操作数据库,而是通过DAO获取/修改数据

价值

  1. 将业务规则、计算、判断逻辑集中在Service层实现,实现高内聚;业务逻辑的变化之需要修改Service层
  2. 协调多个资源和多个DAO,可以实现复杂的业务流程

DAO层

核心职责

仅负责与数据库交互,实现数据的 CRUD(增删改查),不包含任何业务逻辑

作用

隔离 "业务逻辑" 与 "数据操作",当数据库(如 MySQL→PostgreSQL)或访问方式(如 JDBC→MyBatis)变化时,仅需修改 DAO 层,不影响 Service 和 Controller。


例子

下面我用 Go 语言实现一个极简的用户登录注册模块,展示 Controller→Service→DAO 三层架构的设计。这个示例包含用户注册和登录两个核心功能,清晰展示了各层的职责划分。

main.go

go 复制代码
package main

import (
	"net/http"
)

func main() {
	// 初始化各层实例
	userDAO := NewUserDAO()
	userService := NewUserService(userDAO)
	userController := NewUserController(userService)

	// 注册路由
	http.HandleFunc("/register", userController.Register)
	http.HandleFunc("/login", userController.Login)

	// 启动服务
	println("Server is running on :8080")
	http.ListenAndServe(":8080", nil)
}

Controller层

go 复制代码
package main

import (
	"encoding/json"
	"net/http"
	"strings"
)

// UserController 处理用户相关的HTTP请求
type UserController struct {
	userService *UserService
}

// NewUserController 创建UserController实例
func NewUserController(service *UserService) *UserController {
	return &UserController{
		userService: service,
	}
}

// Register 处理用户注册请求
func (c *UserController) Register(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")

	// 只允许POST方法
	if r.Method != http.MethodPost {
		w.WriteHeader(http.StatusMethodNotAllowed)
		json.NewEncoder(w).Encode(map[string]string{"error": "只允许POST方法"})
		return
	}

	// 解析请求参数
	var req struct {
		Username string `json:"username"`
		Password string `json:"password"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		w.WriteHeader(http.StatusBadRequest)
		json.NewEncoder(w).Encode(map[string]string{"error": "请求参数格式错误"})
		return
	}

	// 简单的参数校验
	if strings.TrimSpace(req.Username) == "" || strings.TrimSpace(req.Password) == "" {
		w.WriteHeader(http.StatusBadRequest)
		json.NewEncoder(w).Encode(map[string]string{"error": "用户名和密码不能为空"})
		return
	}

	// 调用Service层处理业务逻辑
	user, err := c.userService.Register(req.Username, req.Password)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
		return
	}

	// 返回成功响应
	w.WriteHeader(http.StatusCreated)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"message": "注册成功",
		"user":    user,
	})
}

// Login 处理用户登录请求
func (c *UserController) Login(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")

	// 只允许POST方法
	if r.Method != http.MethodPost {
		w.WriteHeader(http.StatusMethodNotAllowed)
		json.NewEncoder(w).Encode(map[string]string{"error": "只允许POST方法"})
		return
	}

	// 解析请求参数
	var req struct {
		Username string `json:"username"`
		Password string `json:"password"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		w.WriteHeader(http.StatusBadRequest)
		json.NewEncoder(w).Encode(map[string]string{"error": "请求参数格式错误"})
		return
	}

	// 调用Service层处理业务逻辑
	user, err := c.userService.Login(req.Username, req.Password)
	if err != nil {
		w.WriteHeader(http.StatusUnauthorized)
		json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
		return
	}

	// 返回成功响应
	json.NewEncoder(w).Encode(map[string]interface{}{
		"message": "登录成功",
		"user":    user,
	})
}

Service层

go 复制代码
package main

import (
	"errors"
	"golang.org/x/crypto/bcrypt"
)

// UserService 处理用户相关的业务逻辑
type UserService struct {
	userDAO *UserDAO
}

// NewUserService 创建UserService实例
func NewUserService(dao *UserDAO) *UserService {
	return &UserService{
		userDAO: dao,
	}
}

// Register 处理用户注册业务逻辑
func (s *UserService) Register(username, password string) (*User, error) {
	// 检查用户名是否已存在
	existingUser, _ := s.userDAO.FindByUsername(username)
	if existingUser != nil {
		return nil, errors.New("用户名已存在")
	}

	// 密码加密
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return nil, errors.New("密码加密失败")
	}

	// 调用DAO层保存用户
	user := &User{
		Username: username,
		Password: string(hashedPassword),
	}
	return s.userDAO.Save(user)
}

// Login 处理用户登录业务逻辑
func (s *UserService) Login(username, password string) (*User, error) {
	// 调用DAO层查询用户
	user, err := s.userDAO.FindByUsername(username)
	if err != nil || user == nil {
		return nil, errors.New("用户名或密码错误")
	}

	// 验证密码
	err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
	if err != nil {
		return nil, errors.New("用户名或密码错误")
	}

	return user, nil
}

DAO层

go 复制代码
package main

import (
	"errors"
	"sync"
)

// UserDAO 处理用户数据的访问操作
type UserDAO struct {
	users  map[string]*User // 模拟数据库,key为用户名
	nextID int
	mu     sync.Mutex       // 保证并发安全
}

// NewUserDAO 创建UserDAO实例
func NewUserDAO() *UserDAO {
	return &UserDAO{
		users:  make(map[string]*User),
		nextID: 1,
	}
}

// Save 保存用户到数据存储
func (d *UserDAO) Save(user *User) (*User, error) {
	d.mu.Lock()
	defer d.mu.Unlock()

	// 设置ID
	user.ID = d.nextID
	d.nextID++

	// 保存用户
	d.users[user.Username] = user
	return user, nil
}

// FindByUsername 根据用户名查询用户
func (d *UserDAO) FindByUsername(username string) (*User, error) {
	d.mu.Lock()
	defer d.mu.Unlock()

	user, exists := d.users[username]
	if !exists {
		return nil, errors.New("用户不存在")
	}
	return user, nil
}
go 复制代码
package main

// User 用户模型
type User struct {
	ID       int    `json:"id"`
	Username string `json:"username"`
	Password string `json:"-"` // 密码不返回给前端
}
相关推荐
嘉伟桑1 小时前
Python调用电价API返回JSON示例:分时电价、现货电价和字段解析
后端
武子康1 小时前
Shippy:确定性工具、会话级 Sandbox 与 Live-Data Eval(4 类收敛 + 7 步实现方案 + 6 类评测指标)
前端·人工智能·后端
我叫黑大帅1 小时前
我为什么单一消费者的场景下,要用 Redis List 当消息队列?
redis·后端·面试
AskHarries2 小时前
文件上传系统
后端
止语Lab3 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活3 小时前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
程序员天天困3 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn3 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端
用户298698530143 小时前
Python 实现 Excel 与 Markdown 互转的实用指南
后端·python·excel
用户77283104908403 小时前
krono-job:零侵入、单二进制交付的分布式任务调度平台(开源)
后端