android mvc mvp 简书,android MVC和MVP探讨

关于这个模式,虽然网上的资料一大堆,但是思索了好久还是决定写一篇自己的心得体会以加深自己的理解,本片以一个耳熟能详的例子来从简单的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代码如下:

285fb7648c0c

这里写图片描述

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

285fb7648c0c

这里写图片描述

第三个版本的登录功能

稍微分析上面的代码,就会发现不妥之处:比如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:

285fb7648c0c

这里写图片描述

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

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

285fb7648c0c

这里写图片描述

这是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。

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

285fb7648c0c

这里写图片描述

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就实现了,那么用第五副图片做对比的话,则如下所示:

285fb7648c0c

这里写图片描述

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

285fb7648c0c

这里写图片描述

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MVCMVPMVVM是常用的软件设计模式,用于分离应用程序的不同组件,提高代码的可维护性和可重用性。以下是它们的概念和区别: 1. MVC模式(模型-视图-控制器):MVC模式是最古老也是最常用的设计模式之一。它将应用程序分为三个组件:模型、视图和控制器。模型负责处理数据和业务规则,视图负责展示数据给用户,控制器负责处理用户输入并更新模型和视图。MVC模式通过分离关注点,使得修改一个组件对其他组件没有依赖,增强了代码的可维护性。 2. MVP模式(模型-视图-展示器):MVP模式是基于MVC模式的演化,主要用于桌面和移动应用程序的开发。它与MVC的不同之处在于,视图和控制器被合并成一个展示器,展示器负责处理用户输入、更新模型并更新视图。MVP模式通过与视图分离,使得视图的变化不会影响展示器的逻辑。这样,在测试时可以更轻松地独立对展示器进行单元测试。 3. MVVM模式(模型-视图-视图模型):MVVM模式是一种用于构建用户界面的设计模式。它将视图的状态和行为抽象成一个视图模型,视图模型负责处理用户输入、保存视图状态、与模型进行交互,并通过数据绑定将数据自动更新到视图上。MVVM模式通过数据绑定机制,使得视图和模型之间的通信变得更简单,提高了可维护性和可重用性。它常用于Web前端开发和桌面应用程序的现代界面开发。 总结来说,MVCMVPMVVM是三种常见的软件设计模式。MVC模式是最早的一种,将应用程序分为模型、视图和控制器,用于分离关注点。MVP模式是基于MVC模式的演化,通过将视图和控制器合并成一个展示器,便于测试和维护。MVVM模式是用于构建用户界面的设计模式,通过视图模型和数据绑定机制,实现了视图与模型之间的解耦。每种模式都有自己的特点和适用场景,根据具体需求选择合适的模式可以提高开发效率和代码质量。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值