MVP 模式总结

前言

最近学习了 MVP 模式,趁着现在还印象深刻,赶紧写下这篇博客进行记录。这里需要特别提醒一点的是,MVP 应当理解成一种思想,无需局限于某一种实现方式或套路,每个人都可以有自己的 MVP 实现。废话不多说,现在开始进入正文吧!

为什么使用 MVP 模式

在介绍 MVP 之前,我们先来了解下 MVP 的前身——MVC。MVC 的全称是 Model-View-Controller,即 模型 - 视图 - 控制器 的关系。它们之间的关系图如下所示:

MVC
Model:业务模型层,JavaBean 实体类,用于保存实例数据。
View:视图层,程序的 UI 界面,用于向用户展示数据和接收输入的数据。
Controller:控制层,更新 UI 界面和数据实例。

下面举一个简单的例子说明 MVC 模式。
假设我们现在做一个登陆界面,用户输入账号密码之后,如果正确,就用 Toast 提示登陆成功,否则就用 Toast 提示登陆失败。

首先是 Model 层的 User 类,用于保存用户的用户名和密码信息:

public class User {
    
    private String name;
    
    private String pwd;
    
    public User(String name, String pwd){
        this.name = name;
        this.pwd = pwd;
    }

    public String getPwd() {
        return pwd;
    }

    public String getName() {
        return name;
    }
}

接下来是 MainActivity 的实现:

public class MainActivity extends AppCompatActivity {

	// 用户名输入
    private EditText mNameEdit;

	// 用户密码输入
    private EditText mPwdEdit;

	// 登陆按钮
    private Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mNameEdit = (EditText)findViewById(R.id.edit_username);
        mPwdEdit = (EditText)findViewById(R.id.edit_pwd);
        mButton = (Button)findViewById(R.id.btn_login);
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                User user = new User(mNameEdit.getText().toString(), mPwdEdit.getText().toString());
                login(user);
            }
        });
    }

    private void login(User user){
        if(!TextUtils.isEmpty(user.getName()) && !TextUtils.isEmpty(user.getPwd())){
            if(user.getName().equals("Marck") && user.getPwd().equals("123456")){
                Toast.makeText(this, "登陆成功", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(this, "登陆失败", Toast.LENGTH_SHORT).show();
            }
        } else {
            Toast.makeText(this, "用户名或密码不能为空", Toast.LENGTH_SHORT).show();
        }
    }
}

MainActivity 的代码逻辑非常简单,通过两个 EditText 输入用户名和密码之后,按下登陆按钮,触发按钮点击事件,将用户名和密码保存到 User 实例之后交由 login 方法进行判断,从而输出不同的 Toast 信息。这也就是我们平时最为习惯的代码书写方式了。

在这段代码中,Model 层即为我们的 User 类,用于保存用户数据。View 层接收用户的用户名和密码数据以及弹出相应的 Toast 消息。Controller 层则是处理接收到的用户数据,控制着 View 应当弹出什么提示信息。这和我们上图的关系是一致的,即 Model 层获取 View 层接收到的信息,然后 Controller 层对 Model 层的信息进行处理,将处理的结果交由 View 层进行相应的 UI 显示。

但是 MVC 模式存在着几个很明显的缺陷:

  1. View 层和 Controller 层都位于 MainActivity 中,没有实现分离,如果业务逻辑比较复杂的话,很容易造成逻辑混乱。
  2. 代码臃肿,业务量很大的话,同一个文件中的代码会非常的多,难以维护。
  3. 耦合度高,可以看到在 login 方法中(Controller 层)也含有 View 层(Toast),如果发生需求变更的时候维护成本将非常的高昂。

所以在这种情况下,为了使得 View 层和逻辑层实现解耦,我们选择沿用 MVC 模式的改进版——MVP 模式。

什么是 MVP 模式

MVP 模式的全称为 Model - View - Presenter,Presenter 的意思是“任命者”,它是 ModelView 之间的桥梁。它们之间的关系图如下所示:
MVP模式
从上图可以看到,View 层和 Model 层和 MVC 模式的 View 层和 Model 层的含义是一致的,它们之间唯一的区别是 MVP 模式使用的是 Presenter 层而不是 Controller 层。
Presenter 层充当了 View 层和 Model 层之间的桥梁,这样 Model 和 View 就不会直接进行交互了,它们的交互均交由 Presenter 进行处理,也就是说, Presenter 必须同时持有 View 和 Model 的对象。

所以 MVP 模式的核心思想就是:将 Activity 的 UI 逻辑抽象成 View 接口,把业务逻辑抽象成 Presenter 接口,Model 依然是 Model。

说完了概念,我们接下来说就把上面的登陆例子改成 MVP 模式的形式来实战一下吧!

