Struts2技术内幕:深入解析Struts架构设计与实现原理
上QQ阅读APP看书,第一时间看更新

第2章 固本清源—Web开发浅谈

现今,在谈到Web开发有关的话题时,程序员们总是热衷于讨论一些我们耳熟能详的Web开发框架,如Struts2、Spring、Hibernate等。有些程序员将这些框架奉为宝典,并且趋之若鹜地挖掘框架的方方面面、比较各种开发框架的优劣。对这些框架的熟悉与否,似乎已成为衡量一个程序员是否精通Java、精通J2EE开发的事实标准。甚至在广大程序员求职的过程中,这些主流的开发框架的知识细节也常常成为面试中必考的元素,答不上这些问题,无疑会为求职蒙上一层阴影。

面对这些框架,大家是否真的思考过,我们为什么要学习这些框架?这些框架到底从何而来?框架的本质到底是什么?使用框架,又能够为我们的开发带来什么样的好处呢?在深入分析Struts2及其源码之前,我们首先必须弄清楚这些比框架更为核心的问题。因为只有了解了为什么,我们才能知道怎么做,知道如何才能做得更好。

2.1 面向对象浅谈

在谈框架之前,我们不得不首先面对一个比框架更为重要的概念,那就是面向对象的概念。面向对象的概念是一个看起来、听起来简单,实际却蕴含着丰富内容的概念。众多的国内外学者为了讲清楚这个概念,采用了各种的不同的比喻、也给出了多种多样的代码示例,还为面向对象的概念建立起一套完整的理论体系。

不过迄今为止,能够完整地将面向对象的来龙去脉讲清楚、讲透彻的毕竟还是少数。随着编程语言从早期的“面向过程式”的C语言发展到后来的C++、Java,直至近几年来非常热门的Ruby,面向对象的基本概念已经逐渐成为编程语言的核心设计法则。因而“面向对象”的概念也逐渐成为每个程序员都认同,并且在日常编程过程中遵循的最高纲领。

限于篇幅,我们实在无法涉及面向对象概念的方方面面。不过我们可以将话题聚焦在构成面向对象概念最基本的元素之上,这个基本元素就是:对象。在接下来的章节中,我们将分析对象的构成模型以及对象的关系模型,并以此为基础阐述在面向对象编程过程中的一些基本观点。

2.1.1 对象构成模型

2.1.1.1 对象的构成分析

作为面向对象编程最基本的构成元素,对象是由一个叫做类(Class)的概念来描述的。因此,针对对象构成分析的研究,也就转化为针对编程语言中类的构成分析。以Java语言为例,我们可以对Java语言中类的定义进行一些构成上的分析,如图2-1所示。

图2-1 对象的构成分析

在图中,我们可以看到构成一个对象的基本要素主要有:

签名(Signature)—对象的核心语义概括

属性(Property)—对象的内部特征和状态的描述

方法(Method)—对象的行为特征的描述

在进行面向对象的编程时,首先要做的就是对世间万物进行编程元素的抽象。这个过程说白了,就是通过使用编程语言所规定的语法,例如类(Class)或者接口(Interface)来表达事物的逻辑语义。在图2-1中我们所谈到的构成一个对象定义的基本要素,实际上不仅反映出我们对世间万物的抽象过程,也是人类使用高级编程语言来实现外部世界表述的基本方式。

从图2-1中我们可以看到,签名用以描述事物的核心语义,它的作用实际上是界定我们所描述的事物的范畴。而在对象的内部,作为对象内部构成的重要元素,属性方法刚好从两个不同的角度对事物的内在特性给予了诠释。其中,属性所勾勒的是一个对象的构成特性和内部状态的特性;方法则表达了一个对象的动态行为特性。这就像我们人一样,人由头、躯干、四肢构成,它们可以看作是人这个对象的“属性”。与此同时,人具有“直立行走”的行为特性,我们可以定义一个“方法”来模拟这一行为。

以上这些分析,我们还停留在语法这个层面,因为无论是属性还是方法,它们都是Java语言的原生语法支持。将事物抽象成为对象,并且赋予这个对象属性和方法,是一个很自然的编程逻辑,这也符合面向对象编程语言的基本思路。不过我们也同时发现在实际编程过程中,对象将表现为三种不同的形态和运作模式。

属性--行为模式

这种模式是指一个对象同时拥有属性定义和方法定义。这是对象最为普遍的一种运行模式,绝大多数对象都运作在这种模式之上。

属性模式

这种模式是指一个对象只拥有属性定义,辅之以相应的setter和getter方法。Java规范为运行在这种模式下的对象取了一个统一的名称:JavaBean。JavaBean从其表现出来的特性看,可以作为数据的存储模式和数据的传输载体。

行为模式

这种模式是指构成一个对象的主体是一系列方法的定义,而并不含有具体的属性定义,或者说即使含有一些属性定义,也是一些无状态的协作对象。运行在这种模式之下的对象,我们往往称之为“无状态对象”,其中最为常见的例子是我们熟悉的Servlet对象。

我们发现,对象的运行模式的划分是根据对象的构成特点进行的。这三种对象的运行模式在我们日常编程中都已经见过并且亲自实践过。接下来的章节,我们将针对后两种构成模式做进一步的分析。

2.1.1.2 属性对象模式

属性对象模式又称为JavaBean模式。这种对象的运行模式我们在日常编程中见得非常多。作为数据存储和数据传输的载体,运行在JavaBean模式下的对象,在众多的编程层次都会被用到,并且根据作用不同被冠以各种不同的名称,如

PO(Persistent Object)—持久化对象

BO(Business Object)—业务对象

VO(Value Object)—值对象

DTO(Data Transfer Object)—数据传输对象

FormBean—页面对象

对于这些纷繁复杂的缩写和对象类别,许多初学者会感到非常头疼。它们从形式上看是一系列难记的缩写,不过真正让程序员头疼的,不仅在于它们被用于不同的业务场景和编程层次,还在于它们在某些时候甚至只是同一个对象在不同层次上的不同名称。

不过我们大可不必在对象的名称和叫法上过分纠结。因为对于程序员来说,无论这些对象被冠以什么花里胡哨的名称,它们只不过是对基本的、运行在JavaBean模式下对象的有效扩展或增强。

以PO(Persistent Object)为例,当我们使用Hibernate作为O/R Mapping的工具时,一个典型的PO会被定义成如代码清单2-1所示的样子。

代码清单2-1 User.java

          @Entity
          @Proxy(lazy = true)
          @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
          public class User {
              @Id
              @GeneratedValue
              private Integer id;
              @Column
              private String name;
              @Column
              private String password;
              @Column
              private String email;
              // 这里省略了所有的setter和getter方法
          }

假设去除那些Annotation,我们会发现这个PO和一个普通的JavaBean并无不同,至少我们无法在形式上将它们区分开。因此,我们说Annotation在这里的所用是丰富了一个普通JavaBean的语义,从而使之成为一个持久化对象。而当我们使用O/R Mapping的工具Hibernate进行处理时,也是根据这些Annotation才能够对这些PO进行识别并赋予其相应功能的。也就是说,JavaBean自身的特性并没有发生改变,只是引入了一些额外的编程元素从而对JavaBean进行了增强。

当一个对象运作在属性对象模式时,其本质是对象的JavaBean特性。我们可以从其表现形式和运行特征中得出下面这样的一个结论。

结论 JavaBean对象的产生主要是为了强调对象的内在特性和状态,同时构造一个数据存储和数据传输的载体。

我们在本节开篇所提到的各种不同的对象名称的定义,它们之间的界定实际上是非常模糊的。同样一个对象,往往可以兼任多种不同的角色,在不同的编程层次表现为不同的对象实体,其最终目的是在特定的场合表现出其作用。

