android MVC和MVP探讨

2018-06-05  本文已影响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的探讨,如果有发现不对的地方,欢迎批评指正,共同学习提高

上一篇下一篇

猜你喜欢

热点阅读