第6章 6.5 综合实战:MVC 框架迷你版

🎯 开场 3 分钟:为什么要学这个?

想象一下,你开了一家小餐馆。客人进门点"宫保鸡丁",服务员记下订单,交给厨房,厨房按菜谱做好,端回给客人——这个流程顺畅无比。但如果有一天,厨房突然既要知道"今天食材还剩多少",又要操心"装修风格合不合适",还要记住"哪个服务员负责哪个桌"……这厨房非炸锅不可。

MVC 框架就是餐馆的分工制度:让数据(Model)只管食材,让展示(View)只管摆盘,让协调(Controller)只管传话,各司其职,才能不乱。

上一章我们学了单例、工厂、观察者这些设计模式,这一章要把它们真正用起来——手写一个迷你 MVC 框架。学完你就能理解为什么 Laravel、Flask、Django 这些框架要这样设计,以后读别人代码不再懵。


🧱 基础 25 分钟:核心概念

什么是 MVC?用点餐系统类比

先把这三个字母拆开:

  • Model(模型):数据的"仓库管理员",只管存取数据,不问数据拿来干嘛
  • View(视图):数据的"形象设计\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\n师",只管怎么展示,不问数据从哪来
  • Controller(控制器):数据的"调度员",接收请求、调用模型、选择视图

用一个生活场景理解:你打开外卖 App 点奶茶,Controller 接收"点单"请求,告诉 Model"减一杯库存",Model 改完数据返回给 Controller,Controller 再找 View"渲染订单页面"给你看。

为什么非要用 MVC?

假设不用 MVC,所有代码全塞在一个文件里:
- 某天你要改页面样式 → 得在密密麻麻的代码里找半天
- 某天你要换数据库 → 得挨个改每个用到数据库的地方
- 多人协作 → 两个人改同一个文件,改着改着就冲突了

MVC 让你改哪块就动哪块,别的地方几乎不用动。

迷你 MVC 四件套:Router + Controller + Model + View

一个最小的 MVC 框架需要四样东西:

组件 职责 类比
Router 根据 URL 分配请求 餐厅领位员
Controller 处理业务逻辑 传话的服务员
Model 管理数据和业务规则 仓库管理员
View 负责输出 HTML/JSON 摆盘的设计师

第一步:写一个最简 Router

Router 的活儿很简单:看 URL,告诉框架"这个请求该哪个 Controller 处理"。

class Router:
def __init__(self):
    self.routes = {}

def register(self, path, controller, method):
    """注册路由:path 是路径,controller 是控制器类,method 是处理方法名"""
    self.routes[path] = (controller, method)

def dispatch(self, path, request_data):
    """分发请求:根据 path 找到对应的 controller 和 method 来处理"""
    if path not in self.routes:
        return {"status": 404, "body": "Not Found"}

    controller_class, method_name = self.routes[path]
    controller = controller_class()
    method = getattr(controller, method_name)
    return method(request_data)

解释一下这几行在干嘛:
- routes 是个字典,存"路径"到"处理方法"的映射
- register 负责把映射关系存进去
- dispatch 根据路径找到处理方法并调用

类比:Router 就像电话总机,有人打电话来,总机查通讯录说"这个要找王经理",就把电话转过去。

第二步:写一个基类 Controller

所有控制器都继承同一个基类,好处是统一接口,方便 Router 调用:

from abc import ABC, abstractmethod

class BaseController(ABC):
@abstractmethod
def handle(self, request):
    """处理请求,子类必须实现这个方法"""
    pass

这里用到了抽象基类(上一章没讲,但很实用),意思是"我的子类必须实现 handle 方法,不然就不让你创建实例"。

第三步:写一个 Model 基类

Model 负责数据操作,我们写个基类让它更规范:

class BaseModel:
def __init__(self):
    self.data = {}

def get(self, key, default=None):
    """读取数据,没有就返回默认值"""
    return self.data.get(key, default)

def set(self, key, value):
    """存储数据"""
    self.data[key] = value

这个 BaseModel 现在只是个内存存储,后面换成数据库就改这一层就行。

第四步:写一个 View 基类

View 负责把数据变成人能看的内容:

class BaseView:
def render(self, data):
    """把数据渲染成字符串,默认转 JSON"""
    return str(data)

🔥 实战 35 分钟:3 个递进的小项目

项目 1(5 分钟):跑通"Hello World"级 MVC

先写一个最小可运行的例子,感受一下流程:

# mini_mvc.py

class Router:
def __init__(self):
    self.routes = {}

def register(self, path, controller, method):
    self.routes[path] = (controller, method)