在上面的结论中,我们还读到另外一层意思,那就是JavaBean对象的一个重要的性质在于它是一个数据存储和数据传输的载体。有关这一点,我们将在之后的章节进行分析。

2.1.1.3 行为对象模式

根据之前有关对象构成的分析,运行在行为对象模式之下的对象,我们往往称之为无状态对象。在上一节中,我们提到对象的内在特性和状态是由构成对象的属性来表达的。所以,所谓的无状态对象实际上就是指对象的方法所表达的行为特性并不依赖于对象的内部属性的状态。而这种无状态的特性,非常适合进行一个请求的响应,并在方法体的内部实现中进行复杂的业务逻辑处理。

我们在这里可以针对对象的行为方法的语法做进一步的分析,如图2-2所示。

图2-2 对象的行为方法的语法分析

从图2-2中,我们看到对象方法的语法定义同样体现出一定的规律性:

方法签名(Signature)—行为动作的逻辑语义概括

参数(Parameter)—行为动作的逻辑请求输入

返回值(Return)—行为动作的处理响应结果输出

我们可以看到,方法的定义实际上是一种触发式的逻辑定义。当需要完成某种行为动作(业务逻辑)时,我们会将请求作为参数输入到方法的参数列表中,而返回值则自然而然地成为业务逻辑的处理结果。由此,我们可以得出以下结论:

结论 对象中的方法定义是进行请求响应的天然载体。

这个结论我们将在之后对各种Web框架对Http请求的响应设计中再次提及。因为在Java标准中,对Http请求的响应是通过Servlet标准来实现的。而我们知道,Servlet对象就是一个非常典型的运行在行为对象模式之上的无状态对象。

上述结论对读者理解“请求-响应”的过程在编程语言中的逻辑表达有很大的帮助。因而读者应仔细体会这些构成要素在“请求-响应”过程中所起的作用,从而对Web框架的设计有更深的感悟。

2.1.2 对象关系模型

对象的构成模型是从对象内部结构的角度对面向对象编程中的基本元素进行的分析。在本节中,我们分析的角度将由“内”转向“外”,考虑对象与对象之间的关系。

谈到对象之间的关系,我们很容易想到两个不同的层次:

从属关系—一个对象在逻辑语义上隶属于另外一个对象

协作关系—对象之间通过协作来共同表达一个逻辑语义

这两种关系在面向对象编程语言中分别拥有不同的表现形式和逻辑意义,这二者构成了绝大多数的对象关系模型。接下来,我们就来分别分析这两种对象关系模型。

2.1.2.1 对象的从属关系

对象的从属关系,主要指一个对象在逻辑语义上隶属于另外一个对象。这个定义实际上依然非常抽象。要理解这一定义,我们就必须从“隶属”这个核心词汇入手。逻辑语义上的“隶属”,主要有两种不同的含义。

归属

归属的逻辑含义很直观。比如说,一个人总是归属于一个国家;一本书总是有作者。因而,当我们把人和国家、书和作者都映射成面向对象编程语言中所定义的一个个对象时,它们之间自然而然就形成了归属关系。这种归属关系是由外部世界的逻辑关系映射到编程元素之上而带来的。

继承

继承的逻辑含义就有点晦涩。比如说,马、白马和千里马之间的关系。首先,白马和千里马都是马的一种,然而白马和千里马却各自拥有自己独有的特性:白马是白色的、千里马一日可行千里。此时,我们可以说白马和千里马都属于马,它们都继承了马的基本特征,却又各自扩展了自身独有的特质。

明确了“隶属”的两层含义,我们就需要把它们和面向对象编程语言联系在一起。归属和继承,它们在面向对象的编程语言中又以怎样的形式表现出来呢?

我们先来看看“归属”这层含义。对于“归属”关系的编程语言表现形式,我们可以得出下面的结论:

结论 “归属”关系在面向对象编程语言中,主要以对象之间互相引用的形式存在。

我们以书和作者之间的对象定义作为例子来说明,其相关源码如代码清单2-2所示。

代码清单2-2 Book.java

          public class Book {
            private String name;
            private List<Author> authors;
          }

我们在这里所表达的是书和作者之间的“归属”关系。从代码中,可以很明显看到,一本书可能有多个作者,所以我们在书(Book)的属性定义中加入了一个List的容器结构,List中的对象类型是作者(Author)。这样一来,书和作者之间就形成了一个引用关系。

使用对象之间的引用来表达“归属”关系,是一种非常廉价的做法。因为这种关系的表达来源于对象定义的基本模式,不会对对象自身产生破坏性影响。

细心的读者还会发现,我们这里所说的“归属”关系,实际上还蕴含了一层“数量”的对应关系。在上面的例子中,我们发现书和作者的数量关系是“一对多”。除了“一对多”以外,对象之间的归属关系在数量这个维度上还可以是“一对一”和“多对多”。有趣的是,这三种归属关系正好也和我们关系型数据库模型中所定义的基本关系一一对应。这种关系模型也就为我们在Java世界和数据库世界之间进行O/R Mapping打下了理论基础。

看完了“归属”关系,我们再来看看“继承”关系。有关“继承”关系的编程形式表述,我们可以用下述结论来进行说明:

结论 “继承”关系在面向对象编程语言中,主要以原生语法的形式获得支持。

什么是“以原生语法的形式获得支持”呢?我们来看看之前说的那个白马的例子,其相关源码如代码清单2-3所示。

代码清单2-3 Horse.java

          public class Horse {
            public void run() {
            }
          }
          public class WhiteHorse extends Horse {
          }
          public class ThousandMileHouse extends Horse {
          }

白马和马之间,我们使用了Java中的关键字extends来表达前者继承自后者。这种方式与我们之前所看到的对象之间的引用模式完全不同,它使用了编程语言中的原生语法支持。

这种对象关系的表达方式非常简单而有效,不过当我们引入一个语法,就不得不遵循这个语法所规定的编程规范所带来的编程限制。例如在Java中,我们就必须遵循单根继承的规范。这一点也是“继承”这种方式经常被诟病的原因之一。

在Java中,除了extends关键字以外,还有implements关键字来表达一个实现类与接口类之间的关系。实际上这是一种特殊的“继承”关系,它无论从语义上还是表现形式上,与extends都基本相同。

2.1.2.2 对象的协作关系

对象的从属关系从现实世界逻辑语义的角度描述了对象与对象之间的关系。从之前的分析中,我们可以发现无论是“归属”关系还是“继承”关系,它们都在围绕着对象构成要素中的属性做文章。那么读者不禁要问,围绕着对象的行为动作特征,对象之间是否能够建立起关系模型呢?

从哲学的观点来看,万事万物都存在着普遍而必然的联系。从对象的行为特性上分析,一个对象的行为特征总是能够与另外一个对象的行为特征形成依赖关系。而这种依赖关系,在极大程度上影响着对象的逻辑行为模式。例如,一个人“行走”这样一个动作,需要手脚的共同配合才能完成,具体来说就是“摆手”和“抬脚”。而当我们把手和脚分别看作一个对象时,“摆”和“抬”就成为手和脚的行为动作了。

这样一说,似乎对象之间的协作关系就非常容易理解了,请看以下结论:

结论 当对象的行为动作需要其他对象的行为动作进行配合时,对象之间就形成了协作关系。

可以想象,一个对象在绝大多数情况下都不是孤立存在的,它总是需要通过与其他对象的协作来完成其自身的业务逻辑。这是软件大师Martin Fowler曾经提过的一个重要观点。然而这却为我们的编程带来了一些潜在的问题:如何来管理对象和协作对象之间的关系呢?有关这一问题,我们将在第5章详细进行讲解。

