Skip to content

Latest commit

 

History

History
479 lines (381 loc) · 12.6 KB

File metadata and controls

479 lines (381 loc) · 12.6 KB

前言

MVC、RESTful、DTO 和 VO,它们本质上是工程实践中的约定/规范,而非编译器强制执行的规则。

编译器不会阻止你违背这些约定,但能通过编译的代码并不一定经得起实际使用的考验,无论是用户体验还是团队协作效率等等。约定/规范的意义正是为了解决这些实际问题。

1. 三层架构

1.1 早期问题

早期的Web应用程序通常将所有代码混合在一起,导致:

  • 代码耦合度高,难以维护
  • 业务逻辑与数据访问混杂
  • 界面展示代码与业务处理代码混合
  • 代码复用性差
  • 团队协作困难

1.2 三层架构的诞生

为解决这些问题,引入了三层架构:

表示层(Presentation Layer) → 用户看到的界面和交互
    ↓
业务层(Business Layer)     → 核心逻辑处理
    ↓
数据访问层(Data Access Layer) → 数据库操作

每层职责明确:

  • 表示层:处理用户交互,展示数据
  • 业务层:实现业务逻辑,处理业务规则
  • 数据访问层:负责数据持久化,与数据库交互

2. MVC模式

2.1 为什么是MVC

三层架构的局限性:
  1. 表示层职责过重:
//伪代码
Presentation UserPresentation {
    init {
        handle UIEvents      // UI事件
        validate Forms      // 表单验证
        process Data        // 数据处理
        manage State        // 状态管理
    }

    render {
        if validateForm() {
            processData()
            updateUI()
        }
    }
}
  1. 层间耦合性强:
//伪代码
Business UserBusiness {
    dependency {
        DataAccess userDataAccess
        Presentation userPresentation
    }

    update(userData) {
        userDataAccess.save(userData)      // 直接调用数据访问
        userPresentation.refresh()         // 直接操作界面
    }										
}

2.2 MVC模式的改进

职责明确分离:

// MVC改进后的结构
//伪代码
Model UserModel {
    update(userData) {
        // 只处理业务逻辑
        validate(userData)
        return repository.save(userData)
    }
}

View UserView {
    render(viewData) {
        // 只负责展示
        display userInfo
        show status
    }
}

Controller UserController {
    route "/user/update" {
        userData = parseRequest()
        user = model.update(userData)
        view.render(user)
    }
}

2.3 MVC图示:

	View (视图)
    ↗         ↖
   ↙           ↘
Controller ←→ Model
(控制器)     (模型)

View: 负责界面展示,将数据呈现给用户
Controller: 处理用户请求,协调Model和View
Model: 包含业务逻辑和数据处理

3. 向前后端分离演进

回到开始所说MVC只是一种约定,M,V,C三者只是抽象概念,view既可以放在后端用模板引擎渲染,也可以放在一个纯前端,使用前后端分离的方式开发。

3.1 传统前后端不分离MVC的局限

// 传统MVC控制器
// 伪代码
Controller UserController {
    route "/user" {
        model.data = getUser()
        return view "user"
    }
}

局限性:

  • 前后端代码强耦合

  • 前端开发受限

  • 难以适应多端需求

  • 服务端渲染性能消耗大

3.2 前后端分离

Frontend (Client)                    Backend (Server)
┌──────────────┐     HTTP/HTTPS     ┌──────────────┐
│   Browser    │ ─────RESTful API─> │   API Layer  │
│              │ <────JSON/Data──── │              │
│  - React     │                    │  - Controller│
│  - Vue       │                    │  - Service   │
│  - Angular   │                    │  - Model     │
└──────────────┘                    └──────────────┘

3.3 MVC在前后端分离中的变化

在传统的MVC架构中,Model 不仅负责数据的定义,还承担了业务逻辑的处理:

  • Model: 定义数据结构,处理业务逻辑
  • View: 展示用户界面
  • Controller: 接收用户请求,操作Model,返回View