def dispatch(self, path, request_data):
    if path not in self.routes:
        return {"status": 404, "body": "Not Found"}
    controller_class, method_name = self.routes[path]
    controller = controller_class()
    method = getattr(controller, method_name)
    return method(request_data)


class BaseController:
def handle(self, request):
    pass


class HomeController(BaseController):
def handle(self, request):
    return {"status": 200, "body": "欢迎来到首页!"}


class UserController(BaseController):
def handle(self, request):
    return {"status": 200, "body": f"用户 {request.get('name', '匿名')} 你好!"}


# 创建路由并注册
router = Router()
router.register("/", HomeController, "handle")
router.register("/user", UserController, "handle")

# 模拟请求
if __name__ == "__main__":
print(router.dispatch("/", {}))
print(router.dispatch("/user", {"name": "小明"}))

预期输出

{'status': 200, 'body': '欢迎来到首页!'}
{'status': 200, 'body': '用户 小明 你好!'}

这段代码演示了:请求进来 → Router 匹配路径 → 分发到对应 Controller → 返回响应。

项目 2(15 分钟):加一个"待办清单"功能

现在加点真实场景:做一个待办清单,能添加任务、查看任务列表。

# todo_app.py

class Router:
def __init__(self):
    self.routes = {}

def register(self, path, controller, method):
    self.routes[path] = (controller, method)

def dispatch(self, path, request_data):
    if path not in self.routes:
        return {"status": 404, "body": "Not Found"}
    controller_class, method_name = self.routes[path]
    controller = controller_class()
    method = getattr(controller, method_name)
    return method(request_data)


class BaseController:
pass


class TaskModel:
"""数据模型:负责存取任务数据"""
def __init__(self):
    self.tasks = []

def add_task(self, title):
    task_id = len(self.tasks) + 1
    self.tasks.append({"id": task_id, "title": title, "done": False})
    return task_id

def list_tasks(self):
    return self.tasks

def complete_task(self, task_id):
    for task in self.tasks:
        if task["id"] == task_id:
            task["done"] = True
            return True
    return False


class TaskView:
"""视图:负责把任务渲染成 HTML"""
def render_list(self, tasks):
    html = "<h1>我的待办清单</h1><ul>"
    for task in tasks:
        status = "✓" if task["done"] else "○"
        html += f"<li>{status} {task['title']}</li>"
    html += "</ul>"
    return html

def render_success(self, message):
    return f"<p style='color:green'>{message}</p>"


class TaskController(BaseController):
"""控制器:协调 Model 和 View"""
def __init__(self):
    self.model = TaskModel()
    self.view = TaskView()

def add(self, request):
    title = request.get("title", "")
    if not title:
        return {"status": 400, "body": "任务标题不能为空"}
    self.model.add_task(title)
    return {"status": 200, "body": self.view.render_success(f"已添加:{title}")}

def list(self, request):
    tasks = self.model.list_tasks()
    return {"status": 200, "body": self.view.render_list(tasks)}

def complete(self, request):
    task_id = request.get("id")
    if self.model.complete_task(int(task_id)):
        return {"status": 200, "body": self.view.render_success(f"任务 {task_id} 已完成")}
    return {"status": 404, "body": "任务不存在"}


# 初始化应用
router = Router()
task_controller = TaskController()
router.register("/tasks/add", type('AddController', (), {"handle": lambda r: task_controller.add(r)}), "handle")
router.register("/tasks/list", type('ListController', (), {"handle": lambda r: task_controller.list(r)}), "handle")
router.register("/tasks/complete", type('CompleteController', (), {"handle": lambda r: task_controller.complete(r)}), "handle")

# 模拟使用
if __name__ == "__main__":
# 添加任务
router.dispatch("/tasks/add", {"title": "写完 MVC 教程"})
router.dispatch("/tasks/add", {"title": "录制视频"})
# 查看列表
response = router.dispatch("/tasks/list", {})
print(response["body"])
# 完成任务
router.dispatch("/tasks/complete", {"id": 1})
# 再次查看
response = router.dispatch("/tasks/list", {})
print(response["body"])

预期输出

<h1>我的待办清单</h1><ul><li>○ 写完 MVC 教程</li><li>○ 录制视频</li></ul>
<h1>我的待办清单</h1><ul><li>✓ 写完 MVC 教程</li><li>○ 录制视频</li></ul>

看!数据层(Model)只管存任务,视图层(View)只管生成 HTML,控制器(Controller)负责串联它们。哪天你要把 HTML 换成 JSON,只需改 View,Model 和 Controller 完全不用动。

项目 3(15 分钟):做一个"学生成绩统计"工具

组合前两个项目的能力,从 JSON 读取学生成绩,算统计指标,输出报表。

# student_stats.py

import json

class Router:
def __init__(self):
    self.routes = {}

