美文网首页
android MVC和MVP探讨

android MVC和MVP探讨

作者: 夜色流冰 | 来源:发表于2018-06-05 13:20 被阅读33次

    关于这个模式,虽然网上的资料一大堆,但是思索了好久还是决定写一篇自己的心得体会以加深自己的理解,本片以一个耳熟能详的例子来从简单的coding到MVC再到MVP,来说说对这个模式的理解,当然不当之处欢迎批评指正。

    什么叫耳熟能详的例子呢?也就是登录功能的例子,因为这玩意儿业务逻辑简单,就是输入户名+密码,然后请求登录操作,很好抽象出来。当然后面还会简单说明用这个例子的真正原因(本篇博文涉及的代码会以android技术实现)。

    其实MVC也好,MVP也罢,这两个是框架模式,与我们熟悉的23种设计模式不是一会事儿,设计模式算可以说是解决某一类问题而总结出来的方法,用它来解决项目种某个特定功能的特定方法,可以说有点具体问题具体分析的味道。而框架模式则是一个项目的总纲,如果非说两者的关系,只能表示成使用了MVC/MVP模式的项目可能使用了23种设计模式的几种(或0种)

    第一个版本的登录功能

    盘古开天之前,一切都是混沌状态,模式这玩意还没有诞生出来(哦,差点忘了那时候还没电脑),古猿们写登录模块的代码可能如下:

    public class LoginActivity extends Activity {
        EditText userNameEdit;
        EditText passwordEdit;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.login_layout);
    
            userNameEdit = (EditText) findViewById(R.id.userName);
            passwordEdit = (EditText) findViewById(R.id.password);
            //登录点击
            findViewById(R.id.login).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                  //获取用户名和密码
                 String userName = userNameEdit.getText().toString();
                 String password = passwordEdit.getText().toString();
                 //执行登录请求
                 doLogin(userName, password);
                   
                }
            });
        }
    
     private void doLogin(String userName, String password){
            //组织参数
            Map<String, String> params = new HashMap<>();
            params.put("name", userName);
            params.put("password", password);
    
            //创建HttpUrlConnection对象
            URL url = new URL("http://www.xx.xxx");
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setUseCaches(false);
            connection.setDoInput(true);
            connection.setRequestMethod("POST");
    
            //为connection 创建post请求参数
            StringBuilder encodedParams = new StringBuilder();
            for (Map.Entry<String, String> entry : params.entrySet()) {
                encodedParams.append(URLEncoder.encode(entry.getKey(), "UTF-8"));
                encodedParams.append('=');
                encodedParams.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
                encodedParams.append('&');
            }
    
            connection.setDoOutput(true);
            connection.addRequestProperty("Content-Type",
                    "application/x-www-form-urlencoded; charset=UTF-8");
            DataOutputStream out = new DataOutputStream(connection.getOutputStream());
            out.write(encodedParams.toString().getBytes("UTF-8"));
            out.close();
    
              int responseCode = connection.getResponseCode();
            if (responseCode == 200) {
                //显示登录成功页面
                setContentView(R.layout.login_success);
            }else{
               //显示登录失败页面
                setContentView(R.layout.login_failer);
            }
    
        }
    
    }
    

    这种代码跑起来当然没问题,但是当前登录页面或者说Activity责任太多了:
    1、负责显示UI
    2、负责处理登录请求:创建HttpURLConnection对象,然后发起登录请求
    3、其他责任,比如Activity的生命周期等

    就相当于一个饭店里的厨师既负责做饭,又负责买菜,还负责到客户面前问食客点菜,甚至还准备让厨师负责送外卖,这么多活儿(责任)堆积起来,就一个结果:厨师享年在当日三更!!!(累死了)

    你可能会说就这么简单的登录逻辑,想这么多干嘛!但是登录逻辑这是一个简单的例子,比如业务复杂的话,业务逻辑代码全部都写在一个java类中,Activity的代码量可以说是轻松上千行,到时候维护都是个问题;再比如就拿这个简单的登录功能来说,如果我想把登录界面不是用Activity来展示,用Fragment、H5、Dialog等等手段来展示登录页面,那么上述的doLogin方法如果不做组织的话,就得ctr+c/ctr+v复制到H5,Fragment,Dialog中;如果登录逻辑有改动的话,那么就得修改若干处。遇到更复杂的业务,维护起来何其蛋疼。

    写到此处想起初次接触Servlet的时候(不了解Servlet的童鞋在此处可以简单的将之理解为java代码里面拼写HTML代码),一个简单的登录功能在一个Servlet里面既要处理登录逻辑,又要动态的拼接Html代码生成web页面,代码的可维护性和可读性很差劲儿,造成这样的原因还是因为Servlet的责任太多:
    1、要处理登录逻辑
    2、还要负责动态拼接html代码
    如果你想要修改登录页面的展示样式,在修改Servlet代码里面的html代码的时候手一抖可能就会出现错误,增加来调试成本。

    后来继续学习JSP技术(JSP可以简单的理解为在静态的HTML中写java代码),也写了个登录功能。jsp的好处之一就是写html代码比较方便,不像servlet那样全部是字符串拼接html代码,但是同样的在jsp中写大量的业务逻辑代码仍然不可取。

    总之上面三个版本(android/servlet/jsp)的登录功能的通病就是界面展示和有业务逻辑混杂在一块,让界面展示和业务逻辑分开让他们各司其职,UI工作的只负责渲染UI,业务方面都交给专门负责业务处理的工作。


    第二个版本的登录功能

    所以如果看过《重构,改善代码既有设计》这本书的话,你可能想到会对上述代码做一下重构,重构后的代码如下:

    public class Login2Activity extends Activity {
        EditText userNameEdit;
        EditText passwordEdit;
        LoginService mLoginService;
      
        protected void onCreate(Bundle savedInstanceState){
            super.onCreate(savedInstanceState);
            setContentView(R.layout.login_layout);
    
            userNameEdit = (EditText) findViewById(R.id.userName);
            passwordEdit = (EditText) findViewById(R.id.password);
    
            mLoginService = new LoginService(new ILoginCallback() {
                @Override
                public void loginSuccess() {
                    setContentView(R.layout.login_success);
                }
    
                @Override
                public void loginFailer() {
                    setContentView(R.layout.login_failer);
                }
            });
    
            //登录点击
            findViewById(R.id.login).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    String userName = userNameEdit.getText().toString();
                    String password = passwordEdit.getText().toString();
                    //执行登录
                    mLoginService.doLogin(userName, password);
                }
            });
        }
    }
    

    对比上下代码可以发现做了如下改动:
    1、原来的登录逻辑转移到了LoginService对象中
    2、点击登陆的时候调用了LoginService的login方法进行登陆
    3、因为登录的结果要回调给Activity,所以有设计了ILoginCallback接口,在初始化LoginService的时候对其进行初始化:

    public interface ILoginCallback {
        void loginSuccess();
        void loginFailer();
    }
    

    在登录请求返回后,调用callback的loginSuccess/loginFailer来向用户展示不同的结果。

    这样的话登录原来的登录逻辑就从LoginActivity中转移到了LoginService中,LoginActivity收到用户登录请求的时候,调用LoginServide的login方法即可,重构后的LoginService代码如下:

    这里写图片描述

    也就是说第二个版本的功能没做多少改动,主要是将用户界面和登录逻辑操作拆分开来,这样UI和登录逻辑的各自的变动都不会受到影响,各司其职,各行其是,且登录逻辑还具有很好的复用性。两个版本的登录功能可以用下图来做个直观的对比:


    这里写图片描述

    第三个版本的登录功能
    稍微分析上面的代码,就会发现不妥之处:比如LoginService可扩展性不强,现在用的HttpUrlConnection来作为网络请求的工具,如果后期要求改网络框架呢,那么我们不得不修改LoginService的代码,如果多次替换框架的话不得不多次修改;且如果要换回原来代码的话,还得查找git的commit纪录来找回原来的实现。简直是what the fuck, 这也可以说是违背了对扩展开放,对修改关闭的原则。

    所以为了防止上面情况的放生,以及对登录行为作约束,提供了登录接口:

    //登录接口,处理登录业务逻辑
    public interface ILogin {
        void login(String userName,
                   String password,
                   ILoginCallback loginCallback);
    }
    

    然后我们在重构LoginService的实现,新的LoginService2如下:

    //实现了ILogin接口
    public class LoginService2 implements ILogin {
    
        @Override
        public void login(String userName, String password, ILoginCallback loginCallback) {
          
            //省略部分代码
    
            int responseCode = connection.getResponseCode();
            if (responseCode == 200) {
                if (loginCallback != null) {
                    loginCallback.loginSuccess();
                }
            } else {
                if (loginCallback != null) {
                    loginCallback.loginFailer();
                }
            }
        }
    }
    

    那么新的登录Activity的逻辑则相应的修改如下:

    public class Login3Activity extends Activity {
        //省略部分代码
        ILogin mLogin;
    
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.login_layout);
    
            //省略部分代码
            
            mLogin = new LoginService2();
    
            //登录点击
            findViewById(R.id.login).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                  //省略部分代码
                  mLogin.login(userName, password, new ILoginCallback() {
                        @Override
                        public void loginSuccess() {
                            setContentView(R.layout.login_success);
                        }
    
                        @Override
                        public void loginFailer() {
                            setContentView(R.layout.login_failer);
                        }
                    });
                }
            });
        }
    }
    

    为了后面的讲述方便,可以将上面的代码改成如下样式,让Activity 实现ILoginCallback:


    这里写图片描述

    此时我们在想替换网络请求框架的话,没有必要修改原来的实现;在保留原来实现的基础上,在提供一个类,让该类实现ILogin接口,login方法的具体逻辑用新的网络框架来实现即可。

    所以新版本用图来跟上面两个版本的做比较就如下所示了:


    这里写图片描述

    这是MVC吗?

    为什么会有这个问题呢,这是我翻阅些许资料后产生的疑惑,就是这个疑惑差点让我放弃了写这篇文章。因为我发现我没法从代码上来说服自己这就是MVC,有点心误导别人。于是本篇博客的写作停滞了几天,而后才算悟出我犯了一种什么样的错误,MVC本身就是一种指导思想,而不是代码编写的具体提现,其目的是实现页面和业务逻辑以及数据的解耦,如果你有更好的方式来实现他们的解耦,是否是MVC或者是否非要在自己的代码中强硬的指出谁是M,谁是C有什么必要么。而我确钻到代码实现的牛角尖严格将自己的代码往MVC的定义上靠拢,以试图来解释这就是MVC。

    而且在是实现上看如果单纯的把xml当作View的话,但是其能做的工作是少之又少,大部分的事件处理代码还都是交给Activity中,而不是像html/jsp那样点击某个input按钮事件处理代码就是写在html/jsp,然后发送到具体的controller去。这就感觉Activity即像Ctroller又像是View。在这里如果把Activity看作View的话,那么这句话就不难理解(摘自本文):

    Most of the modern Android applications just use View-Model architecture,everything is connected with Activity.
    

    就是说上面的看起来像是简单的MV或者VM模式

    上面的论断有点拗口,如果有理解不当的地方欢迎批评指正,博主写博客的初衷出了分享自己的心得体会之外,还有就是“让别人指出自己的错误之处,然后改正从中加深理解"

    啰嗦了这么多,回归正题!!!

    单从整体视觉上看,就一个Activity和ILogin对象(因为View的宿主是Activity,在这里姑且看成一个整体),也就是说我们此时把Activity当成一个View来看待。但是呢如果真要区别分析的话,整个登录功能确实有三种对象:
    1、一个是Activity对象,负责诸如证明周期的管理工作
    2、一个登录页面对象(LoginView)
    3、一个实现登录功能的ILogin接口

    根据上面三条,那么代码的组织可以换成如下方式实现:
    首先看下Activity:

    public class Login4Activity extends Activity implements ILoginCallback {
        //登录页面
        LoginView loginView;
    
        //登录业务逻辑接口
        ILogin loginModel;
    
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            //初始化登录页面
            loginView = new LoginView(this);
            
            setContentView(loginView.getLoginView());
    
            //初始化登录业务逻辑对象
            loginModel = new LoginService2();
    
            loginView.setLoginListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    //点击登录按钮,实现登录
                    loginModel.login(loginView.getUserName(), loginView.getPassword(), Login5Activity.this);
                }
            });
    
        }
        
        //登录成功回调
        public void loginSuccess() {
            setContentView(R.layout.login_success);
        }
    
        //登录失败回调
        public void loginFailer() {
            setContentView(R.layout.login_failer);
        }
    }
    
    

    Activity持有了LoinView和ILogin这个负责业务逻辑的对象,在这次我故意把业务逻辑对象命名为loginModel.

    所以看看LoginView的实现就是如下所示了:

    public class LoginView {
        private ViewGroup loginView;
    
        private EditText userNameEdit;
        private EditText passwordEdit;
        private View loginBtn;
    
        public LoginView(Context context) {
            //初始化登录页面
            loginView = (ViewGroup) LayoutInflater.from(context).inflate(R.layout.login_layout, null);
            
            userNameEdit = (EditText) loginView.findViewById(R.id.userName);
            passwordEdit = (EditText) loginView.findViewById(R.id.password);
            loginBtn = loginView.findViewById(R.id.login);
        }
    
        //设置登录点击
        public void setLoginListener(View.OnClickListener loginListener) {
            loginBtn.setOnClickListener(loginListener);
        }
        
        //获取用户名
        public String getUserName() {
            return userNameEdit.getText().toString();
        }
         
        //获取用户密码
        public String getPassword() {
            return passwordEdit.getText().toString();
        }
        
        public View getLoginView() {
            return loginView;
        }
    
    }
    

    貌似有点MVC的味道了!但是这是纯粹的MVC吗?感觉MVC在android种不论怎么写都写不去那种写web时MVC味道来,总而言之可能就是Activity这个干了太多View的事儿吧(毕竟在android种View的宿主可以说就是Activity,不像网页那样,网页的宿主可以说是浏览器,跟具体的Controller没啥关联)

    鉴于Activity和View千丝万缕的联系,还是将Activity当作View层来处理吧,本文后面的View如果没有特殊说明,指的就是Activity了


    上面的例子种因为我们定义了ILogin接口,所以可以对登录的业务逻辑做很好的扩展。就像上面的图示:分别扩展了UrlConnectionLogin,VolleyLogin,OkhttpLogin等等等,从上面的代码看,我们将一个Activity 作为登录回调接口ILoginCallback的实现类,为了很好的说明MVP,现在将ILoginCallback有意的重命名为ILoginView:

    public interface ILoginView {
        void loginSuccess();
    
        void loginFailer();
    }
    

    原来对应的ILogin接口修改为ILoginModel:

    public interface ILoginModel {
        //将ILoginView替换原来的ILoginCallback
        void login(String userName,
                   String password,
                   ILoginView loginView
                   );
    }
    

    那么对应的Login4Activity就该为如下所示了:

    public class Login5Activity extends Activity implements ILoginView {
        //登录页面
        LoginView loginView;
    
        //登录业务逻辑接口
        ILoginModel loginModel;
        //省略部分代码
    
        public void loginSuccess() {
            setContentView(R.layout.login_success);
        }
    
        public void loginFailer() {
            setContentView(R.layout.login_failer);
        }
    }
    

    LoginService修改如下:

    //实现了ILoginModel
    public class LoginService3 implements ILoginModel {
    
        /**
        *第三个参数由原来的ILoginCallback改成ILoginView
        **/
        @Override
        public void login(String userName, String password, ILoginView loginView) {
                 //省略部分代码
                int responseCode = connection.getResponseCode();
                if (responseCode == 200) {
                    if (loginView != null) {
                        loginView.loginSuccess();
                    }
                } else {
                    if (loginView != null) {
                        loginView.loginFailer();
                    }
                }
        }
    }
    
    

    就这样,因为ILoginView接口的存在,我们很容易更改我们的登录页面的展现方式,比如我们将登录界面用Dialog或者Fragment来想用户展示,那么我们直接让Dialog或者Fragment实现ILoginView 接口,并将其丢给ILoginModel即可。这样UI 的变化,并不能影响业务逻辑;相反的业务逻辑的修改也不会影响到UI。

    那么用第四幅图来跟前面三个图做一个对比,就很明显了:

    这里写图片描述

    MVP粉墨登场

    但是这么写会有一个问题,比如从View这个层面来说,View和Model绑定耦合在了一起;比如有若干个登录页面(扯淡,实际项目中哪有这么多登录页面,just case),那么若干个登录里面都得持有ILoginModel的实现类,如ILoginModel改变的话,那么就需要把若干个登录页面于ILoginModel有关的代码全部都得改,想想就疯了。

    关键还是怎么解决View和Model的解耦问题,所以MVP的P角色闪亮登场。P作为中间人的角色,持有ILoginView和ILgoinModel,这样ILoginModel的改变不会影响ILonView
    为了用MVP实现这个登录功能,现将原有的ILoginView接口新增两个方法:

    
    public interface ILoginView {
        void loginSuccess();
    
        void loginFailer();
    
        //新增方法
        String getUserName();
    
        //新增方法
        String getPassword();
    }
    
    

    所以LoginPresenter这个P角色的代码就可以简单写成:

    
    public class LoginPresenter {
        //登录业务逻辑接口
        private ILoginModel loginModel;
        //登录界面
        private ILoginView loginView;
    
        public LoginPresenter(ILoginView loginView) {
            //如果ILoginModel需要修改的话,修改此处即可
            this.loginModel = new VolleyLoginModel();
            this.loginView = loginView;
        }
    
        //实现登录方法
        public void login(){
            loginModel.login(loginView.getUserName(),loginView.getPassword(),loginView);
        }
    }
    

    可以看出LoginPresenter很简单,就是持有一个ILoginView和ILoginModel的引用,且ILoginView是外部初始化传过来的,ILoginModel是内部自己初始化。这样的话即使ILoginModel的实现类有改动,比如有VolleyLoginModel改为OkhttpLoginModel,修改LoginPersenter里面的ILoginModel代码即可,而不需要将每个ILoginView中关于ILoginModel的代码都改一遍,即将

     this.loginModel = new VolleyLoginModel();
    

    改成:

     this.loginModel = new OkhttpModel();
    

    当然如果抽象的好的话,login 方法应该也是一个接口方法比较好。
    同样的LoginActivity,将修改为如下实现:

    public class Login6Activity extends Activity implements ILoginView {
        //登录页面
        LoginView loginView;
    
        //登录Persenter
        LoginPresenter loginPresenter;
    
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            //初始化登录页面
            loginView = new LoginView(this);
            setContentView(loginView.getLoginView());
    
            //初始化LoginPresnter
            loginPresenter = new LoginPresenter(this);
    
            loginView.setLoginListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    //调用登录方法:见上面实现
                    loginPresenter.login();
                }
            });
    
        }
    
        public void loginSuccess() {
            setContentView(R.layout.login_success);
        }
    
        public void loginFailer() {
            setContentView(R.layout.login_failer);
        }
    
        @Override
        public String getUserName() {
            return loginView.getUserName();
        }
    
        @Override
        public String getPassword() {
            return loginView.getPassword();
        }
    }
    
    

    所以简单的MVP就实现了,那么用第五副图片做对比的话,则如下所示:


    这里写图片描述 最后再来个来个经典的MVP图片总结上图(图片来源百度百科): 这里写图片描述

    其实说白了,MVP就是解耦问题,解耦的问题首先是要抽象,比如IView,IModel等接口的设计。但是呢,抽象的前提是你要对业务或者需求有所了解,为什么博主会拿登录作为本篇的demo,因为业务简单好抽象,在实际开发中可能你做了抽象开始还很好,但是随着需求的不断变化会让你不段频繁的修改接口,比如你修改IView接口相关的实现类都需要改;同样的你修改IModel接口,那么相应的Model也需要改;频繁的改动可能会出现各种问题;所以MVP的利弊博主水平不够,还没发对此作出评论。不做对于简单业务来使用的话,MVP确实使得代码结构很清晰了。

    以上就是博主对于MVP的探讨,如果有发现不对的地方,欢迎批评指正,共同学习提高

    相关文章

      网友评论

          本文标题:android MVC和MVP探讨

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