然而,随着前后端分离架构的普及,为了进一步分离关注点,提高系统的可维护性和扩展性,引入了Service层。此时,Model 仅负责数据实体的定义,业务逻辑被移动到Service层:

前后端分离后的职责分配:

后端:

  • Controller: 处理API请求,调用Service层
  • Service: 负责业务逻辑的实现
  • Model: 定义数据实体

前端:

  • View: 用户界面展示
  • Controller/ViewModel: 处理用户交互,调用后端API

3.4 RESTful风格

分离之后自然需要新的通信方式,在web中的前后端通信,一般我们遵守RESTful风格。

RESTful(Representational State Transfer,表征状态转移)确定了一种规范,即使用名称明确的 url,和基于 http 标准的 method,来实现对资源读取、更改的标准化。

3.4.1为什是RESTful风格?

在早期,Web服务常用SOAP(Simple Object Access Protocol)协议:

<!-- SOAP请求 -->
POST /services/UserService HTTP/1.1
Host: api.example.com
Content-Type: text/xml; charset=utf-8
SOAPAction: "http://api.example.com/GetUser"

<?xml version="1.0"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
  <soap:Header>
    <!-- 认证信息 -->
    <AuthHeader>
      <Username>admin</Username>
      <Password>secret123</Password>
    </AuthHeader>
  </soap:Header>
  <soap:Body>
    <GetUserRequest>
      <UserId>123</UserId>
    </GetUserRequest>
  </soap:Body>
</soap:Envelope>

<!-- SOAP响应 -->
<?xml version="1.0"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
  <soap:Body>
    <GetUserResponse>
      <User>
        <Id>123</Id>
        <Username>john_doe</Username>
        <Email>john@example.com</Email>
        <Status>Active</Status>
      </User>
    </GetUserResponse>
  </soap:Body>
</soap:Envelope>

问题在于:

  1. 所有操作都使用POST方法,无法利用HTTP方法语义
  2. 每个请求都需要大量XML封装
  3. 需要额外的SOAPAction头来区分操作
  4. 每个操作都需要独立的Request/Response结构定义
  5. 调试困难(XML格式复杂且冗长)

相比之下,RESTful风格:

//RESTful请求
GET /api/users/123 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

//RESTful响应
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 123,
  "username": "john_doe",
  "email": "john@example.com",
  "status": "active"
}

通过:

  1. 使用明确的URL标识资源
  2. 使用HTTP标准方法表达操作
  3. 利用HTTP状态码表示结果

这种设计方式不仅使API更加直观,也充分利用了HTTP协议的特性,现已成为Web API设计的主流方案。

3.5 后端接口(API)示例

GET    /api/users           // 获取用户列表
GET    /api/users/{id}      // 获取指定用户
POST   /api/users           // 创建新用户
PUT    /api/users/{id}      // 更新指定用户
DELETE /api/users/{id}      // 删除指定用户

3.6 前端请求示例

const userApi = {
    getUsers: () => axios.get('/api/users'),
    getUser: (id) => axios.get(`/api/users/${id}`),
    createUser: (user) => axios.post('/api/users', user),
    updateUser: (id, user) => axios.put(`/api/users/${id}`, user),
    deleteUser: (id) => axios.delete(`/api/users/${id}`)
};

4. 前后端分离的优势

  1. 技术栈解耦
    • 前端可以使用最新的框架和工具
    • 后端专注于API提供和业务逻辑
  2. 开发效率提升
    • 前后端团队并行开发
    • 接口文档驱动开发
  3. 更好的用户体验
    • 单页应用(SPA)体验
    • 更快的响应速度
  4. 维护性提升
    • 前后端代码完全分离
    • 便于独立部署和扩展

5. Model层的细化:Entity、DTO和VO

5.1 为什么是Entity、DTO和VO

早期的开发中,常见以下问题:

// 1. 数据库实体直接暴露给前端
app.get('/users/:id', async (req, res) => {
    const user = await db.findUser(req.params.id);
    res.json(user); // 直接返回数据库实体,包含密码等敏感信息
});

