MVP架构深层剖析-从六大设计原则的实现角度到用依赖注入深度解耦
主流架构模式(如 MVC、MVP、MVVM等)的设计本质上都是为了遵循六大设计原则,解决开发中的耦合、可维护性、可测试性等问题。因此理解六大设计原则是掌握架构模式的基础,本文主要以MVP架构的剖析为主线,解析MVP模式在六大原则中的体现及MVP的解耦优化
MVP基础概念
MVP 架构里,将逻辑,数据,界面的处理划分为三个部分,模型(Model)-视图(View)-控制器(Presenter)。各个部分的功能如下:
- Model 模型,负责数据的加载和存储。
- View 视图,负责界面的展示。
- Presenter 控制器,负责逻辑控制。
MVP 和 MVC 最大的不同,就是 View 和 Model 不相互持有,都通过 Presenter 做中转。View 产生事件,通知给 Presenter,Presenter 中进行逻辑处理后,通知 Model 更新数据,Model 更新数据后,通知数据结构给 Presenter,Presenter 再通知 View 更新界面。
MVP模块上下层关系
从面向对象设计原则来看MVP架构模式,我们先思考一下MVP三层模块间上下模块关系。答案为View是上层模块,Presenter是中层模块,Model是下层模块,理由如下
-
依赖方向:高层依赖低层,低层 “无感知”
-
高层模块(View)的功能实现必须依赖中间层(Presenter)(比如 View 需要通过 Presenter 触发登录逻辑);
-
中间层(Presenter)的业务协调必须依赖低层(Model)(比如 Presenter 需要调用 Model 的
login()
方法校验账号密码); -
反之,低层模块(Model)完全不依赖任何上层:Model 只关注 “如何处理业务”(如 “账号长度是否≥6 位”“密码是否匹配数据库”),不知道 View 的存在也不需要知道 Presenter 的具体实现
-
-
职责内聚:高层管 “交互”,低层管 “核心”
-
高层(View)的职责是 “与用户交互”,属于 “易变层”:比如 UI 样式可能频繁修改(按钮颜色、输入框位置)、交互逻辑可能调整(比如新增 “验证码登录” 选项),但这些变化不会影响核心业务;
-
低层(Model)的职责是 “核心业务与数据”,属于 “稳定层”:比如登录的核心规则(账号密码校验逻辑、接口请求参数)、数据存储方式(本地 SP 还是数据库),这些一旦确定很少变动;
-
中间层(Presenter)的作用是 “隔离易变与稳定”:当 View 变动时,只需修改 Presenter 与 View 的交互逻辑,无需改动 Model;当 Model 变动时(如换接口),只需修改 Presenter 调用 Model 的代码,无需改动 View,这正是 MVP 解耦的核心价值。
-
MVP 通过接口(View 接口和 Presenter 接口)实现了 OCP。通过依赖接口而非具体实现,使得替换 View 的实现(如用于测试的 Mock View)或扩展 Presenter 的功能变得非常容易,而无需改动现有稳定代码
从面向对象设计原则角度剖析MVP
- 这小节核心从依赖倒置原则出发。高层模块 和 低层模块 都依赖于抽象(接口)。Presenter 依赖于 IView 接口,而不是具体的 Activity。Presenter 通常也依赖于 IMode),而不是具体的网络或数据库操作类。依赖倒置原则是 MVP 架构的核心和灵魂。在传统MVC架构中,Activity(Controller)直接操作 TextView(View)和 HttpClient(Model),这是MVP和MVC架构的核心差异,即MVC架构违反了依赖倒置原则。其他像开闭原则,里氏替换原则都可以从这个角度出发,扩展接口的实现类。
- 从单一职责原则来讲,MVP三层分离,分别处理数据,ui与交互逻辑。
- 从迪米特原则来看,View (Activity) 只知道它的直接朋友 Presenter,也不知道P层的具体实现。Presenter 知道它的两个直接朋友:View 和 Model,但它不知道 View 是一个 Activity 还是一个 Fragment,也不知道 Model 内部的具体实现。这样保证了MVP架构可以单独对P层进行单元测试。
至于接口隔离原则就具体看程序员的实现了。
MVP的一般实现
下面举一个MVP模式的例子
- 契约层,将M,V,P层的接口写到一起防止类膨胀
package com.example.mvctest;public interface LoginContract {public interface IModel {public void login(String account, String password, LoginCallback loginCallback);interface LoginCallback{public void onSuccess(String msg);public void onFailure(String msg);}}public interface IView {public String getAccount ();public String getPassword();public void clearText();public void showSuccess(String msg);public void showFailure(String msg);}public interface IPresenter {public void login();public void clearText();}
}
- Model层,处理数据逻辑。Model层不需要持有Presenter的引用。
public class LoginModel implements LoginContract.IModel{@Overridepublic void login(String account, String password, LoginCallback loginCallback) {new Handler().postDelayed(() -> {if (account.equals("100086") && password.equals("88888888")) {loginCallback.onSuccess("登录成功");} else {loginCallback.onFailure("账号或密码错误");}}, 1500);}
}
- View层
View层是用户界面的抽象,负责展示数据和接收用户输入,并将交互事件转发给Presenter。通常先定义一个接口,再由Activity或Fragment实现。
public interface LoginView {void showLoginSuccess(); // 登录成功时更新UIvoid showLoginFailure(); // 登录失败时更新UI
}
-
Presenter层
Presenter作为Model和View之间的桥梁,接收View的请求,调用Model进行数据处理,并根据结果回调View的方法来更新UI
public class LoginPresenter implements LoginContract.IPresenter{LoginContract.IView loginView;LoginContract.IModel loginModel;public LoginPresenter(LoginContract.IView loginView) {this.loginView = loginView;this.loginModel = new LoginModel();}@Overridepublic void login() {String account = loginView.getAccount();String password = loginView.getPassword();loginModel.login(account, password, new LoginContract.IModel.LoginCallback() {@Overridepublic void onSuccess(String msg) {loginView.showSuccess(msg);}@Overridepublic void onFailure(String msg) {loginView.showFailure(msg);}});}@Overridepublic void clearText() {loginView.clearText();}
}
MVP的解耦优化
工厂模式
不过这里在P层的构造函数中,this.loginModel = new LoginModel();
仍然存在P层和Model层的耦合,从面向对象六大设计原则来讲,高层模块应该依赖抽象而非低层模块。这样直接new显然会导致P层和M层有较高的耦合,但是如果通过构造参数直接获得,会导致V层创建P层引用时又需要new一个M层,这仍会增加耦合。对于小型项目尚可,对于大型项目后期的维护,如果想要换个实现方式或者进行单元测试,P层代码都不能直接达到要求,需要改动,因此就需要看到下面的工厂模式。
我们定义一个用于创建对象的接口,让子类决定实例化哪个类,当我们新增对象创建逻辑时,只需新增工厂类,无需修改已有代码。
// 1. 抽象工厂接口
public interface PresenterFactory {LoginContract.IPresenter createPresenter(LoginContract.IView view);
}// 2. 具体工厂类(实现具体创建逻辑)
public class LoginPresenterFactory implements PresenterFactory {@Overridepublic LoginContract.IPresenter createPresenter(LoginContract.IView view) {return new LoginPresenter(view, new LoginModel());}
}// 3. 扩展另一个工厂(例如测试用的工厂,使用MockModel)
public class TestLoginPresenterFactory implements PresenterFactory {@Overridepublic LoginContract.IPresenter createPresenter(LoginContract.IView view) {return new LoginPresenter(view, new MockLoginModel()); // 使用模拟Model}
}//4. V层代码@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);EdgeToEdge.enable(this);binding = ActivityMainBinding.inflate(getLayoutInflater());setContentView(binding.getRoot());ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);return insets;});LoginContract.IPresenterFactory factory = new LoginPresenterFactory();presenter = factory.createPresenter(this);//通过工厂获得presenter的实例binding.btnLogin.setOnClickListener(this);binding.btnClear.setOnClickListener(this);}
上述代码就可以解决P层和M层耦合的问题。尽管这样又引入了View层和工厂类的耦合,但这种耦合是必要且可控的。相对于P层和M层上下层模块的耦合,工厂类的职责单一且稳定(仅负责创建对象),几乎不会频繁变动。即使变动(如换工厂实现),也只需修改 View 中 “获取 Presenter” 的一行代码,影响范围极小。
依赖注入深层解耦
如果上面的工厂模式的解耦还不满足,我们还可以用**DI(依赖注入)**思想,下面直接使用Hilt
框架进行讲解,不了解hilt框架可以暂时忽略下面的内容。
我们对Model层注入依赖。具体的做法是定义一个抽象方法,里面定义抽象方法(因为对该函数不需要任何实现,我们也不会调用该方法)。该抽象方法的参数是该接口的实例,返回类型是该接口类型,这样就成功注入了依赖。
@Module
@InstallIn(ActivityComponent.class)
public abstract class ModelModul {@Bindspublic abstract LoginContract.IModel bindPresenter(LoginModel loginModel);
}
注入依赖之后在V层定义接口变量的上面加上注解@Inject
即可得到上面注入的LoginModel的实例。
@Inject
public LoginContract.IModel model;
presenter = new LoginPresenter(this, model);
V层获取P层引用时直接传入该Model层变量即可。这样就彻底消除了耦合。如果后面想要换Model层的实现,只需修改抽象方法的参数或者重新加一个抽象方法进行扩展,修改可以完全独立于M,V,P层。加一个抽象方法稍微有些繁琐,需要自定义注解以区分不同的实现,Hilt本身的内容足够作为一篇博客,本篇主要介绍一下解耦思想,不对此进行展开了。