def register(self, path, controller, method):
    self.routes[path] = (controller, method)

def dispatch(self, path, request_data):
    if path not in self.routes:
        return {"status": 404, "body": "Not Found"}
    controller_class, method_name = self.routes[path]
    controller = controller_class()
    method = getattr(controller, method_name)
    return method(request_data)


class StudentModel:
"""数据模型:学生成绩管理"""
def __init__(self):
    self.students = []

def load_from_json(self, json_str):
    """从 JSON 字符串加载数据"""
    self.students = json.loads(json_str)

def get_all(self):
    return self.students

def average_score(self):
    if not self.students:
        return 0
    total = sum(s["score"] for s in self.students)
    return total / len(self.students)

def top_student(self):
    if not self.students:
        return None
    return max(self.students, key=lambda s: s["score"])


class StatsView:
"""视图:输出统计报表"""
def render_report(self, stats):
    report = f"""=== 学生成绩统计报表 ===
平均分:{stats['average']:.1f}
最高分:{stats['max']}
最低分:{stats['min']}
及格人数:{stats['pass_count']}/{stats['total']}
最优学生:{stats['top_student']['name']} ({stats['top_student']['score']}分)
"""
    return report

def render_table(self, students):
    lines = ["姓名    | 分数 | 等级"]
    lines.append("-" * 25)
    for s in students:
        grade = "A" if s["score"] >= 90 else "B" if s["score"] >= 75 else "C" if s["score"] >= 60 else "D"
        lines.append(f"{s['name']:8s}| {s['score']:5d} | {grade}")
    return "\n".join(lines)


class StatsController:
"""控制器:生成统计数据"""
def __init__(self):
    self.model = StudentModel()
    self.view = StatsView()

def load_data(self, json_str):
    self.model.load_from_json(json_str)
    return {"status": 200, "body": "数据加载成功"}

def report(self, request):
    students = self.model.get_all()
    if not students:
        return {"status": 400, "body": "没有学生数据,请先加载"}

    scores = [s["score"] for s in students]
    stats = {
        "average": self.model.average_score(),
        "max": max(scores),
        "min": min(scores),
        "pass_count": len([s for s in students if s["score"] >= 60]),
        "total": len(students),
        "top_student": self.model.top_student()
    }
    return {"status": 200, "body": self.view.render_report(stats)}

def table(self, request):
    students = self.model.get_all()
    return {"status": 200, "body": self.view.render_table(students)}


# 测试数据
test_json = json.dumps([
{"name": "张三", "score": 85},
{"name": "李四", "score": 92},
{"name": "王五", "score": 67},
{"name": "赵六", "score": 78},
{"name": "钱七", "score": 55}
])

# 启动应用
router = Router()
controller = StatsController()
router.register("/stats/load", type('LoadCtrl', (), {"handle": lambda r: controller.load_data(r)}), "handle")
router.register("/stats/report", type('ReportCtrl', (), {"handle": lambda r: controller.report(r)}), "handle")
router.register("/stats/table", type('TableCtrl', (), {"handle": lambda r: controller.table(r)}), "handle")

if __name__ == "__main__":
router.dispatch("/stats/load", test_json)
print(router.dispatch("/stats/report", {})["body"])
print(router.dispatch("/stats/table", {})["body"])

预期输出

=== 学生成绩统计报表 ===
平均分:75.4
最高分:92
最低分:55
及格人数:4/5
最优学生:李四 (92分)

姓名    | 分数 | 等级
-------------------------
张三    |    85 | B
李四    |    92 | A
王五    |    67 | C
赵六    |    78 | B
钱七    |    55 | D

这个项目把 MVC 的优势体现得更明显了:哪天你要从 CSV 文件读数据,只需改 StudentModel.load_from_csv(),View 和 Controller 纹丝不动。


💪 进阶 20 分钟:常见坑 + 性能小贴士

坑 1:Controller 塞了太多逻辑

❌ 错误做法:

class BadController:
def handle(self, request):
    # 业务逻辑全塞这儿了
    data = request["data"]
    result = []
    for item in data:
        if item["type"] == "A":
            item["discount"] = 0.9
        elif item["type"] == "B":
            item["discount"] = 0.8
        # 100 行后……
    return {"status": 200, "body": result}

✅ 正确做法:业务逻辑移到 Model

class OrderModel:
def apply_discount(self, items):
    for item in items:
        if item["type"] == "A":
            item["discount"] = 0.9
        # 只写关键逻辑
    return items

class GoodController:
def __init__(self):
    self.model = OrderModel()

def handle(self, request):
    items = self.model.apply_discount(request["data"])
    return {"status": 200, "body": items}