// 2. 验证逻辑分散
app.post('/users', async (req, res) => {
    // 验证逻辑直接写在控制器
    if (!req.body.email || !req.body.email.includes('@')) {
        return res.status(400).json({ error: 'Invalid email' });
    }
    // 更多验证...
});

// 3. 展示逻辑混乱
app.get('/users', async (req, res) => {
    const users = await db.findUsers();
    // 在控制器中处理展示逻辑
    const processed = users.map(user => ({
        ...user,
        createdAt: formatDate(user.createdAt),
        password: undefined // 临时处理敏感数据
    }));
    res.json(processed);
});

这导致了:

  1. 安全隐患(敏感数据泄露)
  2. 代码重复(相同的验证/转换逻辑散布各处)
  3. 职责不明确(一个对象承担多个角色)
  4. 维护困难(修改需要在多处同步)

5.2 引入分层概念

为解决这些问题,引入了三种不同职责的对象:

// Entity: 数据库映射
class UserEntity {
    id
    username
    password    // 敏感信息
    email
    createdAt
    lastLoginAt
    loginAttempts
    // ... 其他数据库字段
}

// DTO: 数据传输
class CreateUserDTO {
    username    // 必填
    @min = 6
    password    // 必填,最小长度6
    @regexp = "^[a-zA-Z0-9\\u4e00-\\u9fa5]+$"
    email      // 必填,需验证格式
    // ... 请求参数验证规则
}

// VO: 视图展示
class UserVO {
    username    // 显示名称
    email      // 联系方式
    createdAt  // 格式化的日期
    // 不包含敏感信息
}

5.3 各层的职责和优势

Entity(实体)

  • 完整映射数据库结构
  • 包含所有业务数据
  • 只在服务层内部使用
// 服务层示例
class UserService {
    function createUser(dto: CreateUserDTO) {
        // 业务逻辑
        // dto转entity
        entity = new UserEntity({
            ...dto,
            password: hashPassword(dto.password),
            createdAt: now()
        })
        return database.save(entity)
    }
}

DTO(Data Transfer Object/数据传输对象)

  • 定义接口入参的格式和验证规则
  • 防止非法数据进入系统
  • 统一的数据验证处理
// 控制器层示例
route POST "/users" {
    // 统一的验证处理
    dto = validate(input, CreateUserDTO)
    if (!dto.valid) {
        return error(400, dto.errors)
    }
    
    user = userService.createUser(dto)
    return success(user)
}

VO(View Object/视图对象)

  • 专注于数据展示需求
  • 统一的数据脱敏处理
  • 一致的数据格式化
// 展示层转换示例
function toUserVO(user: UserEntity): UserVO {
    return {
        username: user.username,
        email: user.email,
        createdAt: formatDate(user.createdAt)
        // 注意:没有返回密码等敏感信息
    }
}

5.4 完整流程示例

route POST "/users" {
    // 1. 接收并验证输入(DTO)
    dto = validate(input, CreateUserDTO)
    if (!dto.valid) {
        return error(400, dto.errors)
    }

    // 2. 业务处理(Entity)
    entity = userService.createUser(dto)

    // 3. 转换为展示结果(VO)
    vo = toUserVO(entity)

    return success(vo)
}

重要的是在面对工业项目时始终应该:

  • 明确分离数据库实体和API接口数据结构
  • 不要在服务层之外暴露实体对象
  • 始终验证输入数据
  • 注意敏感数据的处理

最后需要注意的是,这些东西并非一成不变,而是应随着团队需求和业务复杂度动态调整:

  • 小型项目中可能仅使用一个 DTO 简化开发;
  • 在缺少现代 ORM 的情况下,有的团队会权衡使用混合的 Entity 和 DTO 来完成数据库表映射;
  • 对于大型项目,则可能细化对象,甚至专门定义用于查询的 QO(Query Object)。

无论采用何种方式,这些实践的出发点都是解决特定的实际问题,而不是僵化的固定模式。