MVC并不属于GOF的23个设计模式之列,但是它在GOF的书中作为一个重要的例子被提出来,并给予了很高的评价。一般的来讲,我们认为GOF的23个模式是一些中级的模式,在它下面还可以抽象出一些更为一般的低层的模式,在其上也可以通过组合来得到一些高级的模式。MVC就可以看作是一些模式进行组合之后的结果。
MVC定义:即Model-View-Controller,把一个应用的输入、处理、输出流程按照Model、View、Controller的方式进行分离,这样一个应用被分成三个层,即模型层、视图层、控制层。
MVC模式结构如下:
图1-1 MVC模式组件类型的关系和功能
模型(Model):封装的是数据源和所有基于对这些数据的操作。在一个组件中,Model往往表示组件的状态和操作状态的方法。
视图(View):封装的是对数据源Model的一种显示。一个模型可以由多个视图,而一个视图理论上也可以同不同的模型关联起来。
控制器(Control):封装的是外界作用于模型的操作。通常,这些操作会转发到模型上,并调用模型中相应的一个或者多个方法。一般Controller在Model和View之间起到了沟通的作用,处理用户在View上的输入,并转发给Model。这样Model和View两者之间可以做到松散耦合,甚至可以彼此不知道对方,而由Controller连接起这两个部分。
MVC应用程序总是由这三个部分组成。Event(事件)导致Controller改变Model或View,或者同时改变两者。只要Controller改变了Model的数据或者属性,所有依赖的View都会自动更新。类似的,只要Controller改变了View,View会从潜在的Model中获取数据来刷新自己。MVC模式最早是smalltalk语言研究团提出的,应用于用户交互应用程序中。
在设计模式中,MVC实际上是一个比较高层的模式,它由多个更基本的设计模式组合而成,Model-View的关系实际上是Observer模式,模型的状态和试图的显示相互响应,而View-Controller则是由Strategy模式所描述的,View用一个特定的Controller的实例来实现一个特定的响应策略,更换不同的Controller,可以改变View对用户输入的响应。而其它的一些设计模式也很容易组合到这个体系中。比如,通过Composite模式,可以将多个View嵌套组合起来;通过FactoryMethod模式来指定View的Controller,等等。在GOF书的 Introduction中,有一小节是“Design Patterns in Smalltalk MVC”即介绍在MVC模式里用到的设计模式。它大概向我们传达了这样的信息:合成模式+策略模式+观察者模式约等于MVC模式(当然MVC模式要多一些 东西)。
使用MVC的好处,一方面,分离数据和其表示,使得添加或者删除一个用户视图变得很容易,甚至可以在程序执行时动态的进行。Model和View能够单独的开发,增加了程序了可维护性,可扩展性,并使测试变得更为容易。另一方面,将控制逻辑和表现界面分离,允许程序能够在运行时根据工作流、用户习惯或者模型状态来动态选择不同的用户界面。因此,MVC模式广泛用于Web程序、GUI程序的架构。
这里实现一个Java应用程序。当用户在图形化用户界面输入一个球体的半径时,程序将显示该球体的体积与表面积。我们首先利用基本MVC模式实现以上程序,然后利用不同数量的模型、视图、控制器结构来扩展该程序。
Model与View的交互使用Observer模式。Model类必须继承Observable类,View类必须实现接口Observer。正是由于实现了上述结构,当Model发生改变时(Controller改变Model的状态),Model就会自动刷新与之相关的View。Controller类主要负责新建Model与View,将view与Mode相关联,并处理触发模型值改变的事件。
[java] view plain copy
- import java.util.Observable;
- //Sphere.java:Model类
- //必须继承Observable,在Observable类中,方法addObserver()将视图与模型相关联
- class Sphere extends Observable {
- private double myRadius;
- public void setRadius(double r) {
- myRadius = r;
- this.setChanged(); //指示模型已经改变
- this.notifyObservers(); //通知各个视图,从父继承的方法
- }
- //......
- }
[java] view plain copy
- import java.util.Observable;
- import java.util.Observer;
- import javax.swing.JPanel;
- //TextView.java:View视图类
- //当模型Sphere类的状态发生改变时,与模型相关联的视图中的update()方法
- //就会自动被调用,从而实现视图的自动刷新
- public class TextView extends JPanel implements Observer {
- @Override
- public void update(Observable o, Object arg) {
- Sphere balloon = (Sphere) o;
- radiusIn.setText("" + f3.format(balloon.getRadius()));
- volumeOut.setText("" + f3.format(balloon.volume()));
- surfAreaOut.setText("" + f3.format(balloon.surfaceArea()));
- }
- //......
- }
[java] view plain copy
- import java.awt.Container;
- import java.awt.event.ActionEvent;
- import javax.swing.JFrame;
- import javax.swing.JTextField;
- // SphereWindow.java:Controller类
- // 它主要新建Model与View,将view与Mode相关联,并处理事件
- public class SphereWindow extends JFrame {
- public SphereWindow() {
- super("Spheres: volume and surface area");
- model = new Sphere(0, 0, 100); //新建Model
- TextView view = new TextView(); //新建View
- model.addObserver(view); //将View与Model相关联
- view.update(model, null); //初始化视图,以后就会根据Model的变化自动刷新
- view.addActionListener(this);
- Container c = getContentPane();
- c.add(view);
- }
- //处理事件:改变Model的状态
- public void actionPerformed(ActionEvent e) {
- JTextField t = (JTextField) e.getSource();
- double r = Double.parseDouble(t.getText());
- model.setRadius(r);
- }
- //......
- }
这种MVC模式的程序具有极其良好的可扩展性。它可以轻松实现一个模型的多个视图;可以采用多个控制器;可以实现当模型改变时,所有视图自动刷新;可以使所有的控制器相互独立工作。
比如实现一个模型、两个视图和一个控制器的程序。当用户在图形化用户界面输入一个球体的半径,程序除显示该球体的体积与表面积外,还将图形化显示该球体。该程序的4个类之间的示意图如下:
图1-2 一个模型、两个视图和一个控制器的基本结构
其中Model类及View1类根本不需要改变。对于Controller中的SphereWindows类,只需要增加另一个视图,并与Model发生关联即可。其关键实现代码为:
[java] view plain copy
- public SphereWindow() {
- super("Spheres: volume and surface area");
- model = new Sphere(0, 0, 100);
- TextView tView = new TextView();
- model.addObserver(tView);
- tView.addActionListener(this);
- tView.update(model, null);
- GraphicsView gView = new GraphicsView(); //增加了一个视图
- model.addObserver(gView); //与Model关联
- gView.update(model, null);
- Container c = getContentPane();
- c.setLayout(new GridLayout(1, 2));
- c.add(tView);
- c.add(gView);
- }
程序输出结果如下图:
图1-3 输出结果
在上面的程序中,我们只能通过键盘输入球体半径,现在我们修改以上程序,利用鼠标放大、缩小右边的球体图形,左边的半径、体积、表面积值同时跟着改变。此时的MVC模式为一个模型、两个视图和两个控制器,其结构如下:
图1-3 一个模型、两个视图和两个控制器的基本结构
其中Sphere、TextView与GraphicsView类与前面完全一样。在主程序SphereWindows中,该类这时不是直接作为Controller,它控制Controller1与Controller2的新建。该程序的关键代码为:
[java] view plain copy
- public SphereWindow() {
- super("Spheres: volume and surface area");
- Sphere model = new Sphere(0, 0, 100);
- TextController tController = new TextController(model);
- GraphicsController gController = new GraphicsController(model);
- Container c = getContentPane();
- c.setLayout(new GridLayout(1, 2));
- c.add(tController.getView());
- c.add(gController.getView());
- }
当程序SphereWindow运行时,将鼠标移动到球体的外圆处,点击拖动即可实现球体的放大与缩小,同时球体半径、表面积与球体积也同时变化。
从上面介绍可以看出,通过MVC模式实现与图形用户化界面相关的应用程序具有极其良好的可扩展性。
MVC模式基本实现过程为:
1. 顶端控制器(如Java中的main程序入口)要新建模型;
2. 控制器要新建一个或多个视图对象,并将它们与模型相关联;
3. 控制器改变模型的状态;
4. 当模型的状态改变时,模型将会自动刷新与之相关的视图。
Java Swing、Java EE、Struts框架等都是使用MVC模式的典范。
Swing号称是完全按照MVC的思路来进行设计的。在设计开始前,Swing的希望能够达到的目标就包括:
模型驱动(Model-Driven)的编程方式。
提供一套单一的API,但是能够支持多种视感look-and-feel),为用户提供不同的界面。
很自然的可以发现,使用MVC模式能够有助于实现上面的这两个目标。
严格的说,Swing中的MVC实际上是MVC的一个变体:M-VC。 Swing中只显示的定义了Model接口,而在一个UI对象中集成了视图和控制器的部分机制。View和Control比较松散的交叉组合在一起,而更多的控制逻辑是在事件监听者部分引入的。
但是,这并没有妨碍在Swing中体现MVC的精髓。事实上,在Swing的开发初期,Swing确实是按照标准的MVC模式来设计的,但是很快的问题就出现了:View和Controller实际上是紧密耦合的,很难作出一个能够适应不同View的一般化的Controller来,而且,一般也没有很大的必要。
在Swing中基本上每一个组件都会有对应的Model对象。但其并不是一一对应的,一个Model接口可以为多个Swing对向服务,例如:JProgressBar,JScrollBar,JSlider这三个组件使用的都是BoundedRangeModel接口。这种模型的共享更能够充分的体现MVC的内涵。除了Model接口外,为了实现多个视感间的自由切换,每个Swing组件还包含一个UI接口,也就是View-Controller,负责对组件的绘制和接受用户输入。
Model-View是Subject和Obverser的关系,因而,模型的改变必须要在UI对象中体现出来。Swing使用了JavaBeans的事件模型来实现这种通知机制。具体而言,有两种实现办法,一是仅仅通知事件监听者状态改变了,然后由事件监听者向模型提取必要的状态信息。这种机制对于事件频繁的组件很有效。另外的一种办法是模型向监听者发送包含了已改变的状态信息的通知给UI。这两种方法根据其优劣被分别是现在不同的组件中。比如在JScollBar中使用的是第一种方法,在JTable中使用的是第二种方法。而对Model而言,为了能够支持多个View,它并不知道具体的每一个View。它维护一个对其数据感兴趣的Obverser的列表,使得当数据改变的时候,能够通知到每一个Swing组件对象。
在J2EE中,Sun更是将MVC提升到了一个体系结构模式的高度,这儿的MVC的含义就更为广泛了。与Swing中不同的是,在这儿MVC的各个部件不再是单纯的类或者接口,而是应用程序的一个组成部分!
在J2EE Blueprint中,Sun推荐了一种基于MVC的J2EE程序的模式。对于企业级的分布式应用程序而言,它更需要支持多种形式的用户接口。比如,网上商店需要一个HTML的界面来同网上的客户打交道,WML的界面可以提供给无线用户,管理者可能需要传统的基于Swing的应用程序来进行管理,而对商业伙伴,基于XML的Web服务可能对他们更为方便。
MVC无疑是这样一个问题的有效的解决方法,通过从控制和显示逻辑分离出核心的数据存取功能,形成一个Model模块,能够让多种视图来共享这个Model。
在J2EE中有几个核心的技术,JSP,JavaBean,Servlet,EJB。这里SessionBean,EntityBean构成了J2EE构架的基石。JSP能够生成HTML,WML甚至XML,它对应于Web应用程序中的View部分。EJB作为数据库与应用程序的中介,提供了对数据的封装。一般EntityBean封装的是数据,SessionBean是封装的是对数据的操作。这两个部分合起来,对应于Web应用程序的Model部分。在技术上,JSP能够直接对EJB进行存取,但这并不是好办法,那样会混淆程序中的显示逻辑和控制逻辑,使得JSP的重用性能降低。这时候有两种解决方法,通过JavaBean或者Servlet作为中介的控制逻辑,对EJB所封装的数据进行存取。这时,JavaBean或者Servlet对应于Web引用程序中的Controller部分。两种类型的Controller各有其优缺点:JSP同Servlet的交互不容易规范化,使得交互的过程变得复杂,但是Servlet可以单独同用户交互,实际上JSP的运行时状态就是Servlet;而由于JavaBean的规范性,JSP同JavaBean的交互很容易,利用JavaBean的get/set方法,JSP不需要过多的语句就可以完成数据的存取,这能够让JSP最大限度的集中在其视图功能上,而且,在桌面应用程序中使用JavaBean也很容易,而用Servlet就相对麻烦许多。根据不同的问题背景,可以选取不同的Controller,有时候也可以两者混合使用,或者直接在Servlet中调用JavaBean。
J2EE中的MVC是一个大的框架,这时我们往往把它不再看作为设计模式,而是作为体系结构模式的一个应用了。
Struts框架只实现了MVC的View和Controller两个部分,Model部分需要开发者自己来实现,Struts提供了抽象类Action使开发者能将Model应用于Struts框架中。
MVC的优点:
(1)最重要的是应该有多个视图对应一个模型的能力。在目前用户需求的快速变化下,可能有多种方式访问应用的要求。例如,订单模型可能有本系统的订单,也有网上订单,或者其他系统的订单,但对于订单的处理都是一样,也就是说订单的处理是一致的。按MVC设计模式,一个订单模型以及多个视图即可解决问题。这样减少了代码的复制,即减少了代码的维护量,一旦模型发生改变,也易于维护。 其次,由于模型返回的数据不带任何显示格式,因而这些模型也可直接应用于接口的使用。
(2)由于一个应用被分离为三层,因此有时改变其中的一层就能满足应用的改变。一个应用的业务流程或者业务规则的改变只需改动MVC的模型层。
(3)控制层的概念也很有效,由于它把不同的模型和不同的视图组合在一起完成不同的请求,因此,控制层可以说是包含了用户请求权限的概念。
(4)它还有利于软件工程化管理。由于不同的层各司其职,每一层不同的应用具有某些相同的特征,有利于通过工程化、工具化产生管理程序代码。
MVC的不足体现在以下几个方面:
(1)增加了系统结构和实现的复杂性。对于简单的界面,严格遵循MVC,使模型、视图与控制器分离,会增加结构的复杂性,并可能产生过多的更新操作,降低运行效率。
(2)视图与控制器间的过于紧密的连接。视图与控制器是相互分离,但确实联系紧密的部件,视图没有控制器的存在,其应用是很有限的,反之亦然,这样就妨碍了他们的独立重用。
(3)视图对模型数据的低效率访问。依据模型操作接口的不同,视图可能需要多次调用才能获得足够的显示数据。对未变化数据的不必要的频繁访问,也将损害操作性能。
(4) 目前,一般高级的界面工具或构造器不支持MVC模式。改造这些工具以适应MVC需要和建立分离的部件的代价是很高的,从而造成使用MVC的困难。