对象的协作关系在对象运行在行为对象模式时显得尤为突出。因为当使用一个具体的方法来进行动作响应时,我们总是会借助一些辅助对象的操作来帮助我们共同完成动作的具体逻辑。也就是说,我们会将一个动作从业务上进行逻辑划分,将不同的业务分派到不同的对象之上去执行。这就成为我们所熟知的分层开发模式的理论依据。

2.1.3 面向对象编程的基本观点

在了解了对象的构成模型和对象的关系模型之后,读者不免要问,这些内容和我们的日常编程有关系吗?答案是有关系!而且不仅是有关系,还是有相当大的关系!在本节中,我们就以结论辅之以分析的方法,为读者展示面向对象编程中的一些基本观点。

结论 每一种对象的构成模型,都有其特定的应用范围。

根据之前我们有关对象的构成模型的分析,可以发现三种对象的构成模型在日常的编程过程中都曾经碰到过。因此,我们应该首先明确的观点是每一种对象的构成模型都有其存在的合理性,并没有任何一种模型是错误的模型这一说。

既然如此,我们所要做的就是认清这些对象构成模式的特性,并且能够在最恰当的业务场景中选择最合适的模型加以应用。那么,从面向对象思想的角度,如果我们将这些对象运作模式做一个纵向的比较,它们有没有优劣之分呢?

结论 将对象运作在“属性-行为”模式上,最符合面向对象编程思想的本意。

这一结论承接了上一个结论,可以说是对象建模方式的一种合理的理解和扩展,也回答了我们刚才的问题。当我们进行对象建模的时候,总是首先需要根据业务情况选择一个对象建模设计的角度,而这个角度往往取决于对象在整个程序中所起的作用。例如,当我们需要进行数据传输或者数据映射时,我们应该基于对象的“属性模式”来进行对象建模;当我们需要进行动作响应时,我们应该基于对象的“行为模式”来进行对象建模。

然而,运行在“属性模式”中的对象并不是说完全就不能具备行为动作。基于某一种模式进行建模,只是我们考虑对象设计的角度不同。如果我们站在一个“对象构成的完整性”这样一个高度来看待每一个对象,它们总是由属性和方法共同构成。因此,在任何一种对象的构成模式上走极端都是不符合面向对象编程思想本意的。

软件大师Martin Fowler就曾经撰文指出,在对象建模时不应极端地将对象设计成单一的“属性模式”。读者可以参考:http://www.martinfowler.com/bliki/AnemicDomainModel.html获得全文描述。

The fundamental horror of this anti-pattern is that it's so contrary to the basic idea

of object-oriented design; which is to combine data and process together.

有关这一点,也引起了许多国内软件开发人员的深入讨论,并且引申出许多极具特色的名词,诸如:“贫血模型”、“失血模型”、“充血模型”、“胀血模型”等等。这些讨论非常有价值,对于对象建模有兴趣的读者可以使用搜索引擎就相关的讨论进行搜索。

既然存在着那么多有关“领域模型”的理解方式,为什么Martin Fowler这样的软件大师还是推荐尽可能使对象运行在“属性-行为”模式之上呢?除了它自身在构成形式上比较完整,能够比其他两种运行方式更容易表达对象的逻辑语义之外,还有什么别的特殊考虑吗?笔者通过思考和分析,给出可能的两个理由:

当对象运作在“属性--行为”模式上时,我们能够最大程度地应用各种设计模式

对于设计模式有深入研究的读者,应该会同意这个观点。设计模式的存在基础是对象,因而设计模式自身的分类也围绕着对象展开。我们可以发现,绝大多数的设计模式需要通过类、接口、属性、方法这些语法元素的共同配合才能完成。因而,单一的属性模式和行为模式的对象,在设计模式的应用上很难施展拳脚。

当对象运作在“属性--行为”模式上时,我们能够最大程度地发挥对象之间的协作能力

仔细分析对象的关系模型,我们会发现无论是对象的从属关系还是对象的协作关系,它们在绝大多数情况下是通过对象之间的属性引用来完成的。这种属性引用的方式,只是在形式上解决了对象和对象之间进行关联的问题。而真正谈到对象之间的配合,则不可避免地需要通过行为动作的逻辑调用来完成,这也是对象协作的本质内容。

对象建模是一个很深刻的哲学问题,它将直接影响我们的编程模式。所以对于建模这个问题,大家应该综合各家之言,并形成自己的观点。笔者在这里的观点是:对象建模方式首先是一个哲学问题,取决于设计者本身对于业务和技术的综合考虑。任何一种建模方式都不是绝对正确或者绝对错误的方式。我们所期待看到的对象建模的结果是提高程序的“可读性”、“可维护性”和“可扩展性”。一切建模方式都应该首先服务于这一基本的程序开发的最佳实践。

结论 建立对象之间的关系模型是面向对象编程的核心内容。

对象建模是一个很复杂的逻辑抽象过程。事实上,对象建模最难的地方并不在于设计某一个单体对象的属性构成或者方法构成。因为之前我们也提到,对象总不能以单体的形式孤立存在。对象与对象之间总是以某种方式相互关联、相互配合。这种关联要么形成对象之间的从属关系,要么通过对象的行为方法进行互相协作。

由此可见,我们在进行对象建模的时候,必须优先考虑的就是对象与对象之间的关系模型,关系模型决定我们进行对象关联的具体形式,选择合适的编程语言语法进行关联关系的表达。

将对象之间的协作和关联关系作为设计对象的最重要的考虑因素,可以时刻提醒我们不要将过多的逻辑放在一个对象之中。因为当考虑到对象之间的协作和关联关系,我们就可以充分挖掘每一个对象的职责和语义,从而避免一个对象过于复杂而变得不可维护。

2.2 框架的本质

什么是框架?框架从何而来?为什么要使用框架?这是一系列简单而又复杂的问题。简单,是因为它们本身似乎不应该成为问题。框架实实在在存在,并且在开发中发挥着重要的作用,我们的日常工作,遵循着框架所规定的编程模式,在其指导之下,我们能够编写更为强大的程序。说其复杂,是因为框架本身又是如此纷繁复杂,我们在使用框架的同时,往往会迷失其中。

任何事物都有蕴含在其内部的本质。无论框架本身有多复杂,我们所需要探寻的,都是其最为内在的东西。框架为什么会产生?我们来看一个最最简单的例子。

在Java中,如果要判定一个输入是否为null或空字符串,我们会使用下面的代码:

          if(str == null || str.length() == 0) {
              // 在这里添加你的逻辑
          }

这段代码非常普通,简单学习过Java语法的程序员都能够读懂并编写。那么这段代码是如何运作的呢?我们所编写的Java程序,首先获得的是来自于Java的基本支持:语法支持与基本功能的API级别的支持(str.length()方法实际上就是JDK所提供的字符串的基本API)。换句话说,我们编写的所有程序,都依赖于一个最最基本的前提条件:JDK所提供的API支持。

当一个需求被重复1000次,那么我们就需要重复1000次针对需求的解决办法,这是一个显而易见的道理。然而当上面的代码片段散落在我们的程序中1000次,我们不免会思考,是不是有什么简单有效的途径可以把事情做得更加漂亮一些呢?我们可以针对代码片段做一次简单的逻辑抽取重构,如代码清单2-4所示。

代码清单2-4 StringUtils.java

          // 定义一个类和一个静态工具方法来抽象出将被重复调用的逻辑
          public abstract class StringUtils {
              // 封装了一个静态方法
              public static boolean isEmpty(String str) {
                  return str == null || str.length() == 0;
              }
          }
          // 引用静态方法取代之前的代码片段
          if(StringUtils.isEmpty(string)) {
              // 在这里添加你的逻辑
          }

在上面的代码段中,我们定义了一个静态方法,将之前写的那段逻辑封装起来。这一层小小的封装虽然看上去是一个“换汤不换药”的做法,但是从深远意义上来说,我们至少可以从以下两个方面获得好处:

可读性

静态方法的签名从一个角度向我们揭示了一段逻辑的实际意义。比如在这个例子中,isEmpty表示“判定某个输入是否为空”。与之前的代码片段相比,如果我们在一个1000行的程序代码片段中观察这2种不同的代码形式,那么前者往往会被你无视,它完全无法引起你的思维停顿,而后者却能够显而易见地在逻辑上给你足够且直观的提示。

可扩展性

如果我们对上述需求稍作改动,程序同时需要对输入为空格的字符串做出同样的判定。我们同样将上述的需求应用1000次,那么前者将导致我们在整个应用中进行搜索并替换修改1000次,而后者只需要针对我们封装的逻辑修改1次即可。

从上面的例子我们可以看出,虽然仅仅对代码做了一次简单的重构,却在上述的两个方面为我们解决了潜在的问题。这一现象或许直到现在你才意识到,但很多程序员前辈在很早以前就意识到了。因而,早就有人为此编写了类似的代码。比如说,类似的方法就存在于Apache的commons-lang的JAR包中,如代码清单2-5所示。

代码清单2-5 StringUtils.java

              package org.apache.commons.lang;
              public class StringUtils {
              // 这里省略了许多其他的代码
              public static boolean isEmpty(String str) {
                  return str == null || str.length() == 0;
              }
          }

当我们将Apache的commons-lang的JAR包加到CLASSPATH中时,就能在程序的任何地方“免费地”使用上述方法。也就是说,我们自己无须自行编写代码对JDK进行扩展,因为Apache的commons-lang已经为我们做了。既然如此,我们唯一所需要做的,只是把别人做的东西加到CLASSPATH中并且使用它而已。

这是一个很熟悉的过程,不是吗?我们在搭建程序运行的基本环境时,指定程序所依赖的JAR文件是其中的一个重要步骤。而这一步骤,实际上包含了Java开发中最最基本而浅显的道理:

结论 当我们加载一个JAR包到CLASSPATH时,实际上是获得了JAR中所有对JDK的额外支持。

我们的程序就像一个金字塔形状。位于最底部的当然是JVM,提供运行Java程序的基础环境,包括对整个Java程序的编译运行。在这个之上的是JDK,JDK是构建在JVM之上的基本的对象行为的定义(我们在搭建开发环境时所安装的JDK就是这个)。而再往上,是一个具备层次结构的JAR层,所有被加载到CLASSPATH中的JAR文件都搭建在JDK层次之上,它们之间可能形成互相依赖,但不管怎么说,它们的作用都是提供JDK以外的功能支持。最后,在金字塔尖的,才是我们日常编写的应用程序,它将依赖于金字塔低端的所有程序。这样一个结构如图2-3所示。

图2-3 Java应用的金字塔结构

仔细观察一下处于中间的JAR层,这个层次的组成结构与其他的层次不同。它是由一块块砖头堆砌而成,上层的砖块搭建在下层的砖块之上。如果我们把其中的每一块砖都比作一个JAR文件,它们之间也就形成了明显的具备层次的依赖关系。

这个层次中的任何JAR文件本身可能并不为最终的程序提供具体的功能实现,但它却为我们编写程序提供了必要的支持。如果查看一个标准的J2EE程序运行时所依赖的CLASSPATH中的JAR包,会发现我们所熟悉的那些“框架”,实际上都蕴涵其中。我们在这里给出一个最简单的示例程序在Eclipse中的CLASSPATH截图,如图2-4所示。

图2-4 Eclipse中的CLASSPATH示例

从图中我们看到,JRE System Library是整个应用程序最基本的运行环境。而无论是Struts2还是Spring,它们都以JAR文件的形式被加载到程序运行所依赖的CLASSPATH中,并为我们的应用程序使用。如果我们用更加通俗的话来表述这一现象,则是:

结论 框架只是一个JAR包而已,其本质是对JDK的功能扩展。

当我们说一个程序使用了Spring框架,隐藏在背后的潜台词实际上是说,我们把Spring的分发包加入到CLASSPATH,并且在程序中使用了其功能。框架,其实就是这么回事!就是如此简单!

到现在为止,框架似乎还没有任何在我们的知识范畴以外的东西,它们的本质是如此一致,以至于我们很容易遗忘把一个JAR文件加入到CLASSPATH中的初衷:解决在某个领域的开发中所碰到的困境。正如我们在一开始使用的那个例子一样,框架作为一个JAR包,实际上是许许多多解决各种问题的类和方法的集合。当然,更多时候,它们包含了编写这些JAR包的作者所创造的许多最佳实践。

结论 框架是一组程序的集合,包含了一系列的最佳实践,作用是解决某个领域的问题。

只有解决问题才是所有框架的共同目标。框架的产生就是为了解决一个又一个在开发中所遇到的困境。不同的框架,只是为了解决不同领域的问题。所以,对于广大程序员来说,千万不要为了学习框架而学习框架,而是要为了解决问题而学习框架,这才是一个程序员的正确学习之道。

2.3 最佳实践

一切程序的编写,都需要遵循特定的规范。这里所说的规范,往往是建立在运行环境之上的一系列概念和实现方法的基本定义,并被归纳为一个完整的体系。例如,我们使用Java来进行Web开发,所需要遵循的最基本的规范就是我们所熟悉的Servlet标准、JSP标准,等等。

建立在标准和规范之上的,是各种针对这些标准和规范的实现。这些实现构成了程序运行的基本环境。例如,Tomcat有对Servlet标准的实现方式,而Websphere则有不同的实现方式。然而它们在本质上都实现了Servlet标准所规定的接口,从而让我们的应用程序可以透明地使用这些API,而无须关心真正的Web容器内部的实现机理。

我们所编写的程序,总是建立在一系列的规范和基本运行环境之上。面对纷繁复杂的业务需求,不同的程序员可以按照自己的意愿来编写程序,因此,即使为了表达相同的业务功能,不同的程序代码之间的差异性也是很大的。程序的差异性有时候会给人以创新的灵感,但是更多的时候会造成麻烦。因为差异性越大,维护起来就越麻烦。出于对可维护性和可读性的要求,我们所希望的程序最好能从宏观层面上看上去是一致的,使得每一个程序员都能够读懂并合理运用,这才是我们的目标。这一目标,我们习惯上称之为最佳实践。

结论 最佳实践(Best Practice),实际上是无数程序员在经过了无数次的尝试后,总结出来的处理特定问题的特定方法。如果我们把每个程序员的自由发挥看作是一条通往成功的路径,最佳实践就是其中的最短路径,它能够极大地解放生产力。

所有这些最佳实践,最终又以一个个JAR包的形式蕴含在框架之中,对我们的应用程序提供必要的支持,因此我们有必要在这里探寻一些最最基本的最佳实践,从更深的层次了解框架存在的意义和框架的设计初衷。在之后的章节中,我们会反复提及这些最佳实践,因为它们不仅能够指导我们进行程序开发,它们本身也蕴含在Struts2的内部。

最佳实践 永远不要生搬硬套任何最佳实践,真理之锁永远只为最合适的那把钥匙开启。

这是一条凌驾于任何最佳实践之上的最佳实践。在使用框架编写程序时,程序员最容易犯的毛病就是对某项技术或者某个框架绝对迷信,并将它生搬硬套于任何程序开发之中。应用程序永远服务于具体的业务场景,对于不同的业务需求,我们的解决方案也会有所区别,自然也会涉及不同的框架选择。

在实际开发中,我们遇到的许多编程开发的问题都是没有固定答案的哲学取向问题。所以,往往没有“最好”的答案,只有“最合适”的答案。这是在面对多种解决方案进行取舍时的一个基本准绳。

最佳实践 始终保证程序的可读性、可维护性和可扩展性。

