最简单的 MVP 理解

作者: imyyq_star | 来源:发表于2017-07-08 10:27 被阅读311次

    前言

    一个好的软件总是离不开好的架构,不管是前端后端。
    在Android中,我已知的设计模式有:MVC,MVP、MVVM、Clean,其中各自的优劣不再这里展开,有需要的自行Google。
    这里探讨一下MVP,在很多的文章中,都讲很多的概念性的东西,时常把人讲的云里雾里,对于刚接触的人,就算是理解了,怎么实际应用都不知道。
    因此本文就用最简单最常见的来介绍MVP,其实架构是一种很活的东西,谁说你必须使用某种模式?谁规定代码一定要这么写才是对的?我认为只有在变化中能不断适应的,才是王道。难道后来你会了另一种模式,就不能在已有的项目中应用了吗?
    我认为只要是你逻辑清晰,分层合理,你想怎么玩都行,甚至不用任何所谓的模式,注意:前提是分层一定要清晰,层与层之间的界限要清晰明了。

    先不管概念,来一段简单的代码先

    需求:用户输入账号密码,点击登录按钮进行登录。

    代码如下:注意,只是作为示范用。有所删减,看得懂意图就好。

    activity_login.xml:
    如图,xml 的代码就不贴了,很简单。

    LoginActivity.java:

    public class LoginActivity
            extends AppCompatActivity
    {
        private EditText etAccount;
        private EditText etPwd;
    
        @Override
        protected void onCreate(
                @Nullable
                        Bundle savedInstanceState)
        {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_login);
    
            etAccount = (EditText) findViewById(R.id.et_account);
            etPwd = (EditText) findViewById(R.id.et_pwd);
        }
    
        // 响应登录按钮
        public void onLogin(View view)
        {
            String account = etAccount.getText().toString();
            String pwd = etPwd.getText().toString();
    
            // TODO 这里省掉了空判断
    
            // 发起请求
            RequestParams params = new RequestParams();
            params.add("account", account);
            params.add("pwd", pwd);
            new AsyncHttpClient().get("url", params, new Login());
        }
    
        // 登录请求回调
        private class Login
                extends AsyncHttpResponseHandler
        {
            @Override
            public void onSuccess(int statusCode, Header[] headers, byte[] responseBody)
            {
                if (responseBody != null)
                {
                    LoginResponse response = JSON.parseObject(new String(responseBody),
                            LoginResponse.class);
                    if (response != null)
                    {
                        if (response.getStatus() == 0)
                        {
                            Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show();
    
                            // TODO 去到主界面之类的
    
                            // 然后结束掉登录
                            finish();
                        }
                        else
                        {
                            Toast.makeText(LoginActivity.this, "登录失败," + response.getMsg(),
                                    Toast.LENGTH_SHORT).show();
                        }
                    }
                }
                else
                {
                    onFailure(statusCode, headers, null, null);
                }
            }
    
            @Override
            public void onFailure(int statusCode, Header[] headers, byte[] responseBody,
                                  Throwable error)
            {
                Toast.makeText(LoginActivity.this, "登录失败,请检查网络", Toast.LENGTH_SHORT).show();
            }
        }
    }
    

    很简单吧?就是输入和发起登录。
    其中网络库使用的是:
    android-async-http
    JSON解析使用的是:
    FastJSON Android版本

    分析,以上代码一共分为多少层?有什么缺陷?

    • UI层(View):界面的显示,控件的绑定和操作,用户的输入和操作,都属于UI层需要处理的。比如上述代码中的findViewById,按钮响应,Toast,跳转到其他Activity等操作。在Android中,Activity、Fragment都属于View。
    • 业务逻辑层(Presenter):登录请求的发起,结果的接收和处理,通知UI层界面更新,都属于业务逻辑的范围。比如上述代码中的请求发起和JSON解析,判断等。
    • 数据层(Model):去服务器请求数据,这里不只是云服务器的请求,数据库,文件,智能设备,任何数据源,只要是增删改查的,都属于数据层的工作。比如上述代码的网络库异步请求。

    从分析来看,上述一个简单的需求实际上有三个层的存在,而却全部写在View中,对于新手来说,这样类似的代码是再正常不过了。
    一般来说,简单的需求,项目小,这样写也不会造成什么问题的,但是一旦项目越来越大,并且需求改动也越来越多的时候,就成了一种灾难了,比如无休止的复制和粘贴。

    举个例子:
    现在项目中加入了启动页,要求在启动页判断先前是否已有用户登录过,如果有,则取出账号密码进行登录,登录成功去到主界面,失败则去到登录页;
    如果没有,直接跳转到登录页。

    再用上面的写法,也就是加个启动页,然后复制登录的那段代码,再改改回调处理的,听起来好像没事,但不觉得重复了吗?

    使用MVP模式重写

    先看一张类图


    你肯定会说:什么?一个简单的功能,居然需要这么多类文件,这不是更加烦琐,工作量更加大了吗?
    别急,继续看。下面我们就按上面分析的来写。

    LoginResponse.java :JSON 解析需要的数据类

    public class LoginResponse
    {
        private int status;
        private String msg;
    
        ...省略掉 set/get
    }
    

    再来看两个 Base 类:

    BasePresenter.java:

    /**
     * 所有Presenter的父接口
     */
    public interface BasePresenter
    {
        // TODO 在这里可以声明一些Presenter的通用方法
    }
    

    BaseView.java:

    /**
     * 所有View的父接口
     */
    public interface BaseView
    {
        // TODO 在这里可以声明一些View的通用方法
    }
    

    不知道定义两个 Base 是用来干嘛的,没关系,再来思考关于这个登录界面的两个问题:

    • 1.需要我们处理的用户操作有哪些?
    • 2.界面�显示相关的,需要我们做的有哪些?

    针对以上问题,解答如下:

    • 1.只有登录需要我们处理,其他的诸如输入账号密码,点击按钮这种操作�不需要我们做。至于账号密码的空判断,已经包含在登录这个操作里了。
    • 2.点击登录按钮后,需要显示进度条登录成功需要显示成功或直接去到主页面之类的,登录失败需要隐藏进度条,�提示登录失败的原因之类的。

    因此我们可以�把这些操作和显示都归类到一个地方,称为契约类(Contract)

    LoginContract.java:

    /**
     * 登录契约类,声明了View和Presenter该有的操作,方便管理
     */
    public interface LoginContract
    {
        // 定义界面中所有的 UI 状态
        interface View
                extends BaseView
        {
            void loginSuccess(); // 登录成功
    
            void loginFailure(String msg); // 登录失败
    
            void showLoading(boolean isShowLoading); // 是否显示加载中
        }
    
        // 定义了所有的用户操作
        interface Presenter
                extends BasePresenter
        {
            void login(String account, String pwd); // 登录
        }
    }
    

    定义契约类的目的是方便管理,也能理清你的逻辑。

    好了,�以上都是准备工作,实际的 Model、View、Presenter 相关的具体类还没写。继续看。

    Model:LoginRequest.java

    public final class LoginRequest
    {
        // 单例
        private LoginRequest()
        {
        }
    
        private static class SingletonHolder
        {
            private static final LoginRequest SINGLETON = new LoginRequest();
        }
    
        public static LoginRequest getInstance()
        {
            return SingletonHolder.SINGLETON;
        }
    
        public void login(String account, String pwd, final LoginCallback callback)
        {
            // 发起请求
            RequestParams params = new RequestParams();
            params.add("account", account);
            params.add("pwd", pwd);
            new AsyncHttpClient().get("url", params, new AsyncHttpResponseHandler()
            {
                @Override
                public void onSuccess(int statusCode, Header[] headers, byte[] responseBody)
                {
                    callback.onSuccess(statusCode, responseBody);
                }
    
                @Override
                public void onFailure(int statusCode, Header[] headers, byte[] responseBody,
                                      Throwable error)
                {
                    callback.onFailure(statusCode, responseBody, error);
                }
            });
        }
    
        // 对外暴露的接口
        public interface LoginCallback
        {
            void onFailure(int statusCode, byte[] responseBody, Throwable error);
    
            void onSuccess(int statusCode, byte[] responseBody);
        }
    }
    

    Model 类不负责逻辑的处理,只是负责增删改查,以及必要的保存住自己的状态,比如你这个 Model 表示一个智能开关设备,那么开关的状态你得保存起来,以便�状态改变的时候发出通知,以及外面的人来你这拿状态的时候,你得给人家正确的状态。
    这里的 Model 表示登录,login 方法被调用后,将登录结果通过 LoginCallback 回传给调用者就完成了职责。

    再来看 View。

    View:�LoginActivity.java:

    public class LoginActivity
            extends AppCompatActivity
            implements LoginContract.View // 实现了�契约类中的接口
    {
        private EditText etAccount;
        private EditText etPwd;
    
        private LoginContract.Presenter loginPresenter;
    
        @Override
        protected void onCreate(
                @Nullable
                        Bundle savedInstanceState)
        {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_login);
    
            // 创建Presenter,使View和Presenter,Presenter和Model关联起来,这一步暂且忽略也可以,等看到 Presenter 了再回来看。
            loginPresenter = new LoginPresenter(this, LoginRequest.getInstance());
    
            etAccount = (EditText) findViewById(R.id.et_account);
            etPwd = (EditText) findViewById(R.id.et_pwd);
        }
    
        public void onLogin(View view)
        {
            String account = etAccount.getText().toString();
            String pwd = etPwd.getText().toString();
    
            // 告知Presenter发起登录
            loginPresenter.login(account, pwd);
        }
    
        @Override
        public void showLoading(boolean isShowLoading)
        {
            // 显示和隐藏进度条
        }
    
        @Override
        public void loginSuccess()
        {
            /*
            比如取消进度条,进入到主页面
             */
        }
    
        @Override
        public void loginFailure(String msg)
        {
            /*
            取消进度条,显示登录错误提示,比如密码错误、账号不存在之类的
             */
            Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
        }
    }
    

    可以看到,View 中没有任何的逻辑处理和数据获取,能做的��只是跟界面相关的操作,向�外界发出请求,以及�暴露给外界操作界面的方法。注意:View 中不能有任何的业务逻辑处理,只能有和 View 相关的操作。

    可以看到,Model 和 View 是完全分开的,没有任何的直接关联。下一步,我们需要通过 Presenter 将他们关联起来。

    Presenter:LoginPresenter.java

    public class LoginPresenter
            implements LoginContract.Presenter
    {
        // Presenter �持有 View 和 Model 的引用
        private LoginContract.View loginView;
        private LoginRequest loginRequest;
    
        public LoginPresenter(LoginContract.View loginView, LoginRequest loginRequest)
        {
            this.loginView = loginView;
            this.loginRequest = loginRequest;
        }
    
        @Override
        public void login(String account, String pwd)
        {
            // 账号密码不对的话,直接失败
            if (TextUtils.isEmpty(account.trim()) || TextUtils.isEmpty(pwd))
            {
                loginView.showLoading(false);
                loginView.loginFailure("账号密码不对");
                return;
            }
    
            loginView.showLoading(true);
    
            loginRequest.login(account, pwd, new LoginRequest.LoginCallback()
            {
                @Override
                public void onFailure(int statusCode, byte[] responseBody, Throwable error)
                {
                    loginView.loginFailure("登录错误的提示信息");
                }
    
                @Override
                public void onSuccess(int statusCode, byte[] responseBody)
                {
                    if (responseBody != null)
                    {
                        LoginResponse response = JSON.parseObject(new String(responseBody),
                                LoginResponse.class);
                        if (response != null)
                        {
                            if (response.getStatus() == 0)
                            {
                                loginView.loginSuccess();
                            }
                            else
                            {
                                loginView.loginFailure("登录错误的提示信息");
                            }
                        }
                        else
                        {
                            loginView.loginFailure("登录错误的提示信息");
                        }
                    }
                    else
                    {
                        loginView.loginFailure("登录错误的提示信息");
                    }
                }
            });
        }
    }
    

    可以看到,所有的业务逻辑都在 Presenter 里面了。

    看看以上的代码是不是符合下面这张图:

    Model 和 View 是完全分离的,以上通过小实例目的是为了让大家理解并用起来,更复杂彻底的 MVP ,可以查看 Google 的官方 Sample:
    googlesamples/android-architecture

    还有一个开源项目:
    android10/Android-CleanArchitecture

    MVP 的好处

    • Model 只有一个,View 只有一个,而 Presenter 可以有多个,但是一个 View 至少对应一个 Presenter,还是那句话,架构是很灵活的,你都把 Model 和 View 分开了,低耦合已经实现了,怎么关联他们,你看着办咯。

    • 设计图出来了,接口还没好,你可以专注先写 View,完全不用管数据。设计图没好,接口好了,你可以先写 Model,测试接口是否正常,完全不用管 View 是如何设计的。等到都设计好了,�你再把 Model 和 View 关联起来专注写逻辑。是不是觉得无比的清爽?

    • 非常适合于大型的项目,但是要避免过度设计和正确的抽象。

    MVP 的坏处

    • 类爆炸

    结语:

    本文的目的是让没有玩过 MVP 的设计快速入门的,�理解了以上内容,�进阶的内容可自己 Google,很多这方面的资料。

    相关文章

      网友评论

      • 最好的我们22:感觉项目足够臃肿之后,类爆炸不可避免。。。至少比一个类有几千行要好一点。。
      • 王元_Trump:类爆炸 还是太繁琐 还是不喜欢
        imyyq_star:专注于主要的思想,可以适当的调整以减少类。或者使用MVVM,但MVVM在Android上,我觉得可读性比较差,使用起来反而觉得逻辑也差了。

      本文标题:最简单的 MVP 理解

      本文链接:https://www.haomeiwen.com/subject/mcmdhxtx.html