(转载请注明来源,谢谢)
在深入分析MVC和MVP之前,我们有必要回顾下经典的三层架构。分层是计算机学科解决许多问题的法宝。在企业应用和互联网应用中,分层架构得到了非常广泛的应用。3层架构是各种层架构的基础,3层架构简单描述如下:
展示层:展示层有两个职责
1负责展示业务数据
2提供用户输入的接口
业务逻辑层:业务逻辑层的职责是接受展示层的输入,并经过业务处理逻辑,返回业务数据。
数据访问层:数据访问层提供系统数据的存取服务。
从架构到实现是存在一些"距离"的,架构的实现是要基于应用场景,实现方案也有所不一样。
本文考虑两种应用场景来讨论MVC和MVP:
第一是c/s架构,也就是所谓的胖客户端,像RIA属于这类。Flex,ajax等技术都可以归结到RIA,手机APP,winform连接到远程服务的应用都可以归结这一类。
第二是b/s架构(某些web程序部分的利用了ajax,排除利用ajax的这部分),也就是瘦客户端,b是指浏览器,在b/s架构中http协议是客户端和服务端通信的唯一协议,而且是通过浏览器来展示数据的。
基于层的架构的实现中的一个问题就是层与层之间如何通信,以及如何在层间传递数据。
首先看看数据访问层和业务层之间的通信和数据传递。
(注意,数据访问层不保存持久数据,数据访问层是访问持久数据的接口。持久数据一般位于独立数据服务器之中,比如数据库,索引数据等等。)
在物理上,数据访问层和业务逻辑层通常部署在同一台服务器上,所以它们之间的通信属于进程内数据传递,传递的是业务对象,速度很快,但是数据访问层访问持久数据相对很慢的。数据访问层目标就是尽可能的快的存储业务对象,而业务模型的设计会影响业务对象的存储速度,所以数据访问层的设计和实现通常是在业务模型和数据访问策略之间进行权衡。关于数据访问层设计也有几种模式,关于这方面请自行查阅相关资料。
下面我们看看本文的重点:
业务层和视图层的通信,我们常用的是MVC模式。MVC模式是展示层模式,有必要细致的分解下展示层的职责:
1 渲染界面。在b/s架构中体现在浏览器解析html然后绘制相应的图形。在c/s架构中,比如winform程序中利用控件的状态调用本地GDI接口绘制窗口,填充数据。
在b/s和c/s中,这部分工作只能在客户端完成。
2 执行展示逻辑。依据业务层返回的数据来生成渲染界面所需的数据。
举例说明下,在b/s中,假设业务层返回字符串数组,在展示层需要使用列表进行展示,那么我们必须利用这个数组生成特定的html,这个过程就是展示逻辑。
在b/s架构中,展示逻辑位于服务端。
在c/s架构中这部分工作在客户端进行。比如在客户端利用ajax获取字符串数组后,利用js生成html。这个展示逻辑是在客户端完成的。在winform程序中利用控件的方法设置相应的值。
3 获取输入。在c/s和b/s之中都通过客户端程序来获取数据,比如浏览器提供的输入框,winform提供的文本控件。在c/s和b/s中这部分工作只能客户端完成。
4 引发事件。事件是触发某种业务的的起点,业务层总是在某个事件到来时被调用,事件总是携带某些用户输入的信息。
在b/s架构中,浏览器发送http请求代表着事件,而在服务器web服务器在监听http请求到达这个事件,分析http请求携带的信息,交给相应的程序处理。
在c/s架构中,事件更好理解。比如点击按钮是个事件,文本框的内容改变可以定义为一个事件,文本框的内容可以作为事件的信息。
5 处理事件。展示层还有个非常重要的任务就是调用业务层接口,当事件发生时总有个程序会处理,这段程序会调用真正的业务层。
在b/s架构中,举个例子,一段php脚本从请求读取参数,利用参数调用业务,然后生成html,这段程序就是事件处理的程序,它是属于展示层,而不是业务层的。
在c/s架构中更加明显,像winform程序的onclick事件处理器。
在b/s架构中事件的处理是在服务端,因为http请求事件是在服务端处理,而在c/s架构中事件的处理是在客户端。
下文中分别以 职责1-5代表上面的职责,其中最重要的是2和5。
下面我们看看在b/s架构的应用环境之中MVC有着怎么样的变化。
在b/s场景之下,用户在浏览器中的任何行为导致的事件最终都会转换为http请求,所以只存在单一的事件,所以第一步是我们需要依据事件携带的信息将这个http请求转发给相应的事件处理器。事件处理器在获取业务数据后,需要通知视图去改变,这个过程通过一个http响应来实现,而http响应体就是视图渲染所需的信息,在利用业务数据生成http响应体的过程实际上就是执行展示逻辑的过程。由于http协议是无状态的,所以在基于http协议中实现真正的主动MVC模式是比较困难的,一般的思路有两种:a长连接 b轮询方式
目前在b/s架构下有两种常见的实现MVC的模式,一种叫前端控制器,一种叫页面控制器。
前端控制器实现之中,存在一个类负责处理http请求事件,这个类就叫前端控制器,这个类主要工作就是依据请求信息将这些请求转发给相应的控制器来处理事件。每个控制器处理结束之后会返回一些信息(职责5),然后前端控制器使用视图引擎来生成所需的http响应体(职责2)。前端控制器将职责2和职责5代理给不同的类。
现在主流MVC框架都是采用这种,比如spring mvc,zend等等。(个人十分推崇spring MVC,建议大家都去阅读下源码)
页面控制器更加好理解,每个页面一个控制器。所有该页面能够发生的事件都在这个控制器内响应。比如asp.net中的Page类是典型的页面控制器。页面控制器的缺点是像一些公用的功能很难在各个控制器中复用,只能采取继承的方式,导致复杂的类体系,而且继承的方式是很难维护。
而在c/s架构之中,事件是多样的。点击按钮,勾选复选框等等都是事件,如何管理事件是在c/s实现mvc的核心。
在winform程序中,每个事件都有一个处理器(事件委托),这个处理器通常承担了职责2和职责5。
所以对于winform来讲,MVC实现的非常不好,在winform之中要实现职责2和职责5的分离必须抛弃winform的思维自己实现,或者借助相应的框架实现。
我们再看一下gxt(ext GWT)它提供一个MVC框架。
MVC框架中核心就是事件的分发,对于一个MVC框架应该处理的是应用事件而不是系统事件。系统事件是指用户操作中由操作系统产生的事件,而应用事件是每个应用程序自定义的一些事件,比如点击登录按钮系统事件是Click,而应用事件可能是AppEvent.LonIn。
gxt的Dispatcher类就是将应用级事件分发到各个控制器之中,在程序启动时需要将控制器加入到Dispatcher之中。
控制器Controller类包含其所支持的事件的列表,并包含相应的视图。在初始化时需要将支持的事件注册到事件列表之中。Dispatcher在分发事件时会调用
controller类的handleEvent方法,在handleEvent里面依据不同的事件来使用相应的方法来处理。controller类可以向视图发送事件。
View类包含一个Controller用于向上引发事件,view类包含其所支持的事件处理程序。
在gxt框架之中,controller类负责职责5,而view类可以用于负责职责2。很方便的做到职责的分离。
既然有了MVC,为何又提出MVP?在我看来MVP不过是MVC加上一些约束而已,MVC之中,V可以和M有联系,但是在MVP之中,V不必知道M,在MVP之中强制要求职责2和职责5的分离以及M和V的分离。而在MVC中,某些情况下V还可以调用M来获取数据,但这样没能完全分离职责2和职责5。
当事件到来时在MVC和MVP之中,都需要将事件分发给相应的C或者P
在C和P之中都需和M交互获取数据。但是在V之中有很大的不同,在MVC之中,V可以响应某些事件并可以调用M获取数据。
而在MVP之中,V完全是被动的,P通过V提供的展示接口来控制V并通过接口的参数传递数据给V。因为V中需要将任何事件的处理委托给P,所以V需要暴露回调接口给P。
举一个例子,MVP中登录窗口V的接口定义:
void setUserName(String username);
String getUserName();
void setPassword(String pwd);
String getPassword();
void showErrorMsg(String msg);
void setLoginCallback(EventHandler handler);
Widget asWidget();
}
实现
private final TextField < String > username = new TextField < String > ();
private final TextField < String > password = new TextField < String > ();
private final Button login = new Button();
public LoginViewImpl() {
FormPanel layout = new FormPanel();
initWidget(layout);
layout.add(username);
layout.add(password);
layout.add(login);
layout.setSize( 500 , 500 );
password.setPassword( true );
username.setFieldLabel( " username: " );
password.setFieldLabel( " password: " );
login.setText( " 登录 " );
}
public void setUserName(String username) {
this .username.setValue(username);
}
public String getUserName() {
return this .username.getValue();
}
public void setPassword(String pwd) {
this .password.setValue(pwd);
}
public String getPassword() {
return this .password.getValue();
}
public void showErrorMsg(String msg) {
com.google.gwt.user.client.Window.alert(msg);
}
public void setLoginCallback( final EventHandler handler) {
login.addListener(Events.Select, new Listener < ButtonEvent > () {
public void handleEvent(ButtonEvent be) {
handler.handle();
}
});
}
}
其中EventHandler是当用户点击登录按钮时的回调。EventHandler的实现是在LoginPresenter之中的,对于LoginView的实现只需在onclick事件中回调即可。
对应的presenter代码如下:
private LoginView view;
private static final String LOGIN_CHECK_URL = " ../check " ;
public LoginPresenter() {
this .view = new LoginViewImpl();
view.setLoginCallback( new LoginEventHandler());
}
/*
* 初始化view
*/
public void go(HasWidgets container) {
container.clear();
container.add(view.asWidget());
}
// 处理登录事件
private class LoginEventHandler implements EventHandler {
public void handle() {
//调用业务层 验证
}
}
}
前文说过MVP只不过是MVC加上约束而已,所以在实现时利用现有的MVC现有的框架,只要让V变成MVP中的V都可以看作是MVP。
MVP在b/s中的实现,以spring MVC为例,实际上当我们利用spring mvc的 ModelAndView类向view传递数据时,只要view中不包含对m的使用,就可以看作是MVP。只不过在b/s架构之中每次事件都需重新绘制整个页面,所以这里面隐含所有的视图只包含render接口来绘制所有的部件。
MVP在c/s之中实现,在c/s之中事件到来时只需绘制部分界面,所以这里的V可以提供一个可读性较强的接口。
我们看看在GWT中如何实现MVP,在GWT之中有个EventBus的概念,EventBus是全局应用事件管理者,注意这里是全局应用事件,这是指在不同view中都可以引发的事件。某些事件不需在跨view引发就不要放在EventBus之中。EventBus管理事件和对应的事件的处理程序。
通常存在AppController类来负责初始化EventBus,并负责控制整个程序的流程。
Presenter类类似controller,负责展示相应的view,并处理view中引发的事件。
view,每个presenter都有一个自定义的view接口,view都是被动的被presenter使用。
总结,任何模式的核心都是隔离变化,为了隔离变化必须分离职责。所以MVC和MVP从本质都是一样,理解每个部分的职责才能真正理解MVC和MVP。