MVP架构深层剖析-从六大设计原则的实现角度到用依赖注入深度解耦

156 阅读7分钟

主流架构模式(如 MVC、MVP、MVVM等)的设计本质上都是为了遵循六大设计原则,解决开发中的耦合、可维护性、可测试性等问题。因此理解六大设计原则是掌握架构模式的基础,本文主要以MVP架构的剖析为主线,解析MVP模式在六大原则中的体现及MVP的解耦优化

MVP基础概念

MVP 架构里,将逻辑,数据,界面的处理划分为三个部分,模型(Model)-视图(View)-控制器(Presenter)。各个部分的功能如下:

  • Model 模型,负责数据的加载和存储。
  • View 视图,负责界面的展示。
  • Presenter 控制器,负责逻辑控制。

mvp

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{

    @Override
    public 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(); // 登录成功时更新UI
    void 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();
    }

    @Override
    public void login() {
        String account = loginView.getAccount();
        String password = loginView.getPassword();
        loginModel.login(account, password, new LoginContract.IModel.LoginCallback() {
            @Override
            public void onSuccess(String msg) {
                loginView.showSuccess(msg);
            }
            @Override
            public void onFailure(String msg) {
                loginView.showFailure(msg);
            }
        });
    }
    @Override
    public 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 {
    @Override
    public LoginContract.IPresenter createPresenter(LoginContract.IView view) {
        return new LoginPresenter(view, new LoginModel());
    }
}

// 3. 扩展另一个工厂(例如测试用的工厂,使用MockModel)
public class TestLoginPresenterFactory implements PresenterFactory {
    @Override
    public LoginContract.IPresenter createPresenter(LoginContract.IView view) {
        return new LoginPresenter(view, new MockLoginModel()); // 使用模拟Model
    }
}

//4. V层代码
	@Override
    protected 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 {
    @Binds
    public 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本身的内容足够作为一篇博客,本篇主要介绍一下解耦思想,不对此进行展开了。