关于这个模式,虽然网上的资料一大堆,但是思索了好久还是决定写一篇自己的心得体会以加深自己的理解,本片以一个耳熟能详的例子来从简单的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 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 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的探讨,如果有发现不对的地方,欢迎批评指正,共同学习提高