可读性、可维护性和可扩展性,就像三脚架的三个支撑脚,缺一不可。任何对程序的重构,实际上都围绕着这三个基本原则进行,而它们也同时成为衡量程序写得好坏的最基本标准。代码的不断重构、框架的产生实际上都来自于这三个程序内在属性的驱动。

我们之前已经反复提到了程序的可维护性和可扩展性。事实上,程序的可读性也是程序所应具备的必不可少的基本属性,失去了可读性的程序,其可维护性和可扩展性也就无从谈起了。这三大原则从方法论的角度规定了一切最佳实践都不能违背这三大程序的基本属性。否则,我们迟早会为一些蝇头小利而舍弃程序开发的源头根本。当一个程序失去了可读性、可维护性和可扩展性,它也就失去了生命力。

最佳实践 简单是美(Simple is Beauty)。

简单是美是一种指导思想,它其实包含两个层次的意思。第一层意思是消除重复(Don't repeat yourself),这是一个显而易见的代码重构标准。第二层意思则是要求我们化繁入简(Heavy to Light),用尽量少的代码语言来表达尽量多的逻辑意义。

简单是美,将最佳实践的要求细化到了方法论的层面。然而,无论我们的程序如何简单,都应该始终记得,简单但必须可读,简单但必须可扩展。切忌为了一些细节,而忘记更大的原则。

最佳实践 尽可能使用面向对象的观点进行编程。

我们可以看到,这个层面的最佳实践,已经从基本准则和指导思想转向了具体的编程层面。虽然面向对象自身也只是一种编程的指导思想,然而它却是和程序设计与实现息息相关并且对程序编写影响最大的一条编程准则。

面向对象这个概念本身就是一个非常耐人寻味的问题。要讨论面向对象的概念、设计和方法论,恐怕一天一夜都讲不完。在本章之初,我们从“对象”这个概念入手,通过对“对象”内部结构的分析,试图向读者展示面向对象编程中的一些重要理论。读者对这些理论不应停留在死记硬背的层面,而是要将它们融入到框架的设计理念中去理解。同时,这些理论也将成为我们判别框架和设计方案优劣的重要标准。

最佳实践 减少依赖(消除耦合)。

之前在分析框架的本质时已经提到,任何Java程序总是依赖于其运行环境(JVM层)和支持应用程序的JAR层。加入到CLASSPATH中JAR越多,就意味着程序对外部环境的依赖度越高,对外部环境的依赖度越高,就意味着程序本身越难以脱离特定的外部环境进行单元测试。因此,减少甚至消除依赖,也成为许多框架所追求的目标。

Struts2在这一点上做得尤为成功。Struts2不但实现了Web框架与Web容器之间的解耦合,还在此基础之上实现了各个编程元素之间的有效沟通。在之后的章节中,我们会深入探究Struts2在这条最佳实践上所做出的努力。

2.4 Web开发的基本模式

到此为止,我们花了大量的篇幅介绍了许许多多与Web开发完全无关的东西。无论是面向对象的概念、框架的本质内容还是我们开发中应当遵循的最佳实践,它们都是程序员需要培养的内在修养。接下来,我们将话题真正转入Web开发,来看看在Web开发中应该遵循什么样的最佳实践。

2.4.1 分层开发模式

之前我们讨论了Web开发中几条基本的最佳实践,它们会成为贯穿本书始终的指导思想。明确了指导思想,我们有必要从方法论的角度来探讨一下Web开发的一些基本模式。

从宏观上来说,Web开发模式中最最重要的一条是分层开发模式。分层开发模式是指,在开发J2EE程序时,将整个程序根据功能职责进行纵向划分。一个比较典型并为大家所熟知的划分方法是将整个程序分为:表示层、业务层和持久层,如图2-5所示。

图2-5 分层开发模式示意图

不同的层次,实际上承担了不同的功能职责:

表示层(Presentation Layer)—负责处理与界面交互相关的功能

业务层(Business Layer)—负责复杂的业务逻辑计算和判断

持久层(Persistent Layer)—负责将业务逻辑数据进行持久化存储

分层开发模式是技术层面的“分而治之”设计思想的一种体现。而蕴含在其内部的驱动力还是我们反复强调的:程序的可读性和可扩展性。出于可读性考虑,把不同功能职责的代码分开,能够使程序流程更加清晰明了;出于可扩展性考虑,也只有把相类似的功能归结为一个纵向层次,才使得我们在这个层次之上研究通用的解决方案成为可能。

分层开发模式,从逻辑上是将开发职责分派到不同的对象之上去执行的一种设计思想。回顾我们在面向对象浅谈的章节中所提到的对象协作关系,也正是分层开发模式的理论依据。

既然是职责分派,我们就不得不分清什么是职责、什么样的对象适合什么样的职责。如此一来,有关分层开发模式的讨论就变成了一个典型的哲学问题。凡是哲学问题,都会出现正反两派。分层开发模式所涉及的争论主题主要包括两个方面:第一,分层开发到底有无必要?第二,对于一个J2EE程序到底分为多少层进行开发比较合适?

我们先来探讨第一个问题:对一个程序实施分层开发到底有无必要?分层开发模式,为程序的可扩展性提供了可能性,但是当问题的作用域变小时,分层开发模式反而成为一种累赘。有许多程序员会抱怨,一个简单的逻辑功能,动辄就要十几个文件配合上百行代码来完成。此时,我们不禁要问:我们的开发真的需要分层吗?分层开发到底为我们带来了多少好处呢?针对这一问题,我们不妨来看看Struts2的一个官方的FAQ,如图2-6所示。

图2-6 Struts的FAQ

非常明显,当问题的作用域发生变化时,解决问题的方法也要相应做出改变。所以,分层开发模式,对于大型企业应用或者产品级的应用程序开发是有着重要意义的;然而当一个应用程序足够小,并且需求的变更处于可控的范围之内时,我们对于分层开发模式的选择应该谨慎。这就是所谓的“杀鸡焉用牛刀”。

我们再来看看第二个问题:对于一个J2EE程序,到底要分多少层进行开发比较合适?这是一个与整个应用程序构架相关的话题。有许多程序员赞同分层开发模式,不过他们都希望将层次分得尽量简单,崇尚“简单是美”的原则。对于这一问题,实际上也没有绝对正确的答案。因为一切脱离了业务实际的架构设计都是虚幻的。我们只能在实践中不断总结,并将前人的许多经验作为我们进行开发层次划分的重要依据,选择适合于实际业务需求的开发层次,才是程序开发的最佳实践。

这些有关分层开发的哲学问题的讨论,每个程序员都有自己的见解。然而从框架的角度,我们也能看出一些框架的设计者对于某个开发层次的理解,因为我们最最熟悉的这些著名的框架,实际上就是为了应对各个开发层次的编程问题而设计的解决方案。比如说:

Struts2是表示层的框架;Spring是业务层的框架;Hibernate是持久层的框架。

在本书中,我们所有讨论的重点实际上是围绕着表示层的解决方案 — Struts2进行的。笔者花了那么多笔墨,才把Struts2这位主人公引出来的目的,是希望读者能够站在全局的高度来审视Struts2,也只有这样,才能够真正学好每一个开源框架。

2.4.2 MVC模式

在分层开发模式的前提下,每一个层次都可以单独研究,并寻找合适的解决方案和最佳实践。对于表示层,有一种称之为MVC模式的最佳实践被广泛使用,并在此基础上创建了许多基于这种模式的开发框架。

MVC模式实际上是众多经典的Java开发模式中的一种。它的基本原理是通过元素分解,来处理基于“请求-响应”模式的程序中的各种问题。

M (Model)—数据模型

V (View)—视图展现

C(Control)—控制器

任何一个B/S应用,其本质实际上是一个“请求-响应”的处理过程的集合体。那么MVC模式是如何被提炼出来并成为一个模式的呢? 我们来模拟一个“请求-响应”的过程,如图2-7所示。

图2-7 请求-响应模式

在整个请求-响应过程中,有哪些元素是必不可少的呢?

数据模型

在图中,就是顺着箭头方向进行传输的数据,它们是程序的核心载体,也是贯穿程序运行的核心内容。

对外交互

在图中,对外交互表现为一个“头”和一个“尾”。“头”指的是请求发起的地方,没有请求,一切之后的所有内容都无从谈起。“尾”指的是逻辑执行完成后,对外展现出来的执行结果。在传统意义上,我们利用HTML扩展的技术(如JSP等)来实现对外交互,在展现结果时,我们还需要完成一定的展现逻辑,比如错误展示、分支判断,等等。

程序的执行和控制

实际上它不仅是接受请求数据的场所,也是处理请求的场所。在请求处理完毕之后,它还要负责响应跳转。这个部分可能会存在着不同的表现形式。以前,我们用JSP和Servlet,后来用Struts1或者Struts2的Action。而这一变化,实际上包含了我们不断对程序进行重构的过程。

上面这3大元素,在不同的年代被赋予了不同的表现形式。例如,在很久以前,我们使用Servlet或者JSP来编写程序跳转的控制过程,有了Struts1.X后,我们使用框架所定义的Action类来处理。这些不同的表现形式有的受到时代的束缚,表现形式非常落后,有的甚至已经不再使用。但是我们忽略这些外在的表现形式就可以发现,这不就是我们已经熟悉的MVC吗?

数据模型—Model

对外交互—View

程序的执行和控制—Control

MVC的概念就这么简单,这些概念其实早已深入我们的内心,而我们所缺乏的是将其本质挖掘出来的能力。我们来看看如图2-8所示的这幅流行了很多年的讲述MVC模型的图。

图2-8 MVC模型图

在这幅图中,MVC三个框框各司其职,结构清晰明朗。这也成为我们进行编程开发的最强有力的理论武器,我们需要做的,只是为这些框框赋予不同的表现形式。实际上,框架就是这么干的!而框架的高明之处,仅仅在于它不仅赋予这些元素正确而恰当的表现形式,同时解决了当元素运行起来时所碰到的各种问题。因此,我们始终应该做到:程序时时有,概念心中留。只要MVC的理念在你心中,无论程序怎么变,都能做到万变不离其宗。

2.5 表示层的困惑

当表示层有了MVC模式,程序开发就会变得有章可循。至少,我们不会像无头苍蝇一样无从入手。MVC模式很直观地规定了表示层的各种元素,只要能够通过恰当的程序表现形式来实现这些元素,我们实际上已经在履行最佳实践了。

至此,我们不妨返璞归真,忘记所谓的框架,用最简单的方式来实现一个简单的MVC雏形。在这个过程中,我们不妨回到框架的本质问题上,思考一下究竟一个框架为表示层解决了什么样的编程难题,难道框架只是实现MVC这三大元素那么简单而已?

我们选择Registration(注册)作为业务场景。首先,我们需要一个JSP页面来呈现用户注册的各个字段、一个User类来表示用户实体以及一个RegistrationServlet类来处理注册请求。相关实现源码如代码清单2-6、代码清单2-7和代码清单2-8所示。

代码清单2-6 registration.jsp

          <form method="post" action="/struts2_example/registration">
            user name: <input type="text" name="user.name" value="downpour" />
            birthday: <input type="text" name="user.birthday" value="1982-04-15" />
            <input type="submit" value="submit" />
          </form>

代码清单2-7 User.java

          public class User {
              private String name;
              private Date birthday;
              public User() {
              }
              // 此处省略setter与getter方法
          }

代码清单2-8 RegistrationServlet.java

          public class RegistrationServlet extends HttpServlet {
              @Override
              protected void doPost(HttpServletRequest req, HttpServletResponse
          resp) throws ServletException, IOException {
                  // 从request获取参数
                  String name = req.getParameter("name");
                  String birthdayString = req.getParameter("birthday");
                  // 做必要的类型转化
                  Date birthday = null;
                  try {
                      birthday = new SmpleDateFormat("yyyy-MM-dd").
          parse(birthdayString);
                  } catch (ParseException e) {
                  e.printStackTrace();
                  }
                  // 初始化User类,并设置字段到user对象中去
                  User user = new User();
                  user.setName(name);
                  user.setBirthday(birthday);
                  // 调用业务逻辑代码完成注册
                  UserService userService = new UserService();
                  userService.register(user);
                  req.getRequestDispatcher("/success.jsp").forward(req, resp);
              }
          }

除了上述这3段源代码外,我们还需要建立起JSP页面中的form请求与Servlet类的响应之间的关系。这一关系,是在web.xml中维护的,如代码清单2-9所示。

代码清单2-9 web.xml

          <servlet>
            <servlet-name>Register</servlet-name>
            <servlet-class>example.RegistrationServlet</servlet-class>
          </servlet>
          <servlet-mapping>
            <servlet-name>Register</servlet-name>
            <url-pattern>/struts2_example/registration</url-pattern>
          </servlet-mapping>

我们来看看上面的这4段代码是如何构成MVC的雏形的。

Model(数据模型)—User.java

View(对外交互)—registration.jsp

Control(程序执行和控制)—RegistrationServlet.java

URL Mapping(请求转化)—web.xml

我们可以看到MVC的实现似乎并不复杂。在不借助额外的框架帮助的前提下,只要基本知晓JSP和Servlet标准(它们是使用Java进行Web开发的规范和标准),任何程序员都可以像模像样地实现MVC模式,因为从原理上讲,MVC只是一个概念,我们只需要把这个概念中的各个元素赋予相应的程序实现即可。

不过程序终究是一个动态的执行过程。一旦程序开始运行,上面的这些程序实现就会开始遭遇种种困境。这些困境主要来源于两个方面:其一,出于程序自身的可读性和可维护性考虑,需要通过重构来解决程序的复杂性困境。其二,出于业务扩展的需求,需要通过框架级别的功能增强来解决可扩展性困境。

问题1 当浏览器发送一个Http请求,Web容器是如何接收这个请求并指定相应的Java类来执行业务逻辑并返回处理结果的?

这个问题是使用Java进行Web开发的核心问题之一,我们将这个问题简称为URL Mapping问题。这个问题的本质实际上来源于Http协议与Java程序之间的匹配和交互。Web开发经过了多年的发展,这一核心的哲学问题也经历了多次重大变革,有的崇尚由繁至简,有的则从形式多样化入手。

在上面的例子中,我们可以看到使用web.xml来表达URL Mapping关系遇到的困境:当系统变大,这种配置上的重复操作会让web.xml变得越来越大而难以维护。不仅如此,web.xml的配置也无法为URL Mapping建立起合适的规则引擎。

由此,解决URL Mapping问题的核心在于建立一套由Http协议中的URL表达式到Java世界中类对象的规则匹配引擎。额外的,这种规则匹配最好比较灵活而简单又不失必要的可维护性。

问题2 Web应用是典型的“请求-响应”模式的应用,数据是如何顺利流转于浏览器和Java世界之间的?面对Http协议与Java世界数据形式的不匹配性,我们如何能够在流转时做到数据类型的自动转化?

这个问题伴随着问题1而来,数据请求与数据返回相当于是基于“请求-响应”模式的Web程序的输入和输出。数据的本质是存储于其中的信息,只不过数据在不同的地方有不同的表现形式。例如,在浏览器中,数据总是以字符串形式展现出来,表现出“弱类型”的特征;在Java世界,数据则体现为一个个结构化的Java对象,表现出“强类型”的特征。于是,就需要有一个工具能够帮助我们解决在数据流转时的数据形式的相互转化。

在上面的例子中,我们可以看到RegistrationServlet中,我们编写了额外的代码,把页面上传递过来的日期值转化为Java中的Date对象。在参数的数量和Java对象越来越复杂的情况下,这种额外的代码就会变成一种灾难,甚至成为我们开发的主要瓶颈之一。

解决数据流转问题的方案是使用表达式引擎。将表达式引擎插入到程序逻辑执行之前,我们就能从复杂的对象转化中解放出来,从而进一步简化开发流程。

问题3 Web容器是一个典型的多线程环境,针对每个Http请求,Web容器的线程池会分配一个特定的线程进行处理。那么如何保证在多线程环境下,处理请求的Java类是线程安全的对象?如何保证数据的流转和访问都是线程安全的?

这个问题与问题1一样,也是Web开发中的核心问题之一,因为它涉及Web开发中最为底层的处理机制问题。在上面的例子中,我们使用的是基于Servlet标准的方式进行编程,扩展Servlet用于处理Http请求。然而恰恰就是这种编程模型,是一种非线程安全的编程模型,因为Servlet对象是一个非线程安全的对象。也就是说,如果我们在doPost方法中访问RegistrationServlet中所定义的局部变量,就会产生线程安全问题(第4章会重点介绍线程安全问题产生的来龙去脉)。

传统的表示层框架对于这个问题的处理方式是采用规避问题的方式。既然Servlet对象不是一个线程安全的对象,那么我们就干脆禁止在Servlet对象的方法中访问Servlet对象的内部变量。这种鸵鸟算法固然是一种有效的方案,但它却不是一种合理的方案。最致命的一点是,它是一种非语法检查级别的禁止,因此也就无法从根本上杜绝程序员犯这样的错误。

另外一种解决方案就是在整个请求周期中引入ThreadLocal模式,通过ThreadLocal模式的使用,将整个过程的对象访问都线程安全化,彻底解决多线程环境下的数据访问问题(有关ThreadLocal模式的方方面面,我们在后续章节中会详细介绍)。ThreadLocal模式的引入对于Web层框架的影响是深远并且颠覆性的,因为它为框架摆脱Web容器的依赖铺平了道路,意味着我们可以通过合理的设计,在脱离Servlet等Web容器元素的环境中进行编程。

问题4 Controller层作为MVC的核心控制器,如何能够在最大程度上支持功能点上的扩展?

问题4来源于我们对程序本身的自然属性(可读性和可扩展性)的需求。这一内在需求实际上也驱动着我们着手在整个MVC的构架级别设计更为成熟有效的自扩展方案。

从一个更加宏观的角度来帮助我们理解这个问题,我们来举一个制药工厂生产药品的例子。一个工厂在进行批量生产时,总是会引入“生产线”的概念。生产线能够把整个制药过程划分成若干道工序,当原材料经过每一道工序,最终就会成为一个可出厂销售的药品。某一天,由于市场推广的原因,需要改变药品的包装,那么我们对这条生产线的要求就是它能够改变“包装”这道工序的流程,更改成新的包装。

在上面的例子中,我们可以看到并没有一个“生产线”的概念。这种情况下,我们日后对于逻辑功能的扩展就变得困难重重。虽然我们发现,RegistrationServlet或许和其他所有的Servlet有着非常类似的执行步骤:接收参数、进行类型转换、调用业务逻辑接口执行逻辑、返回处理结果。然而我们却缺乏一条可以任意配置调度的生产线将这个过程规范起来。

解决这个问题从直观上来讲似乎很容易:没有生产线,我们建一条生产线就行了。而事实上,“造轮子”实在是一件费时费力的事情,因为我们要考虑的方面实在太多。这时我们就不得不借鉴许多前辈的经验了,寻找某些事件定义的框架,遵循框架的定义规范来进行编程将是我们解决这个问题的主要途径。

问题5 View层的表现形式总是多种多样的,随着Web开发技术的不断发展,MVC如何在框架级别提供一种完全透明的方式来应对不同的视图表现形式?

这一问题是基于View(视图)技术的不断发展,造成传统的基于HTML的视图已经不能满足所有的需求而提出的。当今,越来越多新的视图技术被用于Web开发中,例如,模板技术、JSON数据流、Stream数据流、Flash展现等等。

在上面的例子中,我们可以看到负责视图层跳转的RegistrationServlet是通过硬编码方式完成程序执行跳转的。这种方式不但无法支持多种新的视图技术,同时也无法使我们从复杂的视图跳转的硬编码中释放出来。

解决这个问题的最有效途径是把不同的视图技术进行分类,针对不同的分类封装不同的视图跳转逻辑,而最重要的一步是将这两者与之前我们所提到的生产线有机结合起来。

问题6 MVC模式虽然很直观地为我们规定了表示层的各种元素,但是如何通过某种机制把这些元素有机整合在一起,从而成为一个整体呢?

这个问题非常宏观,却是我们不得不去面对的一个问题。MVC虽然在概念上被规定下来,在实现上却需要一个完整的机制来把这些元素都容纳在一起。通常情况下,我们往往把这种机制称之为配置元素。配置元素是构成程序的重要组成部分,它把各种形式的程序通过某种配置规则联系在一起。之前我们提到的URL Mapping实际上也属于配置规则的一种,视图的跳转也是配置规则的一种。只有当这种配置规则被建立起来,MVC模式才能真正运作起来。

这一系列配置元素在框架内部往往被定义成统一的可以被框架识别的数据结构并在系统初始化的时候进行缓存。而这些被缓存了的对象,也成为主程序的控制流在MVC框架中各个元素之间进行流转的依据。

如果从元素的表现形式上来看配置元素和控制流的关系,我们实际上可以看到整合过程的两个层面:数据结构和流程控制。所谓的框架,我们也只是在这两个层面上做文章,一方面规定好这些配置元素的定义,另一方面指定程序运转的流程,从而控制和整合散落在各处的表示层元素。

2.6 如何学习开源框架

正确的学习方法不仅能够事半功倍,也能够使我们更加接近真理。在了解了框架的本质和Web开发模式之后,我们来讨论一下学习开源框架的基本方法。

在这里为大家总结了一些正确的学习方法和最佳实践,这些不仅是笔者多年开发中的心得体会,也汲取了网络上的大家之言,希望对初学者或者正在为学习开源框架犯愁的朋友带来一些启示。这些学习方法,不仅适用于Struts2,同样适用于许多其他的开源框架。

最佳实践 阅读、仔细阅读、反复阅读每个开源框架自带的Reference。

这是学习框架最为重要,也是最开始最需要做的事情。不幸的是,事实上,绝大多数程序员对此并不在意,并且总是以种种理由作为借口不仔细阅读Reference。

程序员的常见借口之一:英语水平跟不上,英文文档阅读起来太吃力。针对这样的借口,我们需要指出,阅读英文文档是每个程序员必须具备的基本素质之一,这就和调试程序需要耐心一样,对一个程序员来说非常重要。当然,阅读英文文档这一基本素质是一点一滴积累培养起来的,对于那些阅读起来实在觉得吃力的朋友,笔者的建议是结合中文的翻译版本一起看。国内有许多开源组织,例如满江红的开源支持者已经为大家精心做了许多很有价值的翻译,例如Spring、Hibernate等都有对应的中文翻译文档。但是大家必须注意,看中文文档,必须和英文文档对照,因为没有人可以确保翻译能够百分之百正确,语义的不匹配会给你带来极大的误导,通过对照,才能够将误解降到最低。

程序员的常见借口之二:Reference太长,抓不住重点。在这里,笔者给出的建议是:耐心,耐心,还是耐心!从Reference的质量而言,其实大多数开源框架的Reference都是非常优秀的,基本包含了框架的方方面面。尤其是Struts2,由于历史原因,Struts2的Reference基本上都是一个一个的专题Wiki文章拼起来的文档,每篇文章都有一个固定的主题,不仅包含原理解析、注意事项,有的还包含源码解析和示例讲解。阅读Reference可能会非常枯燥,但是从价值的角度看,对Reference的阅读往往是对大家帮助最大的。因此,笔者对阅读Reference的建议是,多看几遍。第一遍,你可以采取浏览(scan)的方式,目的是了解框架的整体架构的大致功能。第二遍,挑重点章节仔细阅读,并且辅以一定的代码实践,目的是彻底掌握某个分支领域的知识。第三遍,带着问题阅读,在文档中寻找答案。

笔者之所以强烈推荐大家仔细阅读开源框架自带的Reference,主要基于以下的两个原因:

权威性

这些自带的Reference多数出自这些开源框架的作者或者开发人员之手。还有谁能够比他们自己更了解他们自己编写的产品呢?自己写的程序,到底有哪些优点,如何使用,自己肯定是最最清楚的,所以要说到权威性,不可能有任何文档比自带的Reference更加权威。

正确性

自带的Reference几乎很少犯错,所以不会给你带来什么误导信息。不仅如此,许多Reference已经为你总结了框架使用过程中的许多最佳实践。所以我们没有理由不直接通过这些Reference来获得第一手的资料。

最佳实践 精读网络教程。

对于很多初学者来说,他们对看Reference这种学习方式的接受程度很低。相反,他们会去转而学习一些网络教程。一般而言,这些学习材料的实际价值要比Reference低很多。主要原因在于,作者在编写这些教程时,多数都会加入他们自己的学习思路,而忽略了框架本身所期望达到的程序开发最佳实践,甚至会给很多读者以:“程序就是这么写的”的误导。所以,对于网络上的绝大多数网络教程,需要读者有足够的甄别能力,否则很容易被带入歧途。

网络上还有很多原版教程,例如《XXX in Action》系列。《XXX in Action》系列的书籍在市场上深受好评。然而,这些系列的书籍有些内容也带有作者个人的感情色彩。当然,每个作者在编写书籍或撰写教程的过程中都会夹带自己的感情色彩,这本不是什么坏事,不过既然我们已经有了Reference作为阅读的主体了,对这类书籍,我们需要采取的态度是“精读”。

很多网络教程,尤其是中文的网络教程,基本上都是网友的经验之谈,也有写成系列文章的。对于网络教程,笔者的建议是:带着问题去读,去搜索你的答案,而不要当作核心文档来阅读。在找到答案之后,也需要通过实践来反复验证,因为许多解决方案可能只是临时的,并不是程序开发中的最佳实践。

最佳实践 搭建环境运行每个开源框架自带的sample项目。

每个开源框架基本上都会自带有sample项目。以Struts2为例,在Struts2的分发包的apps目录下就有多个sample项目,如图2-9所示。

图2-9 Struts2自带的sample项目

Struts2是一个典型的Web层框架,所以所有Struts2的sample项目都以war包的形式给出,大家可以将这些war包的任何一个复制到你的Web容器的运行目录下,启动Web容器就可以访问这些sample项目。

千万不要小看这些sample项目,我们可以从这些项目中获取许多重要的知识和信息。有些知识恐怕连Reference都不曾提及。这些原生态的东西,使得我们完全无须舍近求远地到网络上去到处寻找例子,只要学习这些例子,就足以掌握开源框架的种种特性了。

我们可以就其中的三个sample项目进行举例分析。

struts2-blank-2.2.1.war

一般而言,名为xx-blank-xxx.war的sample项目是一个开源框架的一个最小可运行范例。所以,如果大家仔细学习这个war包中的内容,至少可以发现组成一个Struts2程序的最小元素到底有哪些。在其中的WEB-INF/lib目录下,我们能够找到Struts2程序运行所需要依赖的JAR包的最小集合(如图2-10所示),我们还能从中学习Struts2的各种基础配置的编写等。

图2-10 Struts2所依赖的基本JAR包的最小集合

struts2-portlet-2.2.1.war

这个sample项目告诉我们在Portal环境下的Struts2的应用应该如何编写。通过与struts2-blank-2.2.1.war这个项目的比较,大家可以发现,Struts2在应对不同的应用服务器环境方面的不同。

struts2-showcase-2.2.1.war

这个sample项目是Struts2特性的一个大杂烩,包含了绝大多数的Struts2的特性示例。这个sample项目对于大家阅读Reference是非常有帮助的。比如说,大家在阅读文档时看到了“文件上传”的章节,那么大家就可以参考这个项目中的upload子目录中的相关的类和配置。这相当于一边看文档,一边已经有一个现成的可以运行的例子辅助你进行学习。所以,这个项目与Reference的搭配是相得益彰、互为补充的,可以作为大家学习Struts的最佳资源。

最佳实践 自己写一个sample项目亲身体验。

这一点其实不用多说,大家也应该明白。不过笔者还是见过不少程序员,眼高手低,整天吹嘘这个框架的优点,那个框架的优势,但如果让他自己动手用这些框架写一段程序,又变得手足无措。

实践是检验真理的唯一标准。只有自己亲自动手去实践,才能说明你真正掌握了某种技术,理解了某个框架的特性。在编写自己的sample项目时,大家可以针对不同的特性,人为设置好业务场景(例如,使用“登录”作为一个基本的业务场景),在实践中不断地重构你的代码,从而领悟框架开发中的最佳实践,提升自己的开发水平。

最佳实践 带着问题调试(Debug)开源框架的源码。

如果大家对某个开源框架的使用已经比较熟练,对其内部的原理也基本掌握,或许你就会对其中的某些设计原理和设计思想产生兴趣。这个时候,通过开源框架的源码来寻找问题的答案不失为一个很好的进一步学习的途径。

在学习开源框架的源码时,笔者的建议是当程序运行在Debug模式的状态下,对源码进行调试,在Debug的过程中,查看在开源框架的内部到底运行了哪些类,它们的执行顺序是怎样的以及这些类中临时变量的执行状态。笔者坚决反对逐个package地去阅读源码,这毫无意义。因为程序本身是一个整体,程序之所以成为程序,其本质在于它是动态的、运行的。如果我们逐一去阅读源码,就相当于把一个完整的整体进行肢体分解,那么我们将永远无法看到一个完整的动态执行过程。学习源码,最重要的一点在于抓住一个程序在运行过程中某一时刻某个关键类的运行状态和最终状态,而这些都能通过调试源码来实现,这才是阅读源码的最佳实践。

2.7 小结

本章讨论的话题是非常重要的,因为任何细节都无法脱离基本概念而存在。如果我们要探寻Struts2的细节,就必须了解Struts2作为一个框架存在的基本意义。本章从面向对象的基本概念谈起,探讨了框架的本质,揭示了Web开发过程与框架之间的依存关系、Web开发中的一些最佳实践,并由此提出Web开发中的一些核心问题。最后,笔者还给出了正确学习Struts2的方法供读者参考。这些学习方法是非常有价值的,建议有经验的程序员也看一看。

读完本章,大家不妨带着本章提出的这些核心问题,到本书其他的章节去寻找答案。等到所有的问题都迎刃而解之时,或许大家对于框架和Web开发的理解也将更上一层楼。

回顾本章的所有内容,大家对下面这些问题是否有了一个大致的答案呢?

对象有哪三种构成模式?

对象有哪些关系模型?

什么是框架?框架存在的根本目的是什么?

在整个Web开发的过程中,我们应该牢记哪些最佳实践?

什么是MVC模式?MVC模式对于Web开发的主要作用是什么?

在Web开发中,我们将遇到哪些主要的困境?

Struts2运行所依赖的最少的JAR文件资源的组合有哪些构成?

如何正确学习一个开源框架?