使用MVP模式重构代码

作者: 骆驼骑士 | 来源:发表于2016-05-07 00:58 被阅读18799次

    之前写了两篇关于MVP模式的文章,主要讲得都是一些概念,这里谈谈自己在Android项目中使用MVP模式的真实感受,并以实例的形式一起尝试来使用MVP模式去重构我们现有的代码。

    有兴趣的童鞋可以先去阅读之前的文章,因为这里将不再重复概念的部分了,本文会假设你对MVP有一点了解了:

    1. 在谈MVP之前,你真的懂MVC吗?

    2. MVP模式是你的救命稻草吗?

    臃肿的Activity

    大部分谈Android架构的时候,都基本会提到Activity越来越臃肿的问题,这几乎是一个普遍现象,而包括我本人在内的,都会首先将这个罪责推到MVC架构上,但如果你真的花时间去重构activity的时候,你会发现问题其实往往出在自己身上。

    一般的MVC里的 Controller 需要做的事情:

    1. 负责获取和处理用户的输入。
    2. 负责将输入传给负责业务逻辑层去做数据上的操作(如增删改查)。
    3. 负责将业务逻辑层对于数据操作的结果,传给View层去做展示。

    因此如果完全按照这种定义的话,你应该很难看到一个非常臃肿的Controller,因为Controller在MVC模式中,本来就应该是很轻的,而不是很重的部分,重的应该是M层,甚至在前端交互复杂的时候,V层都应该比C层要重。

    我认为对于Controller的理解,就是一个站在M和V两者之间的一个翻译家,M来自地球,V来自火星。而如果站在中间的这个翻译者,话比他两的话还多,老是抢话,自言自语,这样显然是不合适的。

    那么我们再来看典型的Activity的代码,处理的业务是常见的登录页面:

    public class UserActivity extends Activity {
      
      private RequestQueue mQueue = Volley.newRequestQueue(this);
      
      private TextView mUsernameTextView;
      private TextView mPasswordTextView;
      private Button mLoginBtn;
      
      @Override
      public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.login);
        
        mUsernameTextView = (TextView) findViewById(R.id.username);
        mPasswordTextView = (TextView) findViewById(R.id.password);
        
        mLoginBtn = (Button) findViewById(R.id.login_btn);
        mLoginBtn.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v)  {
                String username = mUsernameTextView.getText().toString();
                 String password = mPasswordTextView.getText().toString();
                 
                 String loginUrl = "http://somesite.com/login.php";
              
                 JSONObjectRequest request = new JSONObjectRequest(loginUrl, Method.POST,
                    new Response.Listener<JSONObject>() {
                        public void onResponse(JSONObject json) {
                            if (json != null && json.get("isOk") == true) {
                                Toast.makeToast(getApplicationContext(), "Login Success", Toast.LENGTH_SHORT).show();
                                  startActivity(new Intent(LoginActivity.this, MainActivity.class));
                            } else {
                                Toast.makeToast(getApplicationContext(), "Login Fail", Toast.LENGTH_SHORT).show();
                            }
                        }
                   },
                    new Response.ErrorListener(){
                        public void onError(VolleyError error) {
                            Toast.makeToast(getApplicationContext(), "Login Fail", Toast.LENGTH_SHORT).show();
                        }
                   });
                mQueue.add(request);
            } 
        });
      }
      
      @Override
      protected void onStop() {
        super.onStop();
        if (mQueue != null) {
            mQueue.cancelAll();
        }
      }
      
    }
    

    其实这已经是极度简化过的代码了,真正的一个LoginActivity很容易就会超过几千行,不信你去看自己项目里面的代码就明白我在说什么了。

    而一对比概念,我相信大部分人一下子就会发现问题,我们还是来看Activity作为一个 Controller 到底都负责干什么了:

    1. 首先activity必须去操作View的控件,设置它们的回调函数,有时也需要用代码去控制它们如何展示的属性。
    2. 然后activity一定需要去处理用户的输入,例如输入的值,以及点击事件等用户行为。
    3. 而几乎大部分异步网络请求都从activity发起,以及服务器返回数据的处理。
    4. activity一般还需要根据数据的操作结果,负责在页面上将结果告之用户,例如Toast或者其他View的操作。
    5. 除此之外,activity还需要管理其生命周期相关的所有事务,例如在页面退出的时候处理一下View控件和其他与生命期相关的逻辑。

    你会发现Activity天生的责任太重,其中确实覆盖了 Controller 的原本的责任,例如处理用户输入,将用户操作转换成传递给业务逻辑层的命令等职责。

    但如果你仔细的分析,你会发现activity不仅仅需要承担Controller的责任,还需要处理大量View的逻辑,例如控件的监听的属性,如何展示数据的职责也往往落到了它的肩上。更何况你很容易在activity写操作数据和网络请求的代码,也就是让它又承担了Model的责任,那么请问这样的Activity能不臃肿吗?

    当然这是一个坏的例子,其实很多代码是可以封装到独立的层去的,例如网络请求,数据解析等。但就算你怎么封装和重构,你最多能做的事情也就是把本来就不应该放在Controller里的Model层分离出去,这是你原本就应该做的事情。但你很难在activity将controller和view分离开来,怎么写activity作为Controller,都和View的关系太紧密,必须多多少少去控制如何展示数据这个View的责任。

    Presenter是来给activity减负的吗?

    很多人会认为MVP中引入Presenter的概念,是为了给日益臃肿的activity来减负的,而我不这样认为,我认为Presenter和Controller的责任是差不多的,它们后期承担的目的都其实很简单,就是用来隔离Model和View的,也就是常说的展示层和业务层的解藕。

    那么该如何解决activity的问题呢?目前常见的MVP在Android里的实践有两种解决方案:

    1. 直接将Activity看作View,让它只承担View的责任。
    2. 将Activity看作一个MVP三者以外的一个Controller,只控制生命周期。

    在Google推出的官方MVP实例里,使用的就是第2种思路,它让每一个Activity都拥有一个Fragment来作为View,然后每个Activity也对应一个Presenter,在Activity里只处理与生命周期有关的内容,并跳出MVP之外,负责实例化Model,View,Presenter,并负责将三者合理的建立联系,承担的就是一个上帝视角。

    在实践中,也有很多观点会简化掉Fragment,直接将Activity视为View,这个也是我比较赞同的,更简便一些,而且这样观念上也容易理解一些,你就把activity看作View的一部分,永远只让它处理展示的逻辑,不允许它去处理数据,和拥有业务逻辑。但是这样也有一个缺点,就是V和P的依赖关系不太规范了,理论上你是不应该在View里面去实例化Presenter和Model的,这其实是不合理的,正确的依赖关系,确实是应该在一个独立的更上层去实例化Model,View,Presenter的,这样依赖才是较为合理的关系,这点来看Google的架构模式确实更合理,但实操上也会麻烦一点,必须让每个activity拥有一个独立的fragment,这个我是觉得可以自由取舍,你是要概念上的合理,还是现实中的方便,其实都可以。

    因为重点还是在于如何分离展示层和业务层,activity具体承担什么责任都可以,但只能承担一个责任。

    例如之前的代码可以被重构成如下结构:

    /**
     * View负责展示数据
     */
    public interface UserView {
     
      void showLoginSuccessMsg(User loginedUser);
      void showLoginFailMsg(String errorMsg); 
      
    }
    
    /**
     * Presenter负责做View和Model的中间人
     */
    public interface UserPresenter {
     
      void login(String username, String password);
      
    }
    
    /**
     * Model负责数据的处理和业务逻辑
     */
    public interface UserModel {
    
      void login(String username, String password, Callback callback);
      
    }
    

    这里将Activity被视为View, 仅负责数据的展示,并且将用户的操作事件路由给P去做处理。

    public class UserActivity extends Activity implements UserView {
    
      private UserContract.Presenter mPresenter;
      
      private TextView mUsernameTextView;
      private TextView mPasswordTextView;
      private Button mLoginBtn;
      
      @Override
      public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.login);
        
        mPresenter = new AddressListPresenter(this, new UserModelImpl());
        
        mUsernameTextView = (TextView) findViewById(R.id.username);
        mPasswordTextView = (TextView) findViewById(R.id.password);
        
        mLoginBtn = (Button) findViewById(R.id.login_btn);
        mLoginBtn.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v)  {
                String username = mUsernameTextView.getText().toString();
                String password = mPasswordTextView.getText().toString();
                // View将用户的点击事件直接路由给Presenter区处理
                mPresenter.login(username, password); 
            } 
        });
      }
      
      @Override
      public void showLoginSuccessMsg(User loginedUser) {
        // Presenter在处理完毕后, 会通知View更新UI来通知用户数据操作的结果
        Toast.makeToast(getApplicationContext(), "Login Success", Toast.LENGTH_SHORT).show();
      }
      
      @Override
      public void showLoginFailMsg(String errorMsg) {
        // Presenter在处理完毕后, 会通知View更新UI来通知用户数据操作的结果
        Toast.makeToast(getApplicationContext(), "Login Fail", Toast.LENGTH_SHORT).show();
      }
      
      @Override
      protected void onResume() {
          super.onResume();
          mPresenter.subscribe();
      }
    
      @Override
      protected void onPause() {
          super.onPause();
          mPresenter.unSubscribe();
      }
      
    }
    

    Presenter层则负责将在View和Model做中间人:

    /**
     * Model负责数据的处理和业务逻辑
     */
    public class UserPresenterImpl implements UserPresenter {
    
      private UserView mUserView;
      private UserModel mUserModel;
    
      public UserPresenterImpl(UserView view, UserModel model) {
        mUserView = view;
        mUserModel = model;
      }
    
      public void login(String username, String password) {
        // Presenter处理View路由过来的用户操作,
        // 将其转换成相对的命令,传递给Model来做数据操作
        mUserModel.login(username, password, new Callback(){
          public void onSuccess(User user) {
            // Model层对数据操作后,将结果返回给Presenter,
            // 再由Presenter来通知View去更新UI来通知用户数据操作的结果
            mView.showLoginSuccessMsg(user);
          }
          public void onFail(String errorMsg) {
            mView.showLoginFailMsg(user);
          }
        });
      }
      
    }
    

    而Model层大家则已经可以脑补出来了,只负责对于数据的操作而已了。例如请求服务器获取数据,获取查询本地数据库都可以。

    从1个类变为3个类

    在MVP的实践中,很明显的结构变化就是很多页面从1个类变成了3个甚至更多的类。

    例如,原来只有一个 LoginActivity ,而现在会变成至少3个类:

    1. LoginActivity(View)
    2. LoginPresenterImpl (Presenter)
    3. LoginModelImpl (Model)

    而你以为这些就够了,就太天真的,在MVP里,为了解藕三者之间的关系,还需要通过接口来通信,P层是通过接口来和M层通信的,P层和V层之间也是通过接口来互相通信的(但V层对P层的通信被视为被动通信,而非主动通信)

    接口列表:

    1. LoginView (interface for View)
    2. LoginPresenter (interface for Presenter)
    3. LoginMode (interface for Model)

    这里插一句题外话,在Google官方的MVP实例里的,有一个契约类的概念,这个契约类的概念引入我觉得真的很赞,其实它只是将View和Presenter的接口写到了一个类里面,但这样写则会使得读代码的人一目了然就可以了解这个页面需要展示些什么,有什么操作。

    如果你以为MVP各一个接口这样就应该够了,我只能说你还是太年轻太天真。要知道很多消息在MVP三者之间传递,不仅仅是同步消息,还有很多异步消息,例如用户点击了一个按钮,View将该事件传递给Presenter,Presenter异步的向Model请求数据,Model异步的返回数据给Presenter,Presenter再将Model处理的结果异步的传递给View,让其向用户作出回应。

    可想而之,这样异步操作,自然少不了一些Callback的接口类,虽然可以用内部类来解决,但如果不用范型的话,这些Callback的接口类数目还是很多的。

    这里也插一句提外话,我个人推荐使用rxJava来解决回调恶魔的问题,不过这仅仅是个人偏好而已。

    从直来直去变成跳来跳去

    上文说了,从1个类的代码,分离到了N个文件,三个层面以后,原本直来直去的代码结构,就会变成跳来跳去,例如之前1000行代码是写在一起的,现在把其中View的部分代码独立到了View的文件里,把其中Model的部分独立到Model的文件中,然后用Presenter放在它们中间,做一个中间人。

    而且再加上很多消息的传递是异步的,因为在看代码的时候,或者在调试的时候,你必须从过去线性的思维变成跳跃式的,很多代码过去你开一个文件,顺着看下来就明白的,DEBUG模式下,一顺运行下来的,现在变成了你需要开N个文件,DEBUG模式下就看着从View的一个方法,跳到Presenter的一个方法,然后再跳到Model的一个方法,然后再原路跳回来,友情提示,刚开始用MVP的时候,很容易代码很清晰,但大脑却很混乱,甚至有晕车的感觉。

    并且我认为这样的代码结构,甚至加大了调试的困难,过去直来直去,你很容易判断出数据是断在哪里,而现在你很难判断出数据断在哪一个层面,例如用户点击了刷新,需要从服务器拉回数据刷新到列表。但当页面没有正常展示数据的时候,你必须知道在哪个环节出错了,而我告诉你,因为分成了三个层,并且消息和数据在三个层之间传递,那么出错的可能性也变多了:

    1. 可能是View层没有把用户的事件传递给Presenter层。
    2. Presenter层可能接受到View层事件,但没有将操作传递给Model层。
    3. Model层可能接受到Presenter的请求,但没有将数据传回给Presenter层。
    4. Presenter层可能接受到Model的返回值了,但没有正确的将数据传回给View层。
    5. View层可能接受到了Presenter返回值,只是没有正确的将数据显示到页面而已。

    在调试的时候,你会发现,跟踪一个问题变复杂了,消息在MVP之间传来传去的,你很难一下定位到问题出在哪个层面。

    那么为什么要用MVP?

    说了这么MVP带来的麻烦,例如多写了很多类,思维跳来跳去,消息传来传去,层层回调把人转晕,那回头去思考:我们为什么要用MVP,为什么要这样拆分代码,不是说这样代码更清晰,更容易理解了吗,为什么我看不懂我的代码了,为什么调试起来如何麻烦?

    其实,这样我要反复说的,如果你只是学会怎么使用MVP,那么你只是换了一个架构而已,这就和你换了一个IDE写代码,却期望换了IDE就可以让代码突然变的更好一样。而你真正需要做的,依然是我之前说过的:

    你需要换的是脑子,而不是架构。

    如果你还在每次修改一行代码,就整体去测试你的系统,那么你把代码写在一个文件里,还是拆分到几个文件里,其实是没有区别的。你只是把代码拆开在放,而这样的拆注定只是形式的,最终我相信写着写着,你会在View里面写Model的逻辑,在Model里面写View的逻辑,并且和过去一样,Presenter越来越臃肿。

    为什么要把架构里的各个层次分得清清楚楚,每个层面负责什么,不应该负责负责,如何组合起来都需要严格的定义起来,你要知道,每一种架构都不是编码规范,也不是组织代码的规范,它们都是一种思维方式。

    之前说过,良好的架构都是在解决几个问题:低藕合,高复用,易测试,好维护。

    如果你还在你的类和类之间new来new去,你引用我,我引用你,互相依赖,层层依赖,那么你把它们写在一个文件里,和把它们几个文件里有区别吗?

    如果你的一个类还承担多个职责,明明这是个叫 Car 的类,却又在承担轮子,又在承担引擎的责任,那么你抽象和不抽像,封装不封装真的有区别吗?

    如果你的一个方法还在做两件甚至三件事情,甚至把一整套事情都做完了,动辄超过几屏的函数,那么你真的觉得用不用架构真的有区别吗?

    单元测试&MVP

    为什么要把代码拆分成不同的文件,为什么要把架构拆分成不同的层面,其实思想都是在将一个复杂的整体拆分成一个个独立的模块,然后再用合理的接口将这些模块组装到一起,成为一个完整而稳定的系统。

    很多文章都会提到“易测试”的概念,在编码里面,易测试绝对不是易于测试人员去测试的意思,而只有一个意思,那就是易于单元测试,易于将整体拆分成独立的单元进行测试。

    但是很多时候,我们都会认为写单元测试是一种浪费时间的事情,但其实这是非常错误的一种观点,单元测试反倒是在节省时间。

    就像上文提到的,如果你还是修改了一处代码,然后就跑一遍系统,整体的测试一遍,那么不使用MVP反而比使用MVP调试要轻松。但你反过来想,你如果还是每次都是整体的测试,那么你把代码分开的意义又何在呢?将代码拆分成独立的层次,独立的模块,一来是为了更好的复用,二来就是为了能够独立的测试。

    可以说使用MVP,如果只是按照Google的实例去拆分代码,这只做到了第一步,而第二步就是去看Google实例中是如何写单元测试的,如何独立的对Model层去做测试,对View层去做测试,以及Presenter层如何测试。你就会发现之所以拆分,带来的最大好处就是测试友好了。你可以独立的去做测试,因为拆分了,所以互相藕合低了,互相藕合低了,所以各自更独立了,各个更独立了就使得单元测试成为了可能性,你可以独立的对MVP里的每一个层面,每一个模块,每一个公开函数进行独立的测试,当你确保了每一个独立的函数,每一个类,每一个包都能都独立的完成自己的逻辑,那么通过接口把它们组合在一起后,整体测试反而变成依然轻松了,你不需要关心代码跳来跳去,消息传来传去,只要每个模块,每个层次的逻辑是正确的,是经过单元测试的,那么整体系统就不会出现太大的问题。

    所以说,最终我认为MVP的关键还是在于 单元测试 ,不管你是用MVC,还是MVP,如果你的代码是能够进行良好的单元测试,那么说明你的架构就不可能有太大问题,而使用什么架构只是表象,真正起区别代码高低境界的还是思考问题的方式。

    下一篇预告将继续对MVP模式进行展开,并将重点放在如何在Android上对MVP各个模块进行单元测试。

    相关文章

      网友评论

      • Bug集:写得好,很耐看
      • 楷桐:你好,在Fragment里,presenter是什么时候new出来的,什么时候销毁掉presenter
      • e504e0e53b26:对于Service,MVP怎么用,需要拆分业务逻辑出去吗,是否只需要拆分下model
      • 潜翼:你这个例子不太对的,按道理来讲View属于最上层,持有P的引用,P作为中间层肯定也持有V和M的引用,但是例子里面M在V里面new出来,那么问题就大了,更应该在P里面new出来而不是M。弱化fragment也要讲基本法。
      • tea9:请问有源码吗 UserPresenterImpl方法不知道你在哪里调用的
      • 都锋:支持,感谢分享,初学mvp看了思路感觉清晰多了
      • KunMinX:不一定要使用MVP,但一定是要追求模块化,对个人也好,对团队也罢。个人业余时间写程序,时间肯定是不够,将模块的界限划分好,使得每次可以只做一小块,最后再将模块拼接起来。团队多人协作,模块的分离有助于各司其职,前台就只写前台,写完可以自己先做个假后台做单元测试,完后再拿真后台做集成测试。。反正我对解耦是这么理解的,至于是否一定MVP,就看个人或团队的习惯。这样说吧,MVP有个契约类的概念,就像接口目录一样,让数据的交互一目了然
      • 寒冷期的夜:mPresenter = new AddressListPresenter(this, new UserModelImpl()); 这个
        AddressListPresenter哪里来的??
      • 黑马有点白986:辛苦了,谢谢分享
      • a14500ec6607:感觉对于MVP的定义其实还是很好理解的,也有办法自己写一些简单的代码。但是真到实际APP开发就很有难度,感觉思维会混乱,甚至有一种无从下手的感觉。
      • 凡浩浩:在UserActivity这个类里面,showLoginSuccessMsg(User loginedUser)这个方法能直接用Toast方法吗?不会报错吗?我运行报错了
      • 凡浩浩:非常棒,受益匪浅!!!
      • a14ccfab4675:楼主,什么时候再出下一篇关于MVP的文章啊?
        朱凯奇:@a14ccfab4675 二凯大神
      • XL_Man:你要知道,每一种架构都不是编码规范,也不是组织代码的规范,它们都是一种思维方式!
        大赞!
      • bb506eeddd69:评论的人。在好好看看吧 回答的好蠢
      • eb0cfe994f95:弱弱的问一个问题, 登录时, 通常不是需要写一些验证 用户名, 密码的逻辑么, 这些逻辑应该写到哪里????
        dc0f08158bd9:@Unwillingtomedi 并不是在model层直接弹toast。比如说:model层load DB发现用户名与密码不对应,或者在load DB的过程有异常,那就去调用我们定义的回掉接口Callback.fail()。你可以看看这个例子(https://github.com/tonychancode/Login_MVP.git),不过我把load DB简化成一个字符串比对了~
        eb0cfe994f95:@终日乾乾_CS <a href="jianshu://users/1732288">Unwillingtomedi</a>: <a href="jianshu://users/952def166162">@终日乾乾</a> <br><br>那么,如果用户名密码不对的时候,一般来说都会弹toast,这个代码按说,我觉得应该是view层,但是这个时候在你model层让view弹toast,感觉好难
        dc0f08158bd9:@Unwillingtomedi 这里是写在model层,UserModelImpl里面的login(String username, String password, Callback callback)方法里,我觉得他的interface UserMode应该有定义这个Callback接口,不然想不通。。。
      • dc0f08158bd9:Hi, 你好~
        我最近才开始看MVP方面的东西,所以这篇文章对我的帮助很大,thx a lot~
        但是我还是有点疑问的,我试着重写你的draft code,但是在Model层这里遇到了一点困难。
        -------------
        login(String username, String password, Callback callback)
        -------------
        这个callback应该怎么处理呢,我不是很理解的,你能大概讲一下吗,谢谢!
      • fef3a2360375:https://github.com/CarlLu/MVPframe
        mvp+rxjava+retrofit+dagger2开源框架,希望大家支持
      • 捡淑:mark
      • KunMinX:我个人觉得MVP最大的优点在于各司其职。你会知道它只是负责提供UI片段和触发Presenter的,在改代码的时候你会知道直接在Presenter类中查看业务方法就行,你不用再在几千行由业务和UI混杂在一起的无关代码中搞的不快、搞的自己晕头转向
      • 奋斗的Leo: :flushed: 之前看过官方出的示范项目,想在新项目上实践
      • Wing_Li:“对于Controller的理解,就是一个站在M和V两者之间的一个翻译家,M来自地球,V来自火星。而如果站在中间的这个翻译者,话比他两的话还多,老是抢话,自言自语,这样显然是不合适的。” 太对了。
      • 向晚轻烟:为什么不用泛型呢?既然是重构每个模块都这样写岂不是任务更加繁重
      • 言覃土艮: :smile: 感觉很多实际场景,能把M和V分清就算好的架构了
      • 9efe1db2c646:再给你打赏一次,快点出下一篇~
        9efe1db2c646:@骆驼骑士 哈哈哈,那是手指的肌肉记忆
        骆驼骑士:@彼时芒种 谢谢老板。。。输入法坑死人
        9efe1db2c646:@骆驼骑士 是老板!
      • 9efe1db2c646:“如果你还在你的类和类之间new来new去,你引用我,我引用你,互相依赖,层层依赖,那么你把它们写在一个文件里,和把它们几个文件里有区别吗?”
        一针见血。。。
        很早就计划在项目中引入单元测试了,但一直无从下手(也不知道怎样用单元测试),希望下一篇mvp单元测试能详细分享你宝贵的开发心得!

      • 纤沫:继续关注..
      • 13itch:个人感觉,mvp的架构还不够完善,就目前而言,很难在繁重的开发任务中,提升效率。低耦合的好处是便于需求更改时代码改动量,但如果有些需求基本上不会变了的,使用mvp就有点过于反锁了。新手个人见解,大神勿喷,谢谢
        7de928b3e4a2:觉得需求是否改变不是关键,还是在于逻辑和业务复杂度,如果简单,确实没必要用框架复杂化,反之,测试,维护和新人接盘都是有效率提升的
        Wing_Li:@骆驼骑士 非常同意,如果不知道这个项目是不是长期做。还不如直接用以前的方式,在写代码的时候尽可能的将模块功能区分开,便于以后去重构。
        骆驼骑士: @13itch 一定程度上我同意你的观点,在开发效率上确实看使用场景的,只有需要复用或改版时才能感受到效率的提升,也就是说做需要长期维护和迭代的项目才适合这样写。

      本文标题:使用MVP模式重构代码

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