MVC 模式实践:模型-视图-控制器解耦
MVC 模式实践:模型-视图-控制器解耦
什么是 MVC 模式
MVC(Model-View-Controller)是一种经典软件架构设计模式,最早出现在 Smalltalk 语言中,如今广泛用于 Web 应用、桌面应用和移动开发。它把应用程序划分为三个核心组件:模型(Model)、视图(View) 和 控制器(Controller),旨在通过职责分离实现高内聚、低耦合。
对初学者而言,MVC 的本质就是:让数据处理、界面展示和用户交互各自独立,互不干扰。当你修改界面时不必修改业务逻辑,改变数据存取方式时也不影响显示,这正是“解耦”带来的最大价值。
三个核心组件
模型(Model)—— 数据与业务逻辑的守护者
模型负责:
- 持有应用程序的数据(例如数据库记录、API 返回结果、本地状态)
- 定义业务规则和数据处理逻辑(验证、计算、持久化)
- 不关心数据如何被展示,也不直接与视图通信
模型是可复用的“单一真相来源”。无论多少种不同的界面,只要共用同一个模型,数据就保持一致。
视图(View)—— 用户界面的呈现者
视图负责:
- 将模型中的数据以可视化形式展示给用户
- 监听模型的变化(通过观察者模式或框架机制),自动更新界面
- 不包含任何复杂的业务逻辑,只做纯粹的呈现和简单的格式化
它是一个“哑巴”展示层,所有数据来自模型,所有动作交给控制器。
控制器(Controller)—— 交互的指挥官
控制器负责:
- 接收用户的输入(点击、键盘事件、路由变化等)
- 调用模型上的方法操作数据
- 根据结果决定展示哪个视图或更新视图状态
- 它像一个中间人,连接用户、模型和视图,但自身不存储数据也不渲染界面
MVC 的数据流
典型的 MVC 流程如下(以用户点击按钮为例):
- 用户点击视图上的按钮。
- 视图将点击事件交由对应的控制器方法处理。
- 控制器解析用户意图,调用模型提供的接口(如
addItem())。 - 模型执行逻辑并更新内部数据。
- 模型通知已注册的视图:“我的数据变了”。
- 视图重新从模型获取最新数据并刷新界面。
这一单向依赖关系保证了:视图依赖模型,模型完全不依赖视图;控制器依赖模型和视图,但它们彼此隔离。
动手实践:用 200 行代码实现 MVC
为了让概念落地,我们用一个纯前端 JavaScript 例子来实现一个待办事项管理器,完全遵循 MVC 模式。无需框架,仅用原生 DOM 操作。
1. 创建模型:Model.js
模型持有待办事项列表,并支持增删操作,同时通过自定义事件机制通知视图。
class Model {
constructor() {
this._todos = [
{ id: 1, text: '学习 MVC 模式', completed: false }
];
this._listeners = [];
}
get todos() {
return this._todos;
}
// 订阅变化通知
subscribe(listener) {
this._listeners.push(listener);
}
_notify() {
this._listeners.forEach(fn => fn(this._todos));
}
addTodo(text) {
const newTodo = {
id: Date.now(),
text,
completed: false
};
this._todos.push(newTodo);
this._notify();
}
removeTodo(id) {
this._todos = this._todos.filter(todo => todo.id !== id);
this._notify();
}
}
模型完全不关心界面如何渲染,只提供数据和变更通知。
2. 创建视图:View.js
视图负责生成 DOM,绑定事件时委托给控制器。
class View {
constructor() {
this.app = document.getElementById('app');
this.todoList = this.createElement('ul', 'todo-list');
this.input = this.createElement('input', 'todo-input');
this.input.placeholder = '输入待办事项';
this.addButton = this.createElement('button', 'add-btn');
this.addButton.textContent = '添加';
this.app.append(this.input, this.addButton, this.todoList);
}
createElement(tag, className) {
const el = document.createElement(tag);
if (className) el.className = className;
return el;
}
// 绑定事件给控制器
bindAddTodo(handler) {
this.addButton.addEventListener('click', () => {
if (this.input.value.trim()) {
handler(this.input.value);
this.input.value = '';
}
});
}
bindRemoveTodo(handler) {
this.todoList.addEventListener('click', (e) => {
if (e.target.className === 'remove-btn') {
const id = parseInt(e.target.parentElement.dataset.id);
handler(id);
}
});
}
// 根据模型数据渲染列表
render(todos) {
this.todoList.innerHTML = '';
todos.forEach(todo => {
const li = this.createElement('li');
li.dataset.id = todo.id;
li.textContent = todo.text;
const rmBtn = this.createElement('span', 'remove-btn');
rmBtn.textContent = '✕';
li.appendChild(rmBtn);
this.todoList.appendChild(li);
});
}
}
视图只做渲染和事件传递,任何逻辑都不在此处理。
3. 创建控制器:Controller.js
控制器将模型和视图连接起来,处理用户行为。
class Controller {
constructor(model, view) {
this.model = model;
this.view = view;
// 视图渲染函数订阅模型变化
this.model.subscribe(todos => this.view.render(todos));
// 绑定视图事件
this.view.bindAddTodo(text => this.handleAddTodo(text));
this.view.bindRemoveTodo(id => this.handleRemoveTodo(id));
// 初始渲染
this.view.render(this.model.todos);
}
handleAddTodo(text) {
this.model.addTodo(text);
}
handleRemoveTodo(id) {
this.model.removeTodo(id);
}
}
4. 应用入口
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>MVC 待办事项</title>
<style>
.remove-btn { color: red; cursor: pointer; margin-left: 10px; }
</style>
</head>
<body>
<div id="app"></div>
<script src="Model.js"></script>
<script src="View.js"></script>
<script src="Controller.js"></script>
<script>
new Controller(new Model(), new View());
</script>
</body>
</html>
运行后,添加和删除待办事项都符合 MVC 流程:点击按钮 → 控制器 → 模型更新 → 通知 → 视图更新。你可以换一套 UI 而无需改动模型与控制器的逻辑,这就是解耦的直接体现。
为什么 MVC 能实现解耦?
| 组件 | 依赖项 | 被谁依赖 |
|---|---|---|
| Model | 无视图/控制器依赖 | View,Controller |
| View | 依赖 Model 的数据结构 | Controller |
| Controller | 依赖 Model 接口和 View 方法 | 无人依赖 |
这种单向依赖关系使得每个组件都可以被独立替换和测试。例如:
- 编写单元测试时,可以用假视图和假模型单独测试控制器逻辑。
- 更换前端框架(从 jQuery 到 React)时,只需重写 View,模型和控制器几乎不用动。
- 多人协作时,界面设计师修改 View,后端工程师修改 Model,而控制器的接口约定保证了集成顺利。
注意事项与常见误区
- 模型不是数据库访问层:在很多后端框架(如 ASP.NET MVC)中,Model 常被误解为数据库实体。实际上,模型是业务逻辑的封装,可能包含领域服务、验证、计算等。
- 避免控制器变成“上帝对象”:不要把所有逻辑都堆进控制器,它只做转发和协调。复杂的业务规则一定要下沉到模型中。
- 视图不能直接修改模型:所有数据变更都必须通过控制器调用模型接口,否则会破坏数据流的一致性。
- 解耦不是“完全不通信”:组件之间通过接口、事件或观察者模式通信,但保持依赖关系最弱。
总结
MVC 模式通过将应用拆分为模型、视图和控制器,实现了:
- 清晰的职责划分:每个组件只做自己擅长的事
- 高内聚低耦合:修改底层数据不影响界面,替换界面不影响逻辑
- 可测试性:组件可以独立进行单元测试
- 并行开发:前后端或不同团队可依据接口同步进行
掌握 MVC 思想后,无论是学习像 React、Angular 这样的现代框架,还是自行设计中小型应用,你都能写出结构清晰、易于维护的代码。现在就从修改上面的待办事项例子入手,尝试加入“标记完成”功能,亲手感受 MVC 的魅力吧。