【IT168 技术文章】很多时候,对于一个设计来说(软件上的,建筑上的,或者它他工业上的),经验是至关重要的。好的经验给我们以指导,并节约我们的时间;坏的经验则给我们以借鉴,可以减少失败的风险。然而,从知识层面上来讲,经验只是作为一种工作的积累而存在于个人的大脑中的,很难被传授或者记录。为了解决这样的问题,人们提出了所谓的模式的概念。所谓模式,是指在一个特定背景下,反复出现的问题解决方案。模式是经验的文档化。
软件模式的概念现在比较的广泛,涉及到分析,设计,体系结构,编码,测试,重构等软件构造生命期中的各个部分。这儿主要讨论的是设计模式,指的是在软件设计过程中反复出现的一些问题的解决方法了。不过我们一般在提到设计模式的时候,一般都是指GOF的经典书《Design Pattern--Elements of Reusable Object-Oriented Software》出现的23个模式,因而,它是具体的针对于面向对象软件设计过程的。
从全局上看来,模式代表了一种语言,一种被文档化的经验,甚至是一种文化。往往很多不方便描叙,或者描叙起来很复杂的问题,用模式语言来叙说,会让听者产生心领神会的感觉。当然,这需要交流双方都能够很好地把握模式语言的含义。然而,这并不是一件容易的事情。模式在各个人的理解上往往存在差异,这篇文章旨在从一个具体的应用角度:Java类库,来阐叙设计模式。并结合具体的例子,希望能够加深大家对设计模式的理解。
这儿说的Java类库,其实并没有局限于JDK本身,还包括了一些其他的类库中的例子,比如JAXP等(当然,下一个版本的JDK中也会包含JAXP了)。其实设计模式的思想现在应用的如此广泛,无论在什么样的设计中,只要稍微大一点的设计,都可以找到很多很多设计模式的踪迹,或者说都不可避免的用到设计模式。下面所讲的设计模式,大部分都是GOF的那部经典中出现过的23个模式,然而,还有一些,比如MVC,并不属于那里。一般的来讲,我们认为GOF的23个模式是一些中级的模式,在它下面还可以抽象出一些更为一般的低层的模式,在其上也可以通过组合来得到一些高级的模式。当然,这儿的低中高的区别,如同区别不同的语言一样,并没有优劣之分,仅仅是在应用层面上的区别。
Observer模式
Observer模式的功用,是希望两个(或多个)对象,我们称之为Subject和Observer,当一方的状态发生改变的时候,另一方能够得到通知。也就是说,作为Observer的一方,能够监视到Subject的某个特定的状态变化,并为之做出反应。一个简单的例子就是:当一个用户视图中的数据被用户改变后,后端的数据库能够得到更新,而当数据库被其他方式更新后,用户视图中的数据显示也会随之改变。
图一:Obverser模式的类图
在JDK中实际上有一个对Observer模式的简单的实现:就是类java.util.Observerable和接口java.util.Observer。java.util.Observerable类对应于Subject,而java.util.Observer就是观察者了。JDK中并没有把这两个部分都设计为接口,而是让类java.util.Observerable提供了部分的实现,简化了许多编程的工作。当然,这也减少了一定的灵活性。
下面列出了Observer和Observeral的函数列表,及其简单的功能说明
java.util.Observer:
public void update(Observable obs, Object obj)
java.util.Observer 接口很简单,只定义了这一个方法,狭义的按照Observer模式的说法,Observer应该在这个方法中调用Subject的getXXX()方法来取得最新的状态,而实际上,你可以只是在其中对Subject的某些事件进行响应。这便是Java中的代理事件模型的一个雏形--对事件进行响应。只不过,在Observer模式中将事件特定化为某个状态/数据的改变了。
java.util.Observable
public void addObserver(Observer obs)
向Subject注册一个Observer。也就是把这个Observer对象添加到了一个java.util.Observable内部的列表中。在JDK中对于这个列表是简单的通过一个java.util.Vector类来实现的,而实际上,在一些复杂的Observer模式的应用中,需要把这个部分单另出来形成一个Manager类,来管理Subject和Observer之间的映射。这样,Subject和Observer进一步的被解藕,程序也会具有更大的灵活性。
public void deleteObserver(Observer obs)
从Subject中删除一个已注册了Observer的引用。
public void deleteObservers()
从Subjec中删除所有注册的Observer的引用。
public int countObservers()
返回注册在Subject中的Observer个数。
protected void setChanged()
设置一个内部的标志以指明这个Ovserver的状态已经发生改变。注意这是一个protected方法,也就是说只能在Observer类和其子类中被调用,而在其它的类中是看不到这个方法的。
protected void clearChanged()
清除上叙的内部标志。它在notifyObservers()方法内部被自动的调用,以指明Subject的状态的改变已经传递到Ovserver中了。
public boolean hasChanged()
确定Subject的状态是否发生了改变。
public void notifyObservers(Object obj)
它首先检查那个内部的标志,以判断状态是否改变,如果是的话,它会调用注册在Subject中的每个Observer的update()方法。在JDK中这个方法内部是作为synchronized来实现的,也就是如果发生多个线程同时争用一个java.util.Observerable的notifyObservers()方法的话,他们必须按调度的等待着顺序执行。在某些特殊的情况下,这会有一些潜在的问题:可能在等待的过程中,一个刚刚被加入的Observer会被遗漏没有被通知到,而一个刚刚被删除了的Observer会仍然收到它已经不想要了的通知。
public void notifyObservers()
等价于调用了notifyObservers(null)。
因而在Java中应用Observer就很简单了,需要做的是:让需要被观察的Subject对象继承java.util.Observerable,让需要观察的对象实现java.util.Observer接口,然后用java.util.Observerable的addObserver(Observer obj)方法把Observer注册到Subject对象中。这已经完成了大部分的工作了。然后调用java.util.Observerable的notifyObservers(Object arg)等方法,就可以实现Observer模式的机理。我们来看一个简单使用了这个模式的例子。这个例子有三个类:FrameSubject,DateSubject,FrameObject和EntryClass,FrameSubject中用户可以设置被观察的值,然后自动的会在FrameObject中显示出来,DateSubject封装被观察的值,并且充当Observer模式中的Subject。
2
3 …………..
4
5 //因为无法使用多重继承,这儿就只能使用对象组合的方式来引入一个
6
7 //java.util.Observerable对象了。
8
9 DateSubject subject=new DateSubject();
10
11 //这个方法转发添加Observer消息到DateSubject。
12
13 public void registerObserver(java.util.Observer o){
14
15 subject.addObserver(o);
16
17 }
18
19 //数据改变,事件被触发后调用notifyObservers()来通知Observer。
20
21 void jButton1_actionPerformed(ActionEvent e) {
22
23 subject.setWidthInfo(Integer.parseInt(jTextField1.getText()));
24
25 subject.setHeightInfo(Integer.parseInt(jTextField2.getText()));
26
27 subject.notifyObservers();
28
29 }
30
31 ……………
32
33 }
34
35 public class DateSubject extends Observable {
36
37 //封装被观察的数据
38
39 private int widthInfo;
40
41 private int heightInfo;
42
43 public int getWidthInfo() {
44
45 return widthInfo;
46
47 }
48
49 public void setWidthInfo(int widthInfo) {
50
51 this.widthInfo = widthInfo;
52
53 //数据改变后,setChanged()必须被调用,否则notifyObservers()方法会不起作用
54
55 this.setChanged();
56
57 }
58
59 public void setHeightInfo(int heightInfo) {
60
61 this.heightInfo = heightInfo;
62
63 this.setChanged();
64
65 }
66
67 public int getHeightInfo() {
68
69 return heightInfo;
70
71 }
72
73 }
74
75 public class FrameObserver extends JFrame implements java.util.Observer {
76
77 …………..
78
79 //观察的数据
80
81 int widthInfo=0;
82
83 int heightInfo=0;
84
85 //在update()方法中实现对数据的更新和其它必要的反应。
86
87 public void update(Observable o, Object arg) {
88
89 DateSubject subject=(DateSubject) o;
90
91 widthInfo=subject.getWidthInfo();
92
93 heightInfo=subject.getHeightInfo();
94
95 jLabel1.setText("The heightInfo from subject is: ");
96
97 jLabel3.setText(String.valueOf(heightInfo));
98
99 jLabel2.setText("The widthInfo from subject is: ");
100
101 jLabel4.setText(String.valueOf(widthInfo));
102
103 }
104
105 …………….
106
107 }
108
109 public class EntryClass {
110
111 public static void main(String[] args) {
112
113 ……………..
114
115 FrameSubject frame = new FrameSubject();
116
117 FrameObserver frame2=new FrameObserver();
118
119 //在Subject中注册Observer,将两者联系在一起
120
121 frame.registerObserver(frame2);
122
123 …………..
124
125 frame.setVisible(true);
126
127 frame2.setVisible(true);
128
129 ……………..
130
131 }
132
133 }
134
135
我认为在JDK中这个Observer模式的实现,对于一般的Observer模式的应用,已经是非常的足够了的。但是一方面它用一个类来实现了Subject,另一方面它使用Vector来保存Subject对于Observer的引用,这虽然简化了编程的过程,但会限制它在一些需要更为灵活,复杂的设计中的应用,有时候(虽然这种情况不多),我们还不得不重新编写新的Subject对象和额外的Manager对象来实现更为复杂的Observer模式的应用。
随着现代软件工业的不断进步,软件系统的规模的日益扩大,越来越需要对某些个不断出现的问题进行模式化思维,以成功的经验或者失败的教训来减少软件开发失败的风险。模式代表了一种文档化的经验,它为某一类的问题提供了最好(或者说很好)的解决方案,使得即使不是经验丰富的软件工程师,也能够根据模式来构建相对成功的系统。本节给出的一个Obverser模式的示例,比较好的说明了这一点。Obverser模式主要解决在对象间的状态映射或者镜像的问题。
在设计一般用途的软件的时候,在C或者C++语言中,用的很多的一个技巧就是回调函数(Callback),所谓的回调函数,意指先在系统的某个地方对函数进行注册,让系统知道这个函数的存在,然后在以后,当某个事件发生时,再调用这个函数对事件进行响应。在C或者C++中,实现的回调函数方法是使用函数指针。但是在Java中,并不支持指针,因而就有了Command模式,这一回调机制的面向对象版本。
Command模式用来封装一个命令/请求,简单的说,一个Command对象中包含了待执行的一个动作(语句)序列,以执行特定的任务。当然,并不是随便怎么样的语句序列都可以构成一个Command对象的,按照Command模式的设计,Command对象和它的调用者Incvoker之间应该具有接口约定的。也就是说,Invoker得到Command对象的引用,并调用其中定义好的方法,而当Command对象改变(或者是对象本身代码改变,或者干脆完全另外的一个Command对象)之后,Invoker中的代码可以不用更改。这样,通过封装请求,可以把任务和任务的实现加以分离。
图二:Command模式的类图
而对于请求的处理又有两种不同的方法,一种是Command只充当代理,将请求转发给某个接受者对象,还有一种是Command对象自己处理完所有的请求操作。当然,这只是两个极端,更多的情况是Command完成一部分的工作,而另外的一部分这则交给接受者对象来处理。
在新的JDK的代理事件模型中,就可以看作是这样的一个Command模式。在那个模型中,一个事件监听者类EventListener监听某个事件,并根据接口定义,实现特定的操作。比如,当用Document对象的addDocumentListener(DocumentListener listener) 方法注册了一个DocumentListener后,以后如果在Document对象中发生文本插入的事件,DocumentListener中实现的insertUpdate(DocumentEvent e)方法就会被调用,如果发生文本删除事件,removeUpdate(DocumentEvent e)方法就会被调用。怎么样,想想看,这是不是一个Command模式的应用呢?
然而,最经典的Command模式的应用,莫过于Swing中的Action接口。Action实际上继承的是ActionListener,也就是说,它也是一个事件监听者(EventListener)。但是Action作为一种ActionListener的扩展机制,提供了更多的功能。它可以在其中包含对这个Action动作的一个或者多个文字的或图标的描叙,它提供了Enable/Disable的功能许可性标志。并且,一个Action对象可以被多个Invoker,比如实现相同功能的按钮,菜单,快捷方式所共享。而这些Invoker都知道如何加入一个Action,并充分利用它所提供的扩展机制。可以说,在这儿Action更像一个对象了,因为它不仅仅提供了对方法的实现,更提供了对方法的描叙和控制。可以方便的描叙任何的事务,这更是面向对象方法的威力所在。
下面我们看一个Command模式的应用的例子。假设要实现这样的一个任务:Task Schedule。也就是说,我想对多个任务进行安排,比如扫描磁盘,我希望它每1个小时进行一次,而备份数据,我希望它半个小时进行一次,等等等等。但是,我并不希望作为TaskSchedule的类知道各个任务的细节内容,TaskSchedule应该只是知道Task本身,而对具体的实现任务的细节并不理会。因而在这儿,我们就需要对TaskSchedule和Task进行解耦,将任务和具体的实现分离出来,这不正是Command模式的用武之地吗?
图三:Command模式的应用例子
程序清单:
2 public interface Task {
3 public void taskPerform();
4 }
5 //具体的实现了Task接口的子类,实现特定的操作。
6 public class BackupTask implements Task{
7 public void taskPerform(){
8 System.out.println("Backup Task has been performed");
9 }
10 }
11 //具体的实现了Task接口的子类,实现特定的操作。
12 public class ScanDiskTask implements Task{
13 public void taskPerform(){
14 System.out.println("ScanDisk Task has been performed");
15 }
16 }
17 //一个封装了Task的一个封装类,提供了一些与Task相关的内容,也可以把这些内容
18 //这儿不过为了突出Command模式而把它单另出来,实际上可以和Task合并。
19 public class TaskEntry {
20 private Task task;
21 private long timeInterval;
22 private long timeLastDone;
23 public Task getTask() {
24 return task;
25 }
26 public void setTask(Task task) {
27 this.task = task;
28 }
29 public void setTimeInterval(long timeInterval) {
30 this.timeInterval = timeInterval;
31 }
32 public long getTimeInterval() {
33 return timeInterval;
34 }
35 public long getTimeLastDone() {
36 return timeLastDone;
37 }
38 public void setTimeLastDone(long timeLastDone) {
39 this.timeLastDone = timeLastDone;
40 }
41 public TaskEntry(Task task,long timeInteral){
42 this.task=task;
43 this.timeInterval =timeInteral;
44 }
45 }
46 //调度管理Task的类,继承Thread只是为了调用其sleep()方法,
47 //实际上,如果真的作Task调度的话,每个Task显然应该用单独的Thread来实现。
48 public class TaskSchedule extends java.lang.Thread {
49 private java.util.Vector taskList=new java.util.Vector();
50 private long sleeptime=10000000000l;//最短睡眠时间
51 public void addTask(TaskEntry taskEntry){
52 taskList.add(taskEntry);
53 taskEntry.setTimeLastDone(System.currentTimeMillis());
54 if (sleeptime>taskEntry.getTimeInterval())
55 sleeptime=taskEntry.getTimeInterval();
56 }
57 //执行任务调度
58 public void schedulePermorm(){
59 try{
60 sleep(sleeptime);
61 Enumeration e = taskList.elements();
62 while (e.hasMoreElements()) {
63 TaskEntry te = (TaskEntry) e.nextElement();
64 if (te.getTimeInterval() + te.getTimeLastDone() <
65 System.currentTimeMillis()) {
66 te.getTask().taskPerform();
67 te.setTimeLastDone(System.currentTimeMillis());
68 }
69 }
70 }catch (Exception e1){
71 e1.printStackTrace();
72 }
73 }
74 public static void main (String args[]){
75 TaskSchedule schedule=new TaskSchedule();
76 TaskEntry taks1=new TaskEntry(new ScanDiskTask(),10000);
77 TaskEntry taks2=new TaskEntry(new BackupTask(),3000);
78 schedule.addTask(taks1);
79 schedule.addTask(taks2);
80 while (true){
81 schedule.schedulePermorm();
82 }
83 }
84 }
85
程序本身其实没有多大的意义,因而,程序在编码的时候也只是用的最简单的方法来实现的,如果要做一个真正的TaskSchedule的话,这个程序除了结构上的,其它没有什么好值得参考的了。
基本上来说,AbstractFacotry模式和FactoryMethod模式所作的事情是一样的,都是用来创建与具体程序代码无关的对象,只是面对的对象层次不一样,AbstractFactory创建一系列的对象组,这些对象彼此相关。而FactoryMethod往往只是创建单个的对象。
再开始这两个模式之前,有必要先陈叙一个在设计模式,或者说在整个面向对象设计领域所遵循的一个设计原则:针对接口编程,而不是针对具体的实现。这个思想可以说是设计模式的基石之一。现在的很多对象模型,比如EJB,COM+等等,无不是遵照这个基本原则来设计的。针对接口编程的好处有很多,通过接口来定义对象的抽象功能,方便实现多态和继承;通过接口来指定对象调用之间的契约,有助于协调对象之间的关系;通过接口来划分对象的职责,有助于寻找对象,等等。
AbstractFactory和FactoryMethod,还有其他的一些创建型的设计模式,都是为了实现这个目的而设计出来的。它们创建一个个符合接口规范的对象/对象组,使得用同一个Factory创建出来的对象/对象组可以相互替换。这种可替换性就称为多态,是面向对象的核心思想之一。而多态,是通过动态绑定来实现的。
图四:AbstractFactory模式的类图
客户程序使用具体的AbstractFacotry对象(ConcreteFactoryX)调用CreateProductX()方法,生成具体的ConcreteProductX。每个AbstractFactory所能生成的对象,组成一个系列的对象组,他们可能是相互相关的,紧耦合的。应为各个AbstractFactory对象所能够生成的对象组都遵循一组相同的接口(AbstractProductX),因而当程序是针对接口进行编程的时候,这些实现方法各不相同的对象组却可以相互的替换。
实际上,客户程序本身并不关心,也不知道具体使用的是那些产品对象。它甚至能够不理会到底是哪个AbstractFactory对象被创建。在这种情况下,你可能会问,那么一个AbstractFactory又该如何生成呢?这时候,就该用该FactoryMethod模式了。
前面有说过,AbstractFactory着重于创建一系列相关的对象,而这些对象与具体的AbstractFactory相关。而FactoryMethod则着重于创建单个的对象,这个对象决定于一个参数或者一个外部的环境变量的值;或者,在一个抽象类中定义一个抽象的工厂方法(也成为虚拟构造器),然后再实现的子类中返回具体的产品对象。
FactoryMethod可以借助一个参数或者一个外部的标志来判断该具体生成的哪一个子类的实例。比如对于不同的具体情况,需要有不同的AbstractFactory来生成相应的对象组。这时候,FactoryMethod通常作为一个AbstractFactory对象的静态方法出现,使得其能够在具体的对象被创建之前就能够被调用。
在JAVA中,应用这两个模式的地方实在太多,下面我们来看一个在JAXP中这两个模式的应用。JAXP是用来处理XML文档的一个API。我们都知道XML文件的一个特点就是其平台无关,流通性能好。因而往往也需要处理他们的程序具有更好的平台无关性。Java语言是一个比较好的平台无关语言,可以作为一个选择,但是对XML进行解析的解析器确有很多。有时候需要在不同的解析器之间进行切换,这时候,JAXP的良好设计就能够体现出来了。它能够允许在不同解析器之间竟进行切换的时候,不用更改程序的代码。
我们就拿JAXP中的DOM解析器来作为例子,来例示AbstractFactory和FactoryMethod的用法。
图五:DOM中工厂模式的应用
上图中为了方便起见,只画出了抽象类和接口,DocumentBuilderFactory和DocumentBuilder都是抽象类。
DocumentBuilderFactory的静态方法newInstance()方法根据一个外部的环境变量javax.xml.parsers.DocumentBuilderFactory的值来确定具体生成DocumentBuilderFactory的哪一个子类。这儿的newInstance()是一个工厂方法。当DocumentBuilderFactory被创建后,可以调用其newDocumentBuilder()来创建具体一个DocumentBuilder的子类。然后再由DocumentBuilder来生成Document等DOM对象。
下面是创建一个DOM对象的代码片段:
2 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
3 //第二步:创建一个DocumentBuilder
4 DocumentBuilder db = dbf.newDocumentBuilder();
5 //第三步:解析XML文件得到一个Document对象
6 Document doc = db.parse(new File(filename));
7
8
在这儿,DocumentBuilder,Document,Node等等对象所组成的一个产品组,是和具体的DocumentBuilderFactory相关的。这也就是AbstractFactory模式的含义所在。
当然,FactoryMethod模式应用的很广。这是一个具体的例子,但他不应该限制我们的思路,FactoryMethod和AbstractFactory是解决面向对象设计中一个基本原则--面向接口编程的主要方法。
Singleton模式
Singleton模式要解决的是对象的唯一性问题。由Singleton模式创建的对象在整个的应用程序的范围内,只允许有一个对象的实例存在。这样的情况在Java程序设计的过程中其实并不少见,比如处理JDBC请求的连接池(Connection Pool),再比如一个全局的注册表(Register),等等,这都需要使用到Singleton,单件模式。
在Java中,最简单的实现Singleton模式的方法是使用static修饰符,static可以用在内部类上,也可以用在方法和属性上,当一个类需要被创建成Singleton时,可以把它所有的成员都定义成static,然后再用final和private来修饰其构造函数,使其不能够被创建和重载。这在程序语法上保证了只会有一个对象实例被创建。比如java.util.Math就是这样的一个类。
而Singleton模式所作的显然要比上面介绍的解决方法要复杂一些,也更为安全一些。它基本的思路也还是使用static变量,但是它用一个类来封装这个static变量,并拦截对象创建方法,保证只有一个对象实例被创建,这儿的关键在于使用一个private或者protected的构造函数,而且你必须提供这样的一个构造函数,否则编译器会自动的为你创建一个public的构造函数,这就达不到我们想要的目的了。
2
3 //保存唯一实例的static变量
4
5 static private Singleton _instance = null;
6
7 /*为了防止对象被创建,可以为构造函数加上private修饰符,但是这同样也防止了子类的对象被创建,因而,可以选用protected修饰符来替代private。*/
8
9 protected Singleton() {
10
11 // ...
12
13 }
14
15 //static方法用来创建/访问唯一的对象实例,这儿可以对对象的创建进行控制,使得可//以很容易的实现只允许指定个数的对象存在的泛化的Singleton模式。
16
17 static public Singleton instance() {
18
19 if(null == _instance) {
20
21 _instance = new Singleton();
22
23 }
24
25 return _instance;
26
27 }
28
29 // ...
30
31 }
32
33
对象创建的方法,除了使用构造函数之外,还可以使用Object对象的clone()方法,因而在Singleton中也要注意这一点。如果Singleton类直接继承于Object,因为继承于Object的clone()方法仍保留有其protected修饰,因而不能够被其他外部类所调用,所以可以不用管它,但是如果Singleton继承于一个其他的类,而这个类又有重载clone()方法,这时就需要在Singleton中再重载clone()方法,并在其中抛出CloneNotSupportedException,这样就可以避免多个Singleton的实例被创建了。
在JDK1.2以前的版本中使用Singleton模式的时候有一些需要额外注意的地方,因为Singleton类并没有被任何其他的对象所引用,所以这个类在创建后一段时间会被unload,Singleton类的静态方法就会出现问题,这是由于Java中垃圾收集机制造成的。解决的方法也很容易,只需要为其创建一个引用就行了。而在JDK1.2以后的版本中,Sun重新定义了Java规范,改正了其垃圾收集机制中的一些问题,这个问题也就不复存在了,这儿指出只是为了提起大家的主意。
Command模式用来封装请求,也描叙了一致性的发送请求的接口,允许你配置客户端以处理不同的请求,为程序增添了更大的灵活性。Singleton模式为提供对象的单一入口提供了帮助。AbstractFactory和FactoryMethod模式在功能上比较类似,都是用来处理对象的创建的,但应用在不同的层面上。在创建型模式中,还有Builder模式和Prototype模式,这儿不打算详细的讨论了,简单的说,Builder模式用来处理对象创建的细节。在两个工厂模式中都没有涉及到对象创建的具体细节,都是通过接口来返回一个给定类型的对象。而Builder模式则需要对创建一个给定类型对象的过程进行建模。这对创建复杂对象时很有用,使得创建对象的算法独立于对象各个组成部分的创建。而Prototype模式使用原型机制,通过创建简单原型的拷贝来创建对象。
当初Java刚刚推出来的时候,AWT可是一个比较热的话题,虽然现在有被Swing取代的趋势。但是我一直都觉得AWT也有其优势,至少它使用的本地代码就要比Swing快上许多,而且,可以为用户提供熟悉的本地操作系统界面。如果在Windows XP中运行基于AWT的程序的话,XP中绚烂多变的界面Theme可以轻易应用到AWT程序中,而Swing就不行了,因为AWT所调用的是本带代码,使用的是本地的窗体控件。当然,Swing也有其好处,不可一概而论。
简单来讲,AWT提供对程序员的是对窗体界面系统的抽象,而在内部实现中,针对每一种操作系统,分别有不同实现,这就是同位体(Peer)的概念。当程序员调用AWT对象时,调用被转发到对象所对应的一个Peer上,在由Peer调用本地对象方法,完成对象的显示。例如,如果你使用AWT创建了一个Menu类的实例,那么在程序运行时会创建一个菜单同位体的实例,而由创建的同位体的来实际执行菜单的现实和管理。不同的系统,有不同的同位体实现,Solaris JDK将产生一个Motif菜单的同位体,Windows下的JDK将产生一个Windows的菜单的同位体,等等。同位体的使用,使得交叉平台窗口工具的开发变得极为迅速,因为同位体的使用可以避免重新实现本地窗口控件中已经包含的方法。
图六:AWT中的组件和其对等体
实际上,从设计的角度来看,这是一个抽象和实现分离的过程--AWT是抽象,同位体是实现,抽象和实现各自成为一个对象体系,它们由一个桥连接起来,可以各自发展各自的对象层次,而不必顾虑另一方面。这就是Bridge模式所提供的思想。Bridge模式更可以提供在各个不同的实现中动态的进行切换,而不必从新编译程序。
通常,Bridge模式和AbstractFactory模式一起工作,由AbstractFactory来创建一个具体实现的对象体系。特殊的,当只有一个实现的时候,可以将Implementor抽象类去掉。这样,在抽象和实现之间建立起了一一对应的关系,但这并不损害Bridge模式的内涵。这被称为退化了的Bridge模式。
很多时候,Abstraction层次和Implementor层次之间的方法都不是一一对应的,也就是说,在Abstraction和Implementor之不是简单的的消息转发。通常,我们会将Abstraction作为一个抽象类(而不是接口)来实现。在Implementor层次中定义底层的,或者称之为原子方法,而在Abstraction层次中定义一些中高层的基于原子方法的抽象方法。这样,就能更为清晰的划分Abstraction和Implementor,类的结构也更为清晰。
图七:Bridge模式对系统的划分
下面,我们来看一个Bridge模式的具体应用。考虑这样的一个问题,需要生成一份报告,但是报告的格式并没有确定,可能是HTML文件,也可能是纯ASCII文本。报告本身也可能分为很多种,财务报表,货物报表,等等问题很简单,用继承也较容易实现,因为相互之间的组合关系并不是很多。但是,我们现在需要用Bridge的观点来看问题。
在Bridge模式中,使用一个Report类来描叙一个报告的抽象,用一个Reporter类来描叙Report的实现,它的子类有HTMLReporter和ASCIIReporter,用来分别实现HTML格式和ASCII格式的报告。在Report层次下面,有具体的一个StockListReport子类,用来表示货物清单报告。
2
3 {
4
5 Reporter reporter;
6
7 public Report(Reporter reporter) {
8
9 this.reporter = reporter;
10
11 }
12
13 //抽象类使用桥接对象的方法来实现一个任务
14
15 public void addReportItem(Object item){
16
17 reporter.addLine(item.toString());
18
19 }
20
21 public void addReportItems(List items){
22
23 Iterator iterator = items.iterator();
24
25 while ( iterator.hasNext() )
26
27 {
28
29 reporter.addLine(iterator.next().toString());
30
31 }
32
33 }
34
35 public String report(){
36
37 return reporter.getReport();
38
39 }
40
41 }
42
43 public class StockListReport extends Report{
44
45 ArrayList stock=new ArrayList();
46
47 public StockListReport(Reporter reporter){
48
49 super(reporter);
50
51 }
52
53 public void addStockItem(StockItem stockItem){
54
55 stock.add(stockItem);
56
57 addReportItem(stockItem);
58
59 }
60
61 }
62
63 //实现层次的抽象父类定义原子方法,供抽象层次的类调用
64
65 public abstract class Reporter{
66
67 String header = "";
68
69 String trailer = "";
70
71 String report = "";
72
73 public abstract void addLine(String line);
74
75 public void setHeader(String header){
76
77 this.header = header;
78
79 }
80
81 public void setTrailer(String trailer){
82
83 this.trailer = trailer;
84
85 }
86
87 public String getReport(){
88
89 return header+report+trailer;
90
91 }
92
93 }
94
95 public class HTMLReporter extends Reporter{
96
97 public HTMLReporter(){
98
99 setHeader("\n\n\n");
100
101 setTrailer("\n");
102
103 }
104
105 public void addLine(String line){
106
107 report += line + "
108
109 \n";
110
111 }
112
113 }
114
115 public class ASCIIReporter extends Reporter{
116
117 public void addLine(String line) {
118
119 report += line + "\n";
120
121 }
122
123 }
124
125
实际上,Bridge模式是一个很强大的模式,可以应用在很多方面。其基本思想:分离抽象和实现,是设计模式的基础之一。正如GOF所提到的:"找到变化的部分,并将其封装起来";"更多的考虑用对象组合机制,而不是用对象继承机制"。Bridge模式很好的体现了这几点。
在使用Java中的IO类库的时候,是不是快要被它那些功能相似,却又绝对可称得上庞杂的类搞得要发疯了?或许你很不明白为什么要做这么多功能相似的几十个类出来,这就是Decorator模式将要告诉你的了。
在IO处理中,Java将数据抽象为流(Stream)。在IO库中,最基本的是InputStream和OutputStream两个分别处理输出和输入的对象(为了叙述简便起见,这儿只涉及字节流,字符流和其完全相似),但是在InputStream和OutputStream中之提供了最简单的流处理方法,只能读入/写出字符,没有缓冲处理,无法处理文件,等等。它们只是提供了最纯粹的抽象,最简单的功能。
如何来添加功能,以处理更为复杂的事情呢?你可能会想到用继承。不错,继承确实可以解决问题,但是继承也带来更大的问题,它对每一个功能,都需要一个子类来实现。比如,我先实现了三个子类,分别用来处理文件,缓冲,和读入/写出数据,但是,如果我需要一个既能处理文件,又具有缓冲功能的类呢?这时候又必须在进行一次继承,重写代码。实际上,仅仅这三种功能的组合,就已经是一个很大的数字,如果再加上其它的功能,组合起来的IO类库,如果只用继承来实现的话,恐怕你真的是要被它折磨疯了。
图八:JDK中IO流的类层次
Decorator模式可以解决这个问题。Decorator字面的意思是装饰的意思,在原有的基础上,每添加一个装饰,就可以增加一种功能。这就是Decorator的本意。比如,对于上面的那个问题,只需要三个Decorator类,分别代表文件处理,缓冲和数据读写三个功能,在此基础上所衍生的功能,都可以通过添加装饰来完成,而不必需要繁杂的子类继承了。更为重要的是,比较继机制承而言,Decorator是动态的,可以在运行时添加或者去除附加的功能,因而也就具有比继承机制更大的灵活性。
上面就是Decorator的基本思想,下面的是Decorator模式的静态结构图:
图九:Decorator模式的类图
可以看到,一个Decorator与装饰的Subject对象有相同的接口,并且除了接口中给出的方法外,每个Decorator均有自己添加的方法,来添加对象功能。每个Decorator均有一个指向Subject对象的引用,附加的功能被添加在这个Subject对象上。而Decorator对象本身也是一个Subject对象,因而它也能够被其他的Decorator所修饰,提供组合的功能。
在Java IO操作中,经常可以看到诸如如下的语句:
2
3 FilterInputStream myStream=new LineNumberInputStream
4
5 ( new BufferInputStream( new StringBufferInputStream( myStringBuffer)));
6
7 myStream.read();
8
9 myStream.line();
10
11
多个的Decorator被层叠在一起,最后得到一个功能强大的流。既能够被缓冲,又能够得到行数,这就是Decorator的威力!
不仅仅如此,Java中的IO还允许你引入自定义的Decorator,来实现自己想要的功能。在良好的设计背景下,这做起并不复杂,只需要4步:
创建两个分别继承了FilterInputStream和 FilterOutputStream的子类
重载read()和write()方法来实现自己想要的功能。
可以定义或者重载其它方法来提供附加功能。
确定这两个类会被一起使用,因为它们在功能上是对称的。
就这样,你就可以无限的扩展IO的功能了。
在了解了IO中的Decorator后,我们再来看一个Decorator模式应用的具体的例子。这个例子原本是出现在GOF书中的,这儿稍作改动,引来示例。
在一个图形用户界面(GUI)中,一个组件有时候需要用到边框或者滚动条,而有时候又不需要,有时候可能两者都要用到。当需要动态的去处或者添加职能的时候,就可以考虑使用Decorator模式了。这儿对于一个VisualComponent组件对象,我们引入了两个Decorator类:BoderDecorator和ScrollDecorator,分别用来为组件添加边框和处理滚动。程序类图如下:
图十:Decorator模式的应用例子
程序写得很简单,没有包括具体的代码,只是有一个可以运行的框架以供参考。代码如下:
2
3 class Client{
4
5 public static void main (String[] args ){
6
7 Window window = new Window ();
8
9 TextView textView = new TextView ();
10
11 window.setContents (
12
13 new BorderDecorator (
14
15 new ScrollDecorator (textView, 500), 1));
16
17 }
18
19 }
20
21 //Windows类用来容纳组件对象
22
23 class Window{
24
25 VisualComponent contents;
26
27 public Window () {}
28
29 public void setContents (VisualComponent vc){
30
31 contents = vc;
32
33 }
34
35 }
36
37 //VisualComponent类定义了组件的接口
38
39 class VisualComponent{
40
41 public VisualComponent (){}
42
43 public void draw (){}
44
45 public void resize (){}
46
47 }
48
49 //TextView类是一个显示文本的具体的组件
50
51 class TextView extends VisualComponent{
52
53 public TextView (){}
54
55 public void draw (){
56
57 …
58
59 }
60
61 public void resize (){
62
63 …
64
65 }
66
67 }
68
69 //Decorator类继承于VisualComponent,定义所有Decorator的缺省方法实现
70
71 class Decorator extends VisualComponent{
72
73 private VisualComponent component;
74
75 public Decorator (VisualComponent vc) {
76
77 this.component=vc;
78
79 }
80
81 public void draw () {
82
83 component.draw ();
84
85 }
86
87 public void resize () {
88
89 component.resize ();
90
91 }
92
93 }
94
95 //BorderDecorator类为组件提供边框
96
97 class BorderDecorator extends Decorator{
98
99 private int width;
100
101 public BorderDecorator (VisualComponent vc, int borderWidth){
102
103 super (vc);
104
105 width = borderWidth;
106
107 }
108
109 public void draw (){
110
111 super.draw ();
112
113 drawBorder (width);
114
115 }
116
117 private void drawBorder (int width){
118
119 …
120
121 }
122
123 }
124
125 //ScrollDecorator类为组件提供滚动条
126
127 class ScrollDecorator extends Decorator{
128
129 private int scrollSize;
130
131 public ScrollDecorator (VisualComponent vc, int scrSize){
132
133 super (vc);
134
135 scrollSize = scrSize;
136
137 }
138
139 public void draw (){
140
141 scroll();
142
143 super.draw ();
144
145 }
146
147 private void scroll (){
148
149 …
150
151 }
152
153 }
154
Decorator确实能够很好的缓解当功能组合过多时子类继承所能够带来的问题。但是在得到很大的灵活性的同时,Decorator在使用时也表现得较为复杂。看看仅仅为了得到一个IO流,除了要创建核心的流外,还要为其加上各种各样的装饰类,这使得代码变得复杂而难懂。有几个人一开始时没有被Java的IO库吓一跳呢?
Bridge模式用来分离抽象和实现,使得这两个部分能够分别的演化而不必修改另外一部分的内容。通常的,可以在实现部分定义一些基本的原子方法,而在抽象部分则通过组合定义在实现层次中的原子方法来实现系统的功能。Decorator模式通过聚合机制来为对象动态的添加职责,解决了在子类继承中容易引起的子类爆炸的问题。
毫无疑问的,AWT中的Component-Container体系就是一个很好的Composite模式的例子。Container继承于Component,而Container中有可以包含有多个Component,因为Container实际上也是Component,因而Container也可以包含Container。这样通过Component-Container结构的对象组合,形成一个树状的层次结构。这也就是Composite模式所要做的。
Composite模式是为了简化编程而提出的,一般的在编程的时候,如果严格的区分Component和Container的话,有时候会带来许多不便,而且这些往往是没有必要的。比如,我要在一个Container中放置一个Component,我并不需要知道这个Component到底是一个Container,或者就是一个一般的Component,在父级容器中所要做的,只是记录一个Component的引用,在需要的时候调用Component的绘制方法来显示这个Component。当这个Component确实是一个Container的时候,它可以通过Container重载后的绘制方法,完成对这个容器的显示,并把绘制消息传递给到它的子对象去。也就是说,对一个父级容器而言,它并不不关心,其子对象到底是一个Component,还是一个Container。它需要将Component和Container统一对待。
图十一:Composite模式的类图
Composite模式比较简单,实现起来也不复杂,但是有一定的局限性。比如,在处理树的时候,我们往往需要处理三类对象:子树,页节点和非页节点。而在Composite模式中对于子树和非叶节点的区分并不明显,而是把他们合成为一个Composite对象了。而且在GOF给出的Composite的模式中,对于添加,删除子节点等属于Composite对象的的方法,是放在了Component对象中的,这虽然在实现的时候可以区分开来,但容易造成一些概念上的误解。
由上所叙,我们可以提出一个改进了的Composite模式,引入子树对象,从而将子树和非叶节点分开,如下图所示:
图十二:Composite模式的一种变体
虽然将Composite从Component类层次中分离出来,但并没有损害Composite模式的内涵。这样做不一定就会比上面的那个要好,各有不同的应用,不过有时候用这样的方法来处理子树要容易些,概念上也更为清晰。
下面的代码,给出了一个Composite模式简单的Java实现:
2
3 public abstract void operation();
4
5 public void add(Component component){};
6
7 public void remove(Component component){};
8
9 }
10
11 import java.util.*;
12
13 public class Composite extends Component{
14
15 String name;
16
17 ArrayList children = new ArrayList();
18
19 public Composite(String name){
20
21 this.name = name;
22
23 }
24
25 public void add(Component component){
26
27 children.add(component);
28
29 }
30
31 public void remove(Component component){
32
33 children.remove(component);
34
35 }
36
37 public void operation(){
38
39 System.out.println(name);
40
41 Iterator iterator = children.iterator();
42
43 while(iterator.hasNext()){
44
45 Component child = (Component)iterator.next();
46
47 child.operation();
48
49 }
50
51 }
52
53 }
54
55 public class Leaf extends Component{
56
57 String name;
58
59 public Leaf(String name){
60
61 this.name = name;
62
63 }
64
65 public void operation(){
66
67 System.out.println(name);
68
69 }
70
71 }
72
Strategy模式主要用来将算法实现从类中分离出来,并封装在一个单独的类中。更简单的说,对象与其行为(behaviour)这本来紧密联系的两部分被解耦,分别放在了两个不同的类中。这使得对同一个行为,可以方便的在任何时候切换不同的实现算法。而通过对策略的封装,为其提供统一的接口,也可以很容易的引入新的策略。
AWT的LayoutManager,是Strategy模式的一个例子。对于GUI而言,每个组件(Component)在容器中(Container)的排放是需要遵循一定的算法的。通常的方法是使用绝对坐标,就像VB,Delphi之类的工具所作的那样,记录每个组件在容器中的位置。这当然会带来一些问题,比如在窗体缩放的时候,就需要手工编码改变组件的大小和位置,以使得原来的比例得以保存。而在AWT中,引入了布局管理器(LayoutManager)的概念,使得布局的方法大大丰富,编码过程也变得简单。
一个容器,比如Applet,Panel等,仅仅记录其包含的组件,而布局管理器中封装了对容器中组件进行布局的算法,具体地说,就是指明容器中组件的位置和尺寸的大小。通过布局管理器,你只需要确定想放置的组件间的相对位置即可,这一方面简化编码,另一方面也有助于实现软件的平台无关性。
图十三:AWT中的容器和布局管理器的关系
每一个容器均有一个布局管理器,当容器需要布置它的组件时,它调用布局管理器的方法布置容器内的组件。LayoutManager2继承于LayoutManager,提供更为细致的布局功能,它可以让布局管理器为组件加上约束条件已确定组件如何被布置。例如,为了确定组件被摆放在边框内的位置,BorderLayout在它的组件上加上方向指示。
特别的,通过实现LayoutManager或者LayoutManager2接口,可以很容易实现自定义的布局策略。
回到模式的话题上来,如果有几个很相似的类,其区别仅仅是在个别行为上的动作不同,这时候就可以考虑使用Strategy模式。这样,通过策略组合,将原来的多个类精简为一个带有多个策略的类。这很符合OO设计的原则:找到变化的部分,并将其封装起来!Strategy模式同样的为子类继承提供了一个好的替代方案,当使用继承机制的时候,行为的改变是静态的,你指能够改变一次--而策略是动态的,可以在任何时候,切换任何次数。更为重要的是,策略对象可以在不同的环境中被不同的对象所共享。以布局管理器为例,虽然每一个容器只有一个布局管理器,但是一个布局管理器可以为多个容器工作。
图十四:Strategy模式的类图
Strategy模式也有一些缺点,比如,应用程序必须知道所有的策略对象,并从中选者其一。而且在策略对象被使用的时候,它和Context对象之间通常是紧耦合的,Context对象必须为策略对象提供与具体算法相关的数据或者其它的东西,而这些数据的传递可能并不能够风装载抽象地策略类中,因为并不是所有的算法都会需要这些数据的。另外,因为策略对象通常由应用程序所创建,Context对象并不能够控制Strategy的生命期,而在概念上,这个策略应该从属于Context对象,其生命期不应该超出Context的范围对象。
通常的,Strategy很容易和Bridge模式相混淆。确实,他们有着很相近的结构,但是,他们却是为解决不同的问题而设计的。Strategy模式注重于算法的封装,而Bridge模式注重于分离抽象和实现,为一个抽象体系提供不同的实现。
Iterator模式用来规格化对某一数据结构的遍历接口。
JDK中在Collection Framework中引入了Iterator接口,提供对一个Collection的遍历。每一个Collection类中都定义有从Collection接口中继承而来的iterator()方法,来得到一个Iterator对象,我们称之为遍历器,Iterator接口很简单:
hasNext():用来判断在遍历器中是否还有下一个元素。
next():返回遍历器中的下一个元素。
remove():在被遍历的Collection类中删除最后被返回的那个对象。
我们就以最为常用的Vector为例,看看在Collection Framework中,Iterator模式是如何被实现的。在此之前,我们需要先了解一些Vector和Collection Framework的结构。
Collection接口作为这个Framework的基础,被所有其它的集合类所继承或者实现。对Collection接口,有一个基本的实现是抽象类AbstractCollection,它实现了大部分与具体数据结构无关的操作。比如判断一个对象是否存在于这个集合类中的contains()方法:
2
3 Iterator e = iterator();
4
5 if (o==null) {
6
7 while (e.hasNext())
8
9 if (e.next()==null)
10
11 return true;
12
13 } else {
14
15 while (e.hasNext())
16
17 if (o.equals(e.next()))
18
19 return true;
20
21 }
22
23 return false;
24
25 }
26
27
而这其中调用的iterator()方法是一个抽象方法,有赖于具体的数据结构的实现。但是对于这个containers()方法而言,并不需要知道具体的Iterator实现,而只需要知道它所提供的接口,能够完成某类任务既可,这就是抽象类中抽象方法的作用。其它的在AbstractCollection中实现的非抽象方法,大部分都是依赖于抽象方法iterator()方法所提供的Iterator接口来实现的。这种设计方法是引入抽象类的一个关键所在,值得仔细领悟。
List接口继承Collection接口,提供对列表集合类的抽象;对应的AbstractList类继承AbstractCollection,并实现了List接口,作为List的一个抽象基类。它对其中非抽象方法的实现,也大抵上与AbstractCollection相同,这儿不再赘叙。
而对应于Collection的Iterator,List有其自己的ListIterator,ListIterator继承于Iterator,并添加了一些专用于List遍历的方法:
boolean hasPrevious():判断在列表中当前元素之前是否存在有元素。
Object previous():返回列表中当前元素之前的元素。
int nextIndex():
int previousIndex():
void set(Object o):
void add(Object o):
ListIterator针对List,提供了更为强劲的功能接口。在AbstractList中,实现了具体的iterator()方法和listIterator()方法,我们来看看这两个方法是如何实现的:
2
3 return new Itr(); //Itr是一个内部类
4
5 }
6
7 private class Itr implements Iterator {
8
9 int cursor = 0;//Iterator的计数器,指示当前调用next()方法时会被返回的元素的位置
10
11 int lastRet = -1;//指示刚刚通过next()或者previous()方法被返回的元素的位置,-1
12
13 //表示刚刚调用的是remove()方法删除了一个元素。
14
15 //modCount是定义在AbstractList中的字段,指示列表被修改的次数。Iterator用//这个值来检查其包装的列表是否被其他方法所非法修改。
16
17 int expectedModCount = modCount;
18
19 public boolean hasNext() {
20
21 return cursor != size();
22
23 }
24
25 public Object next() {
26
27 try {
28
29 //get方法仍然是一个抽象方法,依赖于具体的子类实现
30
31 Object next = get(cursor);
32
33 //检查列表是否被不正确的修改
34
35 checkForComodification();
36
37 lastRet = cursor++;
38
39 return next;
40
41 } catch(IndexOutOfBoundsException e) {
42
43 checkForComodification();
44
45 throw new NoSuchElementException();
46
47 }
48
49 }
50
51 public void remove() {
52
53 if (lastRet == -1)
54
55 throw new IllegalStateException();
56
57 checkForComodification();
58
59 try {
60
61 //同样remove(int)也依赖于具体的子类实现
62
63 AbstractList.this.remove(lastRet);
64
65 if (lastRet < cursor)
66
67 cursor--;
68
69 lastRet = -1;
70
71 expectedModCount = modCount;
72
73 } catch(IndexOutOfBoundsException e) {
74
75 throw new ConcurrentModificationException();
76
77 }
78
79 }
80
81 final void checkForComodification() {
82
83 if (modCount != expectedModCount)
84
85 throw new ConcurrentModificationException();
86
87 }
88
89 }
90
91
这儿的设计技巧和上面一样,都是使用抽象方法来实现一个具体的操作。抽象方法作为最后被实现的内容,依赖于具体的子类。抽象类看起来很像是一个介于接口和子类之间的一个东西。
从设计上来讲,有人建议所有的类都应该定义成接口的形式,这当然有其道理,但多少有些极端。当你需要最大的灵活性的时候,应该使用接口,而抽象类却能够提供一些缺省的操作,最大限度的统一子类。抽象类在许多应用框架(Application Framework)中有着很重要的作用。例如,在一个框架中,可以用抽象类来实现一些缺省的服务比如消息处理等等。这些抽象类能够让你很容易并且自然的把自己的应用嵌入到框架中去。而对于依赖于每个应用具体实现的方法,可以通过定义抽象方法来引入到框架中。
其实在老版本的JDK中也有类似的概念,被称为Enumeration。Iterator其实与Enmeration功能上很相似,只是多了删除的功能。用Iterator不过是在名字上变得更为贴切一些。模式的另外一个很重要的功用,就是能够形成一种交流的语言(或者说文化)。有时候,你说Enumeration大家都不明白,说Iterator就都明白了。
Composite,Strategy和Iterator。Composite是一个结构性的模式,用来协调整体和局部的关系,使之能够被统一的安排在一个树形的结构中,并简化了编程。Strategy模式与Bridge模式在结构上很相似,但是与Bridge不同在于,它是一个行为模式,更侧重于结构的语义以及算法的实现。它使得程序能够在不同的算法之间自由方便的作出选择,并能够在运行时切换到其他的算法,很大程度上增加了程序的灵活性。Iterator模式提供统一的接口操作来实现对一个数据结构的遍历,使得当数据结构的内部算法发生改变时,客户代码不需要任何的变化,只需要改变相应的Iterator实现,就可以无缝的集成在原来的程序中。
有了前面诸多设计模式的基础,这儿可以提出一个比较特殊的模式MVC。MVC并不属于GOF的23个设计模式之列,但是它在GOF的书中作为一个重要的例子被提出来,并给予了很高的评价。一般的来讲,我们认为GOF的23个模式是一些中级的模式,在它下面还可以抽象出一些更为一般的低层的模式,在其上也可以通过组合来得到一些高级的模式。MVC就可以看作是一些模式进行组合之后的结果(实际上,MVC的出现要早于设计模式的提出,这而只是对它在设计模式的基础上进行在分析)。如果没有前面的基础,理解MVC或许会有一些困难。
MVC模式
MVC模式比较的特别,它含义比较的广,涉及的层面也不仅仅是设计这一块,不好简单的把它归为设计模式。当然,它主要还是作为一个设计的概念被提到的,而且在Java体系中,MVC有着至关重要的作用。这儿提的是Java中的设计模式,当然不好拉了它不讲了。
关于MVC的来龙去脉,这儿就不再讲了。这里主s要讲两个方面的:作为设计模式的MVC和作为体系结构模式的MVC。
所谓MVC,指的是一种划分系统功能的方法,它将一个系统划分为三个部分:
模型(Model):封装的是数据源和所有基于对这些数据的操作。在一个组件中,Model往往表示组件的状态和操作状态的方法。
视图(View):封装的是对数据源Model的一种显示。一个模型可以由多个视图,而一个视图理论上也可以同不同的模型关联起来。
控制器(Control):封装的是外界作用于模型的操作。通常,这些操作会转发到模型上,并调用模型中相应的一个或者多个方法。一般Controller在Model和View之间起到了沟通的作用,处理用户在View上的输入,并转发给Model。这样Model和View两者之间可以做到松散耦合,甚至可以彼此不知道对方,而由Controller连接起这两个部分。
有了前面介绍的诸多模式之后,就可以很容易的通过模式来解释MVC的内在行为了。前面说过,在设计模式中,MVC实际上是一个比较高层的模式,它由多个更基本的设计模式组合而成,Model-View的关系实际上是Observer模式,模型的状态和试图的显示相互响应,而View-Controller则是由Strategy模式所描叙的,View用一个特定的Controller的实例来实现一个特定的响应策略,更换不同的Controller,可以改变View对用户输入的响应。而其它的一些设计模式也很容易组合到这个体系中。比如,通过Composite模式,可以将多个View嵌套组合起来;通过FactoryMethod模式来指定View的Controller,等等。
使用MVC的好处,一方面,分离数据和其表示,使得添加或者删除一个用户视图变得很容易,甚至可以在程序执行时动态的进行。Model和View能够单独的开发,增加了程序了可维护性,可扩展性,并使测试变得更为容易。另一方面,将控制逻辑和表现界面分离,允许程序能够在运行时根据工作流,用户习惯或者模型状态来动态选择不同的用户界面。
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组件对象。
上面讲到的是作为设计模式的MVC。而在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是一个大的框架,这时我们往往把它不再看作为设计模式,而是作为体系结构模式的一个应用了。
总结
在这篇文章中,从设计的角度,对Java的类库进行了一些分析,并着重于设计模式在其中的使用问题。相信大家看了之后,不论对Java类库本身,还是设计模式,都应该有了一个更深层次的了解。当然,Java类库是一个非常庞大的东西,还有着许多设计精良的结构。因而,对Java源代码的研究,不论对于编码还是设计,都是很有裨益的。本人接触设计模式的时间并不很长,对其的理解可能会有一些偏差,如有谬误的地方,还请能够提出,大家能够共同的探讨。
需要说明的是,对模式的描叙实际上是有一套完整的规格(或者语言)来进行的,涉及到模式的意图(Intent),问题描叙(Problem),背景(Context),约束(Force),解决方案(Solution),结果(Resulting Context)等等。但这儿为了叙述的方便,并没有将它们一一列举。如果需要对模式有详细系统的研究,就应该对这些规格叙述有更深入的了解。