接手过不少“号称用了MVC”的项目,框架是Spring MVC、ASP.NET MVC或者自己搭的路由,目录结构也分了Controller、Service、Model,看起来规规矩矩。但一打开代码,Controller里塞满了业务判断,Service类动辄上千行,Model直接变成了只有getter和setter的“数据袋子”,真正的业务逻辑散落在各个方法里,改一个规则要翻好几个文件。这种项目表面是OOP MVC,实际上已经退化成了“三层大泥球”。这句“OOP MVC(Model负责数据和业务逻辑)”听起来像是教科书里的一句话,但真把它落到代码里的人很少。这篇文章我想把这件事讲透:Model到底是干什么的、为什么业务逻辑应该放在Model里、什么时候你可以不放、以及怎么把已经写烂的代码一步步救回来。适合被Controller和Service层折磨过的后端开发者看,也适合刚接触分层架构、想搞清楚“MVC到底要怎么分”的新手。
1. 先搞清楚:Controller、View、Model这三层分别该干什么
很多人以为MVC就是把代码按Controller、View、Model三个文件夹放好,Controller接收请求、View显示页面、Model操作数据库,完事。这个理解不算错,但漏掉了最关键的分工逻辑。
1.1 Controller的“瘦”是被MVC的结构逼出来的
Controller的本职工作是:接收用户输入、解析请求参数、调用业务处理、把结果交给View去展示。它像一个前台接待员,客人来了登记一下、领到对应的工位,至于工作怎么做,接待员不参与。但实际项目里Controller经常变成“什么都干”的万能层——校验参数、判断权限、计算价格、更新状态、拼接返回数据全堆在里面。一旦业务规则变了,比如满减门槛从100改成200,你得去Controller里翻那个if条件,找到还不一定找得全。
正确做法是Controller只做三件事:
- 把HTTP请求里的参数解析成可用的对象或基本类型
- 调用真正干活的业务入口(可以是一个领域服务,也可以是Model自身的方法)
- 拿到结果后决定渲染哪个View或者返回什么JSON
Controller变瘦之后,最直接的好处是路由层面的逻辑可以单独测试,不依赖业务规则的状态。你想验证某个URL返回什么格式、参数绑定对不对,直接跑接口测试就行,不需要关心背后的价格算法是满100减10还是满200减30。
1.2 View的职责是展示,不是计算
View负责把数据呈现给用户。在Web项目里就是HTML模板,在桌面应用里就是界面控件,在移动端就是页面布局。很多人忽略的一点是:View里不该出现业务规则。像“如果订单金额大于100就显示包邮标签”这种判断,表面上是个展示逻辑,实际上已经涉及定价规则了。将来包邮门槛变成150,除了Controller要改,模板也得跟着改,同一个规则拆在两处,漏改一处就出bug。
展示层能做的事情只有两件:读数据,按条件显示。如果你发现自己不得不在模板里写价格计算、状态拼接、时间格式转换以外的复杂判断,说明后端给View的数据结构设计得不够友好——这时候应该去调整Controller组装的数据结构,而不是往模板里堆逻辑。
1.3 Model在经典MVC里的真实位置
回到MVC最初的Smalltalk定义:Model是“业务对象”和“业务规则”的集合,它不关心自己怎么被显示,也不关心用户怎么操作,它只忠实地表达“业务是什么”。订单就是订单,订单有金额、有状态、能确认、能取消、能计算总价——这些“能做什么”和“怎么算”都是Model自己的事。
后来Web开发流行起来,三层架构(表现层、业务层、数据访问层)和MVC经常被混在一起讲,Model的位置就被挤变了形。很多人把Model理解成了“数据库表的映射”——一个类对应一张表,属性对应字段,仅此而已。这种Model严格来说叫“数据实体”,它承担不了业务逻辑,一有规则就只能往上抛给Service或者Controller。
经典MVC里Model是核心,Controller和View都围绕Model转。Controller修改Model,View观察Model的变化来刷新显示。放到现在的Web开发里,虽然不需要严格实现观察者模式,但分层思想是通用的:业务逻辑应该离数据最近,而不是离HTTP请求最近。数据在哪,规则就该在哪,这既是OOP的封装要求,也是MVC给Model定下的原始职责。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Model的双重身份:既是数据载体,又是业务规则的“家”
标题里“Model负责数据和业务逻辑”不是并列的两件事,而是一件事——数据承载业务状态,业务逻辑保护这个状态的变化过程。把这两者拆开,就会产生最常见的贫血模型问题。
2.1 贫血模型到底哪里不好
贫血模型(Anemic Model)指的是类里只有属性和getter/setter,没有行为。订单类是贫血的,因为它只有一个Order对象、几个属性、无数get/set方法,金额计算在外面、状态流转在外面、能否取消的判断也在外面。这个概念由Martin Fowler提出,他认为这是一种“反模式”。
贫血模型的坏处不是“代码不能跑”,而是规则散了、边界没了。举个真实例子:
php复制// 贫血模型:Order只是一个数据袋子
class Order {
public string $id;
public string $status; // pending / paid / shipped / cancelled
public float $totalAmount;
publ