MVP 模式实践

首先我们先分析一下这个登陆需求,跟着核心思想走,Model 依然是 Model,所以位于 Model 层的 User 类也就不需要做任何变化了;接着就是将 Activity 的 UI 逻辑抽象成 View 接口,Activity 的 UI 逻辑有提示信息 Toast,登陆成功和登陆失败的相应响应,将这些抽象成接口的形式;最后是业务逻辑,业务逻辑就一个登陆,将它抽象成 Presenter 接口。然后就开始写代码吧!

项目文件结构如下图所示:
项目结构
我们首先在项目中创建3个子 package,分别命名为modelviewpresenter,这样做的好处是当项目的 Java 文件变多时,我们可以非常清晰地在对应的子包中找出我们需要的文件,而不会产生混乱。然后将我们的 MainActivity 拖进view包中,因为它是属于 View 层的。而我们的model层里面仍然放置 User 类,它是没有变化的。

View 层

接下来我们来创建 View 层的 UI 逻辑接口,按着我们前面的需求分析,我们需要一个 Toast,登陆成功和登陆失败的界面变化,为了简便,我们的登陆成功和登陆失败都使用 Toast 进行提示信息,所以它们都需要一个参数 msg,代码如下所示:

public interface BaseView {
    void showToast(String msg);
    void loginSuccess(String msg);
    void loginFailed(String msg);
}

好了,这样 View 层的接口工作就做完了,接下来看看 Presenter 层的接口定义:

Presenter 层

Presenter 层放置的是业务逻辑代码,在这里我们只有一个业务逻辑,那就是 login,除此之外,我们还需要有绑定视图的接口,既然有绑定,那也就一定有解绑了,所以它的代码如下所示:

public interface BasePresenter {
    void attachView(BaseView baseview);
    void detachView();
    void login(User user);
}

这里需要强调一点的是,attachViewdetachView 并不是我们这个例子特有的接口方法,基本上我们在定义 Presenter 接口的时候,这两个方法都是必不可少的,因为我们需要有一个途径来和 View 层产生联系。

定义完 View 层的 UI 逻辑接口和 Presenter 层的业务逻辑接口之后,我们就需要一个实现类来实现我们 UI 逻辑和业务逻辑的联系了。它是 View 层和 Model 层真正产生交互的地方,我们先上代码:

public class BasePresenserImpl implements BasePresenter {

    private BaseView mBaseView;

    @Override
    public void login(User user) {
        if(!TextUtils.isEmpty(user.getName()) && !TextUtils.isEmpty(user.getPwd())){
            if(user.getName().equals("Marck") && user.getPwd().equals("123456")){
                mBaseView.loginSuccess("登陆成功");
            } else {
                mBaseView.loginFailed("用户名或密码错误!");
            }
        } else {
            mBaseView.showToast("用户名或密码不能为空!");
        }
    }

    @Override
    public void attachView(BaseView v) {
        mBaseView = v;
    }

    @Override
    public void detachView() {
        mBaseView = null;
    }
}

在这个实现类中,我们实现的接口是 BasePresenter,然后实现接口的三个方法,由于我们需要产生和 View 层的交互,所以我们在这里还要维护一个类型为 BaseView 的私有变量。login 方法和 MVC 的 login 方法几乎是一样的,唯一的区别它调用的是 BaseView 相对应的方法,而不再是直接使用 Toast。然后在 attachView 中我们将参数 v 传给私有变量 mBaseView,在 detachView 中就直接将 mBaseView 设置为 null 就好了。至此,我们在 Presnter 层下的工作就完成了,接下来就是将它们应用于 MainActivity 中了。

在 MainActivity 中的使用

MainActivity 是属于 View 接口的实现类,所以 MainActivity 要实现 BaseView 接口,在按下登陆按钮的时候,我们需要有一个登陆的逻辑,所以我们还要维护一个 BasePresenterImpl 类型的私有变量,它的代码如下所示:

public class MainActivity extends AppCompatActivity implements BaseView {

    private Button mLoginButton;

    private EditText mNameEdit;

    private EditText mPwdEdit;

    private BasePresenserImpl basePrensterImpl;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mLoginButton = (Button)findViewById(R.id.btn_login);
        mNameEdit = (EditText)findViewById(R.id.edit_name);
        mPwdEdit = (EditText)findViewById(R.id.edit_pwd);
        basePrensterImpl = new BasePresenserImpl();
        basePrensterImpl.attachView(this);

        mLoginButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                User user = new User(mNameEdit.getText().toString(), mPwdEdit.getText().toString());
                basePrensterImpl.login(user);
            }
        });
    }

    @Override
    public void loginSuccess(String msg) {
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void loginFailed(String msg) {
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void showToast(String msg) {
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        basePrensterImpl.detachView();
    }
}

可以看到,首先我们需要实例化 BasePresenterImpl ,然后调用 attachView 绑定视图,在最后的 onDestroy 方法中千万不要忘记解绑视图,不然会有内存泄漏的风险;在按钮监听事件中,我们获取了用户的账户和密码信息,然后传进 login 方法;最后便是 BaseView 的三个接口方法了,在 showToast 中我们理所当然的就是使用 Toast 提示信息了,而在 loginSuccessloginFailed 中,这里我们只是简单地使用了 Toast 进行提示信息,当然你也可以有其他的 UI 逻辑。

至此,我们的代码就全部写完了,它们的最终结构如下所示:
在这里插入图片描述

我们来总结一下思路:

  1. 首先在 Model 层放置的是 JavaBean 的实体类,用于保存数据,这一点无论是在 MVC 模式还是在 MVP 模式都是一样的。
  2. 然后在 View 层中我们放置的是 View 的接口 BaseViewBaseView 的实现类 MainActivityBaseView 接口中只有关于 UI 逻辑的方法名,不含有任何关于业务逻辑的方法名。
  3. 最后在 Presenter 层中放置的是关于业务逻辑的接口 BasePresenter 和它的实现类 BasePresenterImpl,永远不要忘记 Presenter 层是 Model 层和 View 层之间的桥梁,所以交互也就发生在这里了。

如此,我们也就成功地实现了业务逻辑和视图之间的分离,从我们的例子也可以很清晰地看到这点,如在 MainActivity 中就不会看到任何有关业务逻辑处理的方法。这也就是 MVP 模式的优点所在了。

一个补充

在实际的开发中,我们可能遇到的是不止一个 Activity 的情况,可能有两个甚至多个 Activity,在定义 UI 逻辑接口和业务逻辑接口时,我们可以每个 Activity 创建一个对应的 View 接口和 Presenter 接口,这样做是行得通的,但是不够简便。

我们可以回想一下,在我们的 Presenter 接口中我们说过,每个 Presenter 接口都会有定义绑定视图和解除绑定视图的方法名,如果我们每个接口都写这两个方法,无非是在做大量的重复工作,这里笔者提供的一个思路是:使用继承。

我们可以将 Presenter 层中共有方法提取出来作为一个父类,然后子类继承父类,这样就不用重复地定义相同的方法名了。将我们的例子做一下修改,假设我们现在有两个 Activity,两个 Activity 中都需要用到 Toast,所以我们可以这么定义 BaseView

public interface BaseView {
    void showToast(String msg);
}

接下来是 MainActivityView 接口 MainView,它也就是相当于我们之前的 BaseView了:

public interface MainView extends BaseView{
    void loginSuccess(String msg);
    void loginFailed(String msg);
}

在这里我们将 MainView 继承自 BseView,以获得 showToast 方法。

接下来我们继续定义 BasePresenter 作为父类:

public interface BasePresenter<T extends BaseView> {
    void attachView(T v);
    void detachView();
}

然后我们新建一个接口 MainPresenter,作为 MainActivityPresenter 接口,同样它也就相当于我们之前的 BasePresenter 接口了:

public interface MainPresenter extends BasePresenter<MainView> {
    void login(User user);
}

可能有人注意到了我们在 BasePresenter 中使用到了泛型,这是因为在绑定视图的时候,每个视图的类型不可能是一样的,例如 MainActivity 的视图类型是 MainView,而另外一个 Activity 我们假设它的 View 接口是 SecondView,可以看出它和 MainView 的类型是不一样的,使用泛型可以解决这个问题,而这个泛型我们应该注意到如果不是 BaseView 的子类的话是会报错的。

最后在我们的 Presenter 层的实现类中,我们将 MainActivty 的 Presenter 层实现类改为实现 MainPresenter 接口就好了,代码和之前的是完全没有区别的,而在 MainActivity 中也是直接改由实现 MainView 的接口就可以了。

其他 Activity 下的 View 层和 Presenter 层的书写都是和上面例子一样的这里就不再赘述了。最后我们看一下两个 Activity 的项目结构是怎么样的:
在这里插入图片描述
可以看到还是非常清晰的。

最后我还得重申一点:MVP 模式更多的是一种思想,希望大家不要把它理解成一种套路,每个人都可以有自己的 MVP 实现方式!笔者这里也只是给大家提供一种解决的方法,希望大家能够正确地认识到 MVP 模式带来的益处和价值。

好了,本篇博客到这里就到此结束了,这里只是对 MVP 模式的思想做一个简单的介绍,想要熟练当然还是平常多上手打代码思考,希望大家都能学有所得!

猜你喜欢

转载自blog.csdn.net/qq_38182125/article/details/86345316
今日推荐