坑来了:Controller 的活儿是"调度",不是"干活"。如果你的 Controller 超过 20 行,八成要拆分。

坑 2:Model 直接返回数据库裸数据

❌ 错误:

class BadModel:
def get_user(self, id):
    return db.query("SELECT * FROM users WHERE id = ?", id)  # 裸数据,格式不确定

✅ 正确:统一返回格式

class GoodModel:
def get_user(self, id):
    row = db.query("SELECT * FROM users WHERE id = ?", id)
    if not row:
        return None
    return {"id": row[0], "name": row[1], "email": row[2]}  # 固定格式

坑 3:路由匹配用字符串硬编码

❌ 错误:

if path == "/user/add":
# 处理
elif path == "/user/delete":
# 处理

✅ 正确:用字典或装饰器

class Router:
def __init__(self):
    self.routes = {}

def register(self, path, handler):
    self.routes[path] = handler

坑 4:View 返回字符串还拼接 SQL

❌ 这个坑太常见了,说白了就是别在 View 层干数据库的活儿。View 只管渲染,数据格式在 Model 层处理好再给 View。

调试技巧:用日志而不是 print

import logging

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

class TaskController:
def add(self, request):
    logger.debug(f"收到添加任务请求: {request}")
    # 业务逻辑
    logger.info(f"任务添加成功: {title}")

正式项目里,loggingprint 强在哪?它有级别(DEBUG/INFO/WARNING/ERROR)、能写文件、能关掉。养成用 logger 的习惯,以后接大项目不慌。


✏️ 练习题 + 作业题

练习题(5 道,10 分钟内完成)

练习 1(2 分钟):添加一个新路由
- 输入:在项目 1 基础上,新增 /about 路由,返回"这是关于页面"
- 预期输出:{'status': 200, 'body': '这是关于页面'}
- 提示:router.register("/about", ...) 只需要改这一行

练习 2(2 分钟):给任务加"删除"功能
- 输入:在项目 2 基础上,给 TaskModeldelete_task 方法
- 预期输出:删除后列表里没有这个任务了
- 提示:删除可以用 list.remove() 或者列表推导式过滤

练习 3(3 分钟):换个数据源
- 输入:把项目 3 的 load_from_json 改成接收 Python 字典列表(不用 JSON 字符串)
- 预期输出:功能一样,但传入参数变了
- 提示:直接在 Model 里面 self.students = data 就行

练习 4(3 分钟):串联两个小项目
- 输入:把项目 2 的待办清单和项目 3 的统计功能合并,要求任务完成后能统计
- 预期输出:完成任务后,任务从"待办"移到"已完成"列表
- 提示:给 TaskModel 加一个 completed_tasks 列表

练习 5(挑战题,5 分钟):读懂报错
- 输入:以下代码运行后报什么错?

router = Router()
router.register("/test", None, "handle")  # 注意第二个参数是 None
print(router.dispatch("/test", {}))
  • 预期输出:解释为什么会报错
  • 提示:None 不是类,没有 handle 方法

作业题(30 分钟-2 小时)

作业:做一个「迷你博客系统」

  • 需求描述:实现一个极简博客,支持发文章、列表页、详情页
  • 功能点:
    1. POST /articles 新建文章(标题 + 内容)
    2. GET /articles 查看文章列表(显示标题和发布时间)
    3. GET /articles/<id> 查看单篇文章详情
  • 加分项:
    1. 加一个"文章点击量"统计
    2. 用单例模式确保 Model 全局唯一
  • 验收标准:能跑起来 + 3 个路由都能正常返回 + 代码有注释
  • 提交方式:评论区贴代码或 GitHub 链接

📚 总结 + 资源

一句话总结:MVC 把数据(Model)、展示(View)、调度(Controller)分开,让你"改哪块动哪块",代码好维护、好扩展。

3 个核心点
- Router 负责"看到请求 → 找对 Controller"
- Model 负责"数据存取和业务逻辑"
- View 负责"把数据变成人能看的内容"

延伸学习资源
- Flask 官方文档:https://flask.palletsprojects.com/ (真正的 MVC 框架,感受一下工业级代码长啥样)
- 《Python Web 开发实战》:董伟明著,实战派 Web 开发入门书
- Django 官方教程:https://docs.djangoproject.com/zh-hans/ (更完整的 MVC 实现,但比 Flask 重)


互动钩子:学完这章,下次打开抖音/微博,想想它们后台是不是也在用 MVC 类似的架构?在评论区聊聊你的理解,老粉优先回复!

下一章我们要进入第 7 章,会讲到 HTTP 协议与 header——搞懂这个,你就能理解"浏览器和服务器是怎么对话的",为后面写真正的 Web 应用打基础。敬请期待!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。