9、MVC变体:MVP模式:MVP(Model-View-Presenter)模式的定义、与MVC的区别以及适用场景
聊完MVC,咱们得聊聊它的一个“亲兄弟”——MVP。说实话,我在早期做Android开发时,跟MVP打交道的时间比MVC还多。那时候MVC在移动端经常搞得一团糟,Activity既当View又当Controller,代码膨胀得让人头疼。后来引入MVP,才算是把这块硬骨头啃下来了。
9.1 MVP模式的定义
MVP,全称Model-View-Presenter。它把应用拆成三块:
- Model:数据层,跟MVC里的Model一样,负责数据获取、存储和业务逻辑。
- View:视图层,负责UI展示和用户交互。但注意,这里的View比MVC里的View更“薄”——它只做展示,不做决策。
- Presenter:中间人,负责从Model拿数据,然后“指挥”View去渲染。它不直接操作DOM或UI控件,而是通过接口跟View通信。
说白了,MVP的核心思想就是:View只负责“长什么样”,Presenter负责“怎么干”。View和Model完全解耦,谁也不认识谁。
核心区别一句话总结:MVC里View可以偷偷看Model一眼,MVP里View连Model的门朝哪开都不知道。
9.2 MVP与MVC的区别
我经常被问到:“MVP和MVC到底差在哪?” 嗯,这个问题其实挺关键的。咱们直接上对比表:
| 对比维度 | MVC | MVP |
|---|---|---|
| View与Model的关系 | View可以直接访问Model(读数据) | View完全不碰Model,全靠Presenter中转 |
| Controller/Presenter职责 | Controller接收用户输入,更新Model | Presenter既处理用户输入,又负责更新View |
| View的“智商” | View可以包含一些展示逻辑 | View几乎为零逻辑,纯被动渲染 |
| 测试友好度 | 中等(View依赖Model,不好单独测) | 极高(Presenter可脱离UI单独测试) |
| 典型应用场景 | Web后端、传统桌面应用 | Android开发、复杂前端组件 |
你想想看,MVC里View可以直接读Model,这在Web场景下问题不大——反正浏览器端渲染,Model就是数据对象。但在移动端或桌面端,View往往跟UI框架深度绑定,如果View直接依赖Model,那测试就变得非常痛苦。我曾经在一个Android项目里,因为Activity直接操作数据库,导致单元测试跑不起来——每次测试都得启动模拟器,慢得让人抓狂。
MVP解决了这个问题。Presenter不依赖任何UI框架,你可以用纯JUnit测试它。View只需要提供一个接口实现,测试时传个Mock进去就行。
9.3 MVP的工作流程
咱们用一张图来理解MVP的协作方式:
流程其实很清晰:
- 用户点击按钮,View把事件转发给Presenter。
- Presenter调用Model获取数据。
- Model返回数据给Presenter。
- Presenter处理数据,调用View的接口方法更新UI。
注意看,View和Model之间没有直接连线。这就是MVP最核心的设计——单向依赖。Presenter是唯一的“话事人”。
9.4 代码示例:一个登录模块
光说不练假把式。咱们写个登录模块的MVP实现,你就明白它怎么用了。
// 1. 定义View接口
public interface LoginView {
void showLoading();
void hideLoading();
void onLoginSuccess(String username);
void onLoginError(String errorMsg);
}
// 2. 定义Presenter
public class LoginPresenter {
private LoginView view;
private UserModel model;
public LoginPresenter(LoginView view) {
this.view = view;
this.model = new UserModel();
}
public void login(String username, String password) {
view.showLoading();
// 模拟异步请求
new Thread(() -> {
boolean result = model.checkLogin(username, password);
// 切回主线程更新UI
if (result) {
view.onLoginSuccess(username);
} else {
view.onLoginError("用户名或密码错误");
}
view.hideLoading();
}).start();
}
}
// 3. View实现(Android Activity示例)
public class LoginActivity extends Activity implements LoginView {
private LoginPresenter presenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
presenter = new LoginPresenter(this);
findViewById(R.id.btn_login).setOnClickListener(v -> {
String user = getUsername();
String pwd = getPassword();
presenter.login(user, pwd);
});
}
@Override
public void showLoading() { /* 显示进度条 */ }
@Override
public void hideLoading() { /* 隐藏进度条 */ }
@Override
public void onLoginSuccess(String username) { /* 跳转主页 */ }
@Override
public void onLoginError(String msg) { /* 显示错误提示 */ }
}
我的经验:写MVP时,View接口的方法粒度要适中。太细了(比如setText、setColor各一个方法)会导致接口爆炸;太粗了(一个refreshAll方法)又失去了MVP的解耦优势。我一般按“业务意图”来划分——比如showLoading、onLoginSuccess这种,一看就知道是干嘛的。
9.5 适用场景
MVP不是万能的,它有自己的“舒适区”。我总结了几类适合用MVP的场景:
- 复杂UI交互:比如表单校验、多步骤向导。Presenter可以集中管理状态,View只管渲染。
- 需要频繁单元测试:如果你对代码质量要求高,MVP的Presenter可以脱离UI跑测试,爽得很。
- 多端复用:同一套Presenter逻辑,可以对接Web端、移动端、桌面端——只要各自实现View接口就行。
- 团队协作:UI设计师改View,后端改Model,Presenter由核心开发维护,互不干扰。
避坑指南:我曾经在一个小工具项目里强行用MVP,结果代码量翻了一倍。MVP的接口定义和Presenter逻辑确实会增加代码量。如果你的应用逻辑简单(比如纯展示页面),用MVC甚至直接写都行,别为了模式而模式。
9.6 MVP的优缺点
咱们客观一点,MVP有它的好,也有它的“坑”:
| 优点 | 缺点 |
|---|---|
| View和Model完全解耦,可替换性强 | 代码量增加,需要定义大量接口 |
| Presenter可单元测试,不依赖UI环境 | Presenter可能变得臃肿(上帝类) |
| 职责清晰,团队协作效率高 | View接口变更时,Presenter和实现类都得改 |
| 适合复杂交互和状态管理 | 简单场景下显得“杀鸡用牛刀” |
嗯,这里要注意一点:Presenter臃肿的问题。我见过一些项目,Presenter里塞了几千行代码,什么逻辑都往里放。这时候就该考虑拆分了——比如拆成多个小的Presenter,或者引入UseCase层。
9.7 什么时候别用MVP
说实话,MVP不是银弹。以下情况我建议你慎重:
- 原型开发阶段:快速验证想法时,MVP的接口定义会拖慢节奏。
- 简单CRUD页面:就一个列表展示,加个搜索框,用MVP纯属自找麻烦。
- 团队不熟悉:如果团队里没人写过MVP,强行推广会导致代码风格混乱。我见过有人把MVP写成“MVC换皮”——Presenter里直接操作DOM,那还不如用MVC呢。
最后说一句:模式是工具,不是信仰。MVP也好,MVC也罢,选哪个取决于你的场景和团队。我个人习惯是:复杂交互用MVP,简单展示用MVC,数据驱动用MVVM。没有最好的模式,只有最合适的。
公众号:蓝海资料掘金营,微信deep3321