官术网_书友最值得收藏!

第2章 固本清源—Web開發(fā)淺談

現(xiàn)今,在談到Web開發(fā)有關(guān)的話題時(shí),程序員們總是熱衷于討論一些我們耳熟能詳?shù)腤eb開發(fā)框架,如Struts2、Spring、Hibernate等。有些程序員將這些框架奉為寶典,并且趨之若鶩地挖掘框架的方方面面、比較各種開發(fā)框架的優(yōu)劣。對(duì)這些框架的熟悉與否,似乎已成為衡量一個(gè)程序員是否精通Java、精通J2EE開發(fā)的事實(shí)標(biāo)準(zhǔn)。甚至在廣大程序員求職的過程中,這些主流的開發(fā)框架的知識(shí)細(xì)節(jié)也常常成為面試中必考的元素,答不上這些問題,無疑會(huì)為求職蒙上一層陰影。

面對(duì)這些框架,大家是否真的思考過,我們?yōu)槭裁匆獙W(xué)習(xí)這些框架?這些框架到底從何而來?框架的本質(zhì)到底是什么?使用框架,又能夠?yàn)槲覀兊拈_發(fā)帶來什么樣的好處呢?在深入分析Struts2及其源碼之前,我們首先必須弄清楚這些比框架更為核心的問題。因?yàn)橹挥辛私饬?span id="qrkf2rk" class="bold">為什么,我們才能知道怎么做,知道如何才能做得更好。

2.1 面向?qū)ο鬁\談

在談框架之前,我們不得不首先面對(duì)一個(gè)比框架更為重要的概念,那就是面向?qū)ο?/span>的概念。面向?qū)ο蟮母拍钍且粋€(gè)看起來、聽起來簡(jiǎn)單,實(shí)際卻蘊(yùn)含著豐富內(nèi)容的概念。眾多的國(guó)內(nèi)外學(xué)者為了講清楚這個(gè)概念,采用了各種的不同的比喻、也給出了多種多樣的代碼示例,還為面向?qū)ο蟮母拍罱⑵鹨惶淄暾睦碚擉w系。

不過迄今為止,能夠完整地將面向?qū)ο蟮膩睚埲ッ}講清楚、講透徹的畢竟還是少數(shù)。隨著編程語言從早期的“面向過程式”的C語言發(fā)展到后來的C++、Java,直至近幾年來非常熱門的Ruby,面向?qū)ο蟮幕靖拍钜呀?jīng)逐漸成為編程語言的核心設(shè)計(jì)法則。因而“面向?qū)ο蟆钡母拍钜仓饾u成為每個(gè)程序員都認(rèn)同,并且在日常編程過程中遵循的最高綱領(lǐng)。

限于篇幅,我們實(shí)在無法涉及面向?qū)ο蟾拍畹姆椒矫婷?。不過我們可以將話題聚焦在構(gòu)成面向?qū)ο蟾拍钭罨镜脑刂?,這個(gè)基本元素就是:對(duì)象。在接下來的章節(jié)中,我們將分析對(duì)象的構(gòu)成模型以及對(duì)象的關(guān)系模型,并以此為基礎(chǔ)闡述在面向?qū)ο缶幊踢^程中的一些基本觀點(diǎn)。

2.1.1 對(duì)象構(gòu)成模型

2.1.1.1 對(duì)象的構(gòu)成分析

作為面向?qū)ο缶幊套罨镜臉?gòu)成元素,對(duì)象是由一個(gè)叫做類(Class)的概念來描述的。因此,針對(duì)對(duì)象構(gòu)成分析的研究,也就轉(zhuǎn)化為針對(duì)編程語言中類的構(gòu)成分析。以Java語言為例,我們可以對(duì)Java語言中類的定義進(jìn)行一些構(gòu)成上的分析,如圖2-1所示。

圖2-1 對(duì)象的構(gòu)成分析

在圖中,我們可以看到構(gòu)成一個(gè)對(duì)象的基本要素主要有:

簽名(Signature)—對(duì)象的核心語義概括

屬性(Property)—對(duì)象的內(nèi)部特征和狀態(tài)的描述

方法(Method)—對(duì)象的行為特征的描述

在進(jìn)行面向?qū)ο蟮木幊虝r(shí),首先要做的就是對(duì)世間萬物進(jìn)行編程元素的抽象。這個(gè)過程說白了,就是通過使用編程語言所規(guī)定的語法,例如類(Class)或者接口(Interface)來表達(dá)事物的邏輯語義。在圖2-1中我們所談到的構(gòu)成一個(gè)對(duì)象定義的基本要素,實(shí)際上不僅反映出我們對(duì)世間萬物的抽象過程,也是人類使用高級(jí)編程語言來實(shí)現(xiàn)外部世界表述的基本方式。

從圖2-1中我們可以看到,簽名用以描述事物的核心語義,它的作用實(shí)際上是界定我們所描述的事物的范疇。而在對(duì)象的內(nèi)部,作為對(duì)象內(nèi)部構(gòu)成的重要元素,屬性方法剛好從兩個(gè)不同的角度對(duì)事物的內(nèi)在特性給予了詮釋。其中,屬性所勾勒的是一個(gè)對(duì)象的構(gòu)成特性和內(nèi)部狀態(tài)的特性;方法則表達(dá)了一個(gè)對(duì)象的動(dòng)態(tài)行為特性。這就像我們?nèi)艘粯?,人由頭、軀干、四肢構(gòu)成,它們可以看作是人這個(gè)對(duì)象的“屬性”。與此同時(shí),人具有“直立行走”的行為特性,我們可以定義一個(gè)“方法”來模擬這一行為。

以上這些分析,我們還停留在語法這個(gè)層面,因?yàn)闊o論是屬性還是方法,它們都是Java語言的原生語法支持。將事物抽象成為對(duì)象,并且賦予這個(gè)對(duì)象屬性和方法,是一個(gè)很自然的編程邏輯,這也符合面向?qū)ο缶幊陶Z言的基本思路。不過我們也同時(shí)發(fā)現(xiàn)在實(shí)際編程過程中,對(duì)象將表現(xiàn)為三種不同的形態(tài)和運(yùn)作模式。

屬性--行為模式

這種模式是指一個(gè)對(duì)象同時(shí)擁有屬性定義和方法定義。這是對(duì)象最為普遍的一種運(yùn)行模式,絕大多數(shù)對(duì)象都運(yùn)作在這種模式之上。

屬性模式

這種模式是指一個(gè)對(duì)象只擁有屬性定義,輔之以相應(yīng)的setter和getter方法。Java規(guī)范為運(yùn)行在這種模式下的對(duì)象取了一個(gè)統(tǒng)一的名稱:JavaBean。JavaBean從其表現(xiàn)出來的特性看,可以作為數(shù)據(jù)的存儲(chǔ)模式和數(shù)據(jù)的傳輸載體。

行為模式

這種模式是指構(gòu)成一個(gè)對(duì)象的主體是一系列方法的定義,而并不含有具體的屬性定義,或者說即使含有一些屬性定義,也是一些無狀態(tài)的協(xié)作對(duì)象。運(yùn)行在這種模式之下的對(duì)象,我們往往稱之為“無狀態(tài)對(duì)象”,其中最為常見的例子是我們熟悉的Servlet對(duì)象。

我們發(fā)現(xiàn),對(duì)象的運(yùn)行模式的劃分是根據(jù)對(duì)象的構(gòu)成特點(diǎn)進(jìn)行的。這三種對(duì)象的運(yùn)行模式在我們?nèi)粘>幊讨卸家呀?jīng)見過并且親自實(shí)踐過。接下來的章節(jié),我們將針對(duì)后兩種構(gòu)成模式做進(jìn)一步的分析。

2.1.1.2 屬性對(duì)象模式

屬性對(duì)象模式又稱為JavaBean模式。這種對(duì)象的運(yùn)行模式我們?cè)谌粘>幊讨幸姷梅浅6?。作為?shù)據(jù)存儲(chǔ)和數(shù)據(jù)傳輸?shù)妮d體,運(yùn)行在JavaBean模式下的對(duì)象,在眾多的編程層次都會(huì)被用到,并且根據(jù)作用不同被冠以各種不同的名稱,如

PO(Persistent Object)—持久化對(duì)象

BO(Business Object)—業(yè)務(wù)對(duì)象

VO(Value Object)—值對(duì)象

DTO(Data Transfer Object)—數(shù)據(jù)傳輸對(duì)象

FormBean—頁(yè)面對(duì)象

對(duì)于這些紛繁復(fù)雜的縮寫和對(duì)象類別,許多初學(xué)者會(huì)感到非常頭疼。它們從形式上看是一系列難記的縮寫,不過真正讓程序員頭疼的,不僅在于它們被用于不同的業(yè)務(wù)場(chǎng)景和編程層次,還在于它們?cè)谀承r(shí)候甚至只是同一個(gè)對(duì)象在不同層次上的不同名稱。

不過我們大可不必在對(duì)象的名稱和叫法上過分糾結(jié)。因?yàn)閷?duì)于程序員來說,無論這些對(duì)象被冠以什么花里胡哨的名稱,它們只不過是對(duì)基本的、運(yùn)行在JavaBean模式下對(duì)象的有效擴(kuò)展或增強(qiáng)。

以PO(Persistent Object)為例,當(dāng)我們使用Hibernate作為O/R Mapping的工具時(shí),一個(gè)典型的PO會(huì)被定義成如代碼清單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方法
          }

假設(shè)去除那些Annotation,我們會(huì)發(fā)現(xiàn)這個(gè)PO和一個(gè)普通的JavaBean并無不同,至少我們無法在形式上將它們區(qū)分開。因此,我們說Annotation在這里的所用是豐富了一個(gè)普通JavaBean的語義,從而使之成為一個(gè)持久化對(duì)象。而當(dāng)我們使用O/R Mapping的工具Hibernate進(jìn)行處理時(shí),也是根據(jù)這些Annotation才能夠?qū)@些PO進(jìn)行識(shí)別并賦予其相應(yīng)功能的。也就是說,JavaBean自身的特性并沒有發(fā)生改變,只是引入了一些額外的編程元素從而對(duì)JavaBean進(jìn)行了增強(qiáng)。

當(dāng)一個(gè)對(duì)象運(yùn)作在屬性對(duì)象模式時(shí),其本質(zhì)是對(duì)象的JavaBean特性。我們可以從其表現(xiàn)形式和運(yùn)行特征中得出下面這樣的一個(gè)結(jié)論。

結(jié)論 JavaBean對(duì)象的產(chǎn)生主要是為了強(qiáng)調(diào)對(duì)象的內(nèi)在特性和狀態(tài),同時(shí)構(gòu)造一個(gè)數(shù)據(jù)存儲(chǔ)和數(shù)據(jù)傳輸?shù)妮d體。

我們?cè)诒竟?jié)開篇所提到的各種不同的對(duì)象名稱的定義,它們之間的界定實(shí)際上是非常模糊的。同樣一個(gè)對(duì)象,往往可以兼任多種不同的角色,在不同的編程層次表現(xiàn)為不同的對(duì)象實(shí)體,其最終目的是在特定的場(chǎng)合表現(xiàn)出其作用。

在上面的結(jié)論中,我們還讀到另外一層意思,那就是JavaBean對(duì)象的一個(gè)重要的性質(zhì)在于它是一個(gè)數(shù)據(jù)存儲(chǔ)和數(shù)據(jù)傳輸?shù)妮d體。有關(guān)這一點(diǎn),我們將在之后的章節(jié)進(jìn)行分析。

2.1.1.3 行為對(duì)象模式

根據(jù)之前有關(guān)對(duì)象構(gòu)成的分析,運(yùn)行在行為對(duì)象模式之下的對(duì)象,我們往往稱之為無狀態(tài)對(duì)象。在上一節(jié)中,我們提到對(duì)象的內(nèi)在特性和狀態(tài)是由構(gòu)成對(duì)象的屬性來表達(dá)的。所以,所謂的無狀態(tài)對(duì)象實(shí)際上就是指對(duì)象的方法所表達(dá)的行為特性并不依賴于對(duì)象的內(nèi)部屬性的狀態(tài)。而這種無狀態(tài)的特性,非常適合進(jìn)行一個(gè)請(qǐng)求的響應(yīng),并在方法體的內(nèi)部實(shí)現(xiàn)中進(jìn)行復(fù)雜的業(yè)務(wù)邏輯處理。

我們?cè)谶@里可以針對(duì)對(duì)象的行為方法的語法做進(jìn)一步的分析,如圖2-2所示。

圖2-2 對(duì)象的行為方法的語法分析

從圖2-2中,我們看到對(duì)象方法的語法定義同樣體現(xiàn)出一定的規(guī)律性:

方法簽名(Signature)—行為動(dòng)作的邏輯語義概括

參數(shù)(Parameter)—行為動(dòng)作的邏輯請(qǐng)求輸入

返回值(Return)—行為動(dòng)作的處理響應(yīng)結(jié)果輸出

我們可以看到,方法的定義實(shí)際上是一種觸發(fā)式的邏輯定義。當(dāng)需要完成某種行為動(dòng)作(業(yè)務(wù)邏輯)時(shí),我們會(huì)將請(qǐng)求作為參數(shù)輸入到方法的參數(shù)列表中,而返回值則自然而然地成為業(yè)務(wù)邏輯的處理結(jié)果。由此,我們可以得出以下結(jié)論:

結(jié)論 對(duì)象中的方法定義是進(jìn)行請(qǐng)求響應(yīng)的天然載體。

這個(gè)結(jié)論我們將在之后對(duì)各種Web框架對(duì)Http請(qǐng)求的響應(yīng)設(shè)計(jì)中再次提及。因?yàn)樵贘ava標(biāo)準(zhǔn)中,對(duì)Http請(qǐng)求的響應(yīng)是通過Servlet標(biāo)準(zhǔn)來實(shí)現(xiàn)的。而我們知道,Servlet對(duì)象就是一個(gè)非常典型的運(yùn)行在行為對(duì)象模式之上的無狀態(tài)對(duì)象。

上述結(jié)論對(duì)讀者理解“請(qǐng)求-響應(yīng)”的過程在編程語言中的邏輯表達(dá)有很大的幫助。因而讀者應(yīng)仔細(xì)體會(huì)這些構(gòu)成要素在“請(qǐng)求-響應(yīng)”過程中所起的作用,從而對(duì)Web框架的設(shè)計(jì)有更深的感悟。

2.1.2 對(duì)象關(guān)系模型

對(duì)象的構(gòu)成模型是從對(duì)象內(nèi)部結(jié)構(gòu)的角度對(duì)面向?qū)ο缶幊讨械幕驹剡M(jìn)行的分析。在本節(jié)中,我們分析的角度將由“內(nèi)”轉(zhuǎn)向“外”,考慮對(duì)象與對(duì)象之間的關(guān)系。

談到對(duì)象之間的關(guān)系,我們很容易想到兩個(gè)不同的層次:

從屬關(guān)系—一個(gè)對(duì)象在邏輯語義上隸屬于另外一個(gè)對(duì)象

協(xié)作關(guān)系—對(duì)象之間通過協(xié)作來共同表達(dá)一個(gè)邏輯語義

這兩種關(guān)系在面向?qū)ο缶幊陶Z言中分別擁有不同的表現(xiàn)形式和邏輯意義,這二者構(gòu)成了絕大多數(shù)的對(duì)象關(guān)系模型。接下來,我們就來分別分析這兩種對(duì)象關(guān)系模型。

2.1.2.1 對(duì)象的從屬關(guān)系

對(duì)象的從屬關(guān)系,主要指一個(gè)對(duì)象在邏輯語義上隸屬于另外一個(gè)對(duì)象。這個(gè)定義實(shí)際上依然非常抽象。要理解這一定義,我們就必須從“隸屬”這個(gè)核心詞匯入手。邏輯語義上的“隸屬”,主要有兩種不同的含義。

歸屬

歸屬的邏輯含義很直觀。比如說,一個(gè)人總是歸屬于一個(gè)國(guó)家;一本書總是有作者。因而,當(dāng)我們把人和國(guó)家、書和作者都映射成面向?qū)ο缶幊陶Z言中所定義的一個(gè)個(gè)對(duì)象時(shí),它們之間自然而然就形成了歸屬關(guān)系。這種歸屬關(guān)系是由外部世界的邏輯關(guān)系映射到編程元素之上而帶來的。

繼承

繼承的邏輯含義就有點(diǎn)晦澀。比如說,馬、白馬和千里馬之間的關(guān)系。首先,白馬和千里馬都是馬的一種,然而白馬和千里馬卻各自擁有自己獨(dú)有的特性:白馬是白色的、千里馬一日可行千里。此時(shí),我們可以說白馬和千里馬都屬于馬,它們都繼承了馬的基本特征,卻又各自擴(kuò)展了自身獨(dú)有的特質(zhì)。

明確了“隸屬”的兩層含義,我們就需要把它們和面向?qū)ο缶幊陶Z言聯(lián)系在一起。歸屬和繼承,它們?cè)诿嫦驅(qū)ο蟮木幊陶Z言中又以怎樣的形式表現(xiàn)出來呢?

我們先來看看“歸屬”這層含義。對(duì)于“歸屬”關(guān)系的編程語言表現(xiàn)形式,我們可以得出下面的結(jié)論:

結(jié)論 “歸屬”關(guān)系在面向?qū)ο缶幊陶Z言中,主要以對(duì)象之間互相引用的形式存在。

我們以書和作者之間的對(duì)象定義作為例子來說明,其相關(guān)源碼如代碼清單2-2所示。

代碼清單2-2 Book.java

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

我們?cè)谶@里所表達(dá)的是書和作者之間的“歸屬”關(guān)系。從代碼中,可以很明顯看到,一本書可能有多個(gè)作者,所以我們?cè)跁˙ook)的屬性定義中加入了一個(gè)List的容器結(jié)構(gòu),List中的對(duì)象類型是作者(Author)。這樣一來,書和作者之間就形成了一個(gè)引用關(guān)系。

使用對(duì)象之間的引用來表達(dá)“歸屬”關(guān)系,是一種非常廉價(jià)的做法。因?yàn)檫@種關(guān)系的表達(dá)來源于對(duì)象定義的基本模式,不會(huì)對(duì)對(duì)象自身產(chǎn)生破壞性影響。

細(xì)心的讀者還會(huì)發(fā)現(xiàn),我們這里所說的“歸屬”關(guān)系,實(shí)際上還蘊(yùn)含了一層“數(shù)量”的對(duì)應(yīng)關(guān)系。在上面的例子中,我們發(fā)現(xiàn)書和作者的數(shù)量關(guān)系是“一對(duì)多”。除了“一對(duì)多”以外,對(duì)象之間的歸屬關(guān)系在數(shù)量這個(gè)維度上還可以是“一對(duì)一”和“多對(duì)多”。有趣的是,這三種歸屬關(guān)系正好也和我們關(guān)系型數(shù)據(jù)庫(kù)模型中所定義的基本關(guān)系一一對(duì)應(yīng)。這種關(guān)系模型也就為我們?cè)贘ava世界和數(shù)據(jù)庫(kù)世界之間進(jìn)行O/R Mapping打下了理論基礎(chǔ)。

看完了“歸屬”關(guān)系,我們?cè)賮砜纯础袄^承”關(guān)系。有關(guān)“繼承”關(guān)系的編程形式表述,我們可以用下述結(jié)論來進(jìn)行說明:

結(jié)論 “繼承”關(guān)系在面向?qū)ο缶幊陶Z言中,主要以原生語法的形式獲得支持。

什么是“以原生語法的形式獲得支持”呢?我們來看看之前說的那個(gè)白馬的例子,其相關(guān)源碼如代碼清單2-3所示。

代碼清單2-3 Horse.java

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

白馬和馬之間,我們使用了Java中的關(guān)鍵字extends來表達(dá)前者繼承自后者。這種方式與我們之前所看到的對(duì)象之間的引用模式完全不同,它使用了編程語言中的原生語法支持。

這種對(duì)象關(guān)系的表達(dá)方式非常簡(jiǎn)單而有效,不過當(dāng)我們引入一個(gè)語法,就不得不遵循這個(gè)語法所規(guī)定的編程規(guī)范所帶來的編程限制。例如在Java中,我們就必須遵循單根繼承的規(guī)范。這一點(diǎn)也是“繼承”這種方式經(jīng)常被詬病的原因之一。

在Java中,除了extends關(guān)鍵字以外,還有implements關(guān)鍵字來表達(dá)一個(gè)實(shí)現(xiàn)類與接口類之間的關(guān)系。實(shí)際上這是一種特殊的“繼承”關(guān)系,它無論從語義上還是表現(xiàn)形式上,與extends都基本相同。

2.1.2.2 對(duì)象的協(xié)作關(guān)系

對(duì)象的從屬關(guān)系從現(xiàn)實(shí)世界邏輯語義的角度描述了對(duì)象與對(duì)象之間的關(guān)系。從之前的分析中,我們可以發(fā)現(xiàn)無論是“歸屬”關(guān)系還是“繼承”關(guān)系,它們都在圍繞著對(duì)象構(gòu)成要素中的屬性做文章。那么讀者不禁要問,圍繞著對(duì)象的行為動(dòng)作特征,對(duì)象之間是否能夠建立起關(guān)系模型呢?

從哲學(xué)的觀點(diǎn)來看,萬事萬物都存在著普遍而必然的聯(lián)系。從對(duì)象的行為特性上分析,一個(gè)對(duì)象的行為特征總是能夠與另外一個(gè)對(duì)象的行為特征形成依賴關(guān)系。而這種依賴關(guān)系,在極大程度上影響著對(duì)象的邏輯行為模式。例如,一個(gè)人“行走”這樣一個(gè)動(dòng)作,需要手腳的共同配合才能完成,具體來說就是“擺手”和“抬腳”。而當(dāng)我們把手和腳分別看作一個(gè)對(duì)象時(shí),“擺”和“抬”就成為手和腳的行為動(dòng)作了。

這樣一說,似乎對(duì)象之間的協(xié)作關(guān)系就非常容易理解了,請(qǐng)看以下結(jié)論:

結(jié)論 當(dāng)對(duì)象的行為動(dòng)作需要其他對(duì)象的行為動(dòng)作進(jìn)行配合時(shí),對(duì)象之間就形成了協(xié)作關(guān)系。

可以想象,一個(gè)對(duì)象在絕大多數(shù)情況下都不是孤立存在的,它總是需要通過與其他對(duì)象的協(xié)作來完成其自身的業(yè)務(wù)邏輯。這是軟件大師Martin Fowler曾經(jīng)提過的一個(gè)重要觀點(diǎn)。然而這卻為我們的編程帶來了一些潛在的問題:如何來管理對(duì)象和協(xié)作對(duì)象之間的關(guān)系呢?有關(guān)這一問題,我們將在第5章詳細(xì)進(jìn)行講解。

對(duì)象的協(xié)作關(guān)系在對(duì)象運(yùn)行在行為對(duì)象模式時(shí)顯得尤為突出。因?yàn)楫?dāng)使用一個(gè)具體的方法來進(jìn)行動(dòng)作響應(yīng)時(shí),我們總是會(huì)借助一些輔助對(duì)象的操作來幫助我們共同完成動(dòng)作的具體邏輯。也就是說,我們會(huì)將一個(gè)動(dòng)作從業(yè)務(wù)上進(jìn)行邏輯劃分,將不同的業(yè)務(wù)分派到不同的對(duì)象之上去執(zhí)行。這就成為我們所熟知的分層開發(fā)模式的理論依據(jù)。

2.1.3 面向?qū)ο缶幊痰幕居^點(diǎn)

在了解了對(duì)象的構(gòu)成模型和對(duì)象的關(guān)系模型之后,讀者不免要問,這些內(nèi)容和我們的日常編程有關(guān)系嗎?答案是有關(guān)系!而且不僅是有關(guān)系,還是有相當(dāng)大的關(guān)系!在本節(jié)中,我們就以結(jié)論輔之以分析的方法,為讀者展示面向?qū)ο缶幊讨械囊恍┗居^點(diǎn)。

結(jié)論 每一種對(duì)象的構(gòu)成模型,都有其特定的應(yīng)用范圍。

根據(jù)之前我們有關(guān)對(duì)象的構(gòu)成模型的分析,可以發(fā)現(xiàn)三種對(duì)象的構(gòu)成模型在日常的編程過程中都曾經(jīng)碰到過。因此,我們應(yīng)該首先明確的觀點(diǎn)是每一種對(duì)象的構(gòu)成模型都有其存在的合理性,并沒有任何一種模型是錯(cuò)誤的模型這一說。

既然如此,我們所要做的就是認(rèn)清這些對(duì)象構(gòu)成模式的特性,并且能夠在最恰當(dāng)?shù)臉I(yè)務(wù)場(chǎng)景中選擇最合適的模型加以應(yīng)用。那么,從面向?qū)ο笏枷氲慕嵌龋绻覀儗⑦@些對(duì)象運(yùn)作模式做一個(gè)縱向的比較,它們有沒有優(yōu)劣之分呢?

結(jié)論 將對(duì)象運(yùn)作在“屬性-行為”模式上,最符合面向?qū)ο缶幊趟枷氲谋疽狻?/span>

這一結(jié)論承接了上一個(gè)結(jié)論,可以說是對(duì)象建模方式的一種合理的理解和擴(kuò)展,也回答了我們剛才的問題。當(dāng)我們進(jìn)行對(duì)象建模的時(shí)候,總是首先需要根據(jù)業(yè)務(wù)情況選擇一個(gè)對(duì)象建模設(shè)計(jì)的角度,而這個(gè)角度往往取決于對(duì)象在整個(gè)程序中所起的作用。例如,當(dāng)我們需要進(jìn)行數(shù)據(jù)傳輸或者數(shù)據(jù)映射時(shí),我們應(yīng)該基于對(duì)象的“屬性模式”來進(jìn)行對(duì)象建模;當(dāng)我們需要進(jìn)行動(dòng)作響應(yīng)時(shí),我們應(yīng)該基于對(duì)象的“行為模式”來進(jìn)行對(duì)象建模。

然而,運(yùn)行在“屬性模式”中的對(duì)象并不是說完全就不能具備行為動(dòng)作。基于某一種模式進(jìn)行建模,只是我們考慮對(duì)象設(shè)計(jì)的角度不同。如果我們站在一個(gè)“對(duì)象構(gòu)成的完整性”這樣一個(gè)高度來看待每一個(gè)對(duì)象,它們總是由屬性和方法共同構(gòu)成。因此,在任何一種對(duì)象的構(gòu)成模式上走極端都是不符合面向?qū)ο缶幊趟枷氡疽獾摹?/p>

軟件大師Martin Fowler就曾經(jīng)撰文指出,在對(duì)象建模時(shí)不應(yīng)極端地將對(duì)象設(shè)計(jì)成單一的“屬性模式”。讀者可以參考: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.

有關(guān)這一點(diǎn),也引起了許多國(guó)內(nèi)軟件開發(fā)人員的深入討論,并且引申出許多極具特色的名詞,諸如:“貧血模型”、“失血模型”、“充血模型”、“脹血模型”等等。這些討論非常有價(jià)值,對(duì)于對(duì)象建模有興趣的讀者可以使用搜索引擎就相關(guān)的討論進(jìn)行搜索。

既然存在著那么多有關(guān)“領(lǐng)域模型”的理解方式,為什么Martin Fowler這樣的軟件大師還是推薦盡可能使對(duì)象運(yùn)行在“屬性-行為”模式之上呢?除了它自身在構(gòu)成形式上比較完整,能夠比其他兩種運(yùn)行方式更容易表達(dá)對(duì)象的邏輯語義之外,還有什么別的特殊考慮嗎?筆者通過思考和分析,給出可能的兩個(gè)理由:

當(dāng)對(duì)象運(yùn)作在“屬性--行為”模式上時(shí),我們能夠最大程度地應(yīng)用各種設(shè)計(jì)模式

對(duì)于設(shè)計(jì)模式有深入研究的讀者,應(yīng)該會(huì)同意這個(gè)觀點(diǎn)。設(shè)計(jì)模式的存在基礎(chǔ)是對(duì)象,因而設(shè)計(jì)模式自身的分類也圍繞著對(duì)象展開。我們可以發(fā)現(xiàn),絕大多數(shù)的設(shè)計(jì)模式需要通過類、接口、屬性、方法這些語法元素的共同配合才能完成。因而,單一的屬性模式和行為模式的對(duì)象,在設(shè)計(jì)模式的應(yīng)用上很難施展拳腳。

當(dāng)對(duì)象運(yùn)作在“屬性--行為”模式上時(shí),我們能夠最大程度地發(fā)揮對(duì)象之間的協(xié)作能力

仔細(xì)分析對(duì)象的關(guān)系模型,我們會(huì)發(fā)現(xiàn)無論是對(duì)象的從屬關(guān)系還是對(duì)象的協(xié)作關(guān)系,它們?cè)诮^大多數(shù)情況下是通過對(duì)象之間的屬性引用來完成的。這種屬性引用的方式,只是在形式上解決了對(duì)象和對(duì)象之間進(jìn)行關(guān)聯(lián)的問題。而真正談到對(duì)象之間的配合,則不可避免地需要通過行為動(dòng)作的邏輯調(diào)用來完成,這也是對(duì)象協(xié)作的本質(zhì)內(nèi)容。

對(duì)象建模是一個(gè)很深刻的哲學(xué)問題,它將直接影響我們的編程模式。所以對(duì)于建模這個(gè)問題,大家應(yīng)該綜合各家之言,并形成自己的觀點(diǎn)。筆者在這里的觀點(diǎn)是:對(duì)象建模方式首先是一個(gè)哲學(xué)問題,取決于設(shè)計(jì)者本身對(duì)于業(yè)務(wù)和技術(shù)的綜合考慮。任何一種建模方式都不是絕對(duì)正確或者絕對(duì)錯(cuò)誤的方式。我們所期待看到的對(duì)象建模的結(jié)果是提高程序的“可讀性”、“可維護(hù)性”和“可擴(kuò)展性”。一切建模方式都應(yīng)該首先服務(wù)于這一基本的程序開發(fā)的最佳實(shí)踐。

結(jié)論 建立對(duì)象之間的關(guān)系模型是面向?qū)ο缶幊痰暮诵膬?nèi)容。

對(duì)象建模是一個(gè)很復(fù)雜的邏輯抽象過程。事實(shí)上,對(duì)象建模最難的地方并不在于設(shè)計(jì)某一個(gè)單體對(duì)象的屬性構(gòu)成或者方法構(gòu)成。因?yàn)橹拔覀円蔡岬?,?duì)象總不能以單體的形式孤立存在。對(duì)象與對(duì)象之間總是以某種方式相互關(guān)聯(lián)、相互配合。這種關(guān)聯(lián)要么形成對(duì)象之間的從屬關(guān)系,要么通過對(duì)象的行為方法進(jìn)行互相協(xié)作。

由此可見,我們?cè)谶M(jìn)行對(duì)象建模的時(shí)候,必須優(yōu)先考慮的就是對(duì)象與對(duì)象之間的關(guān)系模型,關(guān)系模型決定我們進(jìn)行對(duì)象關(guān)聯(lián)的具體形式,選擇合適的編程語言語法進(jìn)行關(guān)聯(lián)關(guān)系的表達(dá)。

將對(duì)象之間的協(xié)作和關(guān)聯(lián)關(guān)系作為設(shè)計(jì)對(duì)象的最重要的考慮因素,可以時(shí)刻提醒我們不要將過多的邏輯放在一個(gè)對(duì)象之中。因?yàn)楫?dāng)考慮到對(duì)象之間的協(xié)作和關(guān)聯(lián)關(guān)系,我們就可以充分挖掘每一個(gè)對(duì)象的職責(zé)和語義,從而避免一個(gè)對(duì)象過于復(fù)雜而變得不可維護(hù)。

2.2 框架的本質(zhì)

什么是框架?框架從何而來?為什么要使用框架?這是一系列簡(jiǎn)單而又復(fù)雜的問題。簡(jiǎn)單,是因?yàn)樗鼈儽旧硭坪醪粦?yīng)該成為問題??蚣軐?shí)實(shí)在在存在,并且在開發(fā)中發(fā)揮著重要的作用,我們的日常工作,遵循著框架所規(guī)定的編程模式,在其指導(dǎo)之下,我們能夠編寫更為強(qiáng)大的程序。說其復(fù)雜,是因?yàn)榭蚣鼙旧碛质侨绱思姺睆?fù)雜,我們?cè)谑褂每蚣艿耐瑫r(shí),往往會(huì)迷失其中。

任何事物都有蘊(yùn)含在其內(nèi)部的本質(zhì)。無論框架本身有多復(fù)雜,我們所需要探尋的,都是其最為內(nèi)在的東西??蚣転槭裁磿?huì)產(chǎn)生?我們來看一個(gè)最最簡(jiǎn)單的例子。

在Java中,如果要判定一個(gè)輸入是否為null或空字符串,我們會(huì)使用下面的代碼:

          if(str == null || str.length() == 0) {
              // 在這里添加你的邏輯
          }

這段代碼非常普通,簡(jiǎn)單學(xué)習(xí)過Java語法的程序員都能夠讀懂并編寫。那么這段代碼是如何運(yùn)作的呢?我們所編寫的Java程序,首先獲得的是來自于Java的基本支持:語法支持與基本功能的API級(jí)別的支持(str.length()方法實(shí)際上就是JDK所提供的字符串的基本API)。換句話說,我們編寫的所有程序,都依賴于一個(gè)最最基本的前提條件:JDK所提供的API支持。

當(dāng)一個(gè)需求被重復(fù)1000次,那么我們就需要重復(fù)1000次針對(duì)需求的解決辦法,這是一個(gè)顯而易見的道理。然而當(dāng)上面的代碼片段散落在我們的程序中1000次,我們不免會(huì)思考,是不是有什么簡(jiǎn)單有效的途徑可以把事情做得更加漂亮一些呢?我們可以針對(duì)代碼片段做一次簡(jiǎn)單的邏輯抽取重構(gòu),如代碼清單2-4所示。

代碼清單2-4 StringUtils.java

          // 定義一個(gè)類和一個(gè)靜態(tài)工具方法來抽象出將被重復(fù)調(diào)用的邏輯
          public abstract class StringUtils {
              // 封裝了一個(gè)靜態(tài)方法
              public static boolean isEmpty(String str) {
                  return str == null || str.length() == 0;
              }
          }
          // 引用靜態(tài)方法取代之前的代碼片段
          if(StringUtils.isEmpty(string)) {
              // 在這里添加你的邏輯
          }

在上面的代碼段中,我們定義了一個(gè)靜態(tài)方法,將之前寫的那段邏輯封裝起來。這一層小小的封裝雖然看上去是一個(gè)“換湯不換藥”的做法,但是從深遠(yuǎn)意義上來說,我們至少可以從以下兩個(gè)方面獲得好處:

可讀性

靜態(tài)方法的簽名從一個(gè)角度向我們揭示了一段邏輯的實(shí)際意義。比如在這個(gè)例子中,isEmpty表示“判定某個(gè)輸入是否為空”。與之前的代碼片段相比,如果我們?cè)谝粋€(gè)1000行的程序代碼片段中觀察這2種不同的代碼形式,那么前者往往會(huì)被你無視,它完全無法引起你的思維停頓,而后者卻能夠顯而易見地在邏輯上給你足夠且直觀的提示。

可擴(kuò)展性

如果我們對(duì)上述需求稍作改動(dòng),程序同時(shí)需要對(duì)輸入為空格的字符串做出同樣的判定。我們同樣將上述的需求應(yīng)用1000次,那么前者將導(dǎo)致我們?cè)谡麄€(gè)應(yīng)用中進(jìn)行搜索并替換修改1000次,而后者只需要針對(duì)我們封裝的邏輯修改1次即可。

從上面的例子我們可以看出,雖然僅僅對(duì)代碼做了一次簡(jiǎn)單的重構(gòu),卻在上述的兩個(gè)方面為我們解決了潛在的問題。這一現(xiàn)象或許直到現(xiàn)在你才意識(shí)到,但很多程序員前輩在很早以前就意識(shí)到了。因而,早就有人為此編寫了類似的代碼。比如說,類似的方法就存在于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;
              }
          }

當(dāng)我們將Apache的commons-lang的JAR包加到CLASSPATH中時(shí),就能在程序的任何地方“免費(fèi)地”使用上述方法。也就是說,我們自己無須自行編寫代碼對(duì)JDK進(jìn)行擴(kuò)展,因?yàn)锳pache的commons-lang已經(jīng)為我們做了。既然如此,我們唯一所需要做的,只是把別人做的東西加到CLASSPATH中并且使用它而已。

這是一個(gè)很熟悉的過程,不是嗎?我們?cè)诖罱ǔ绦蜻\(yùn)行的基本環(huán)境時(shí),指定程序所依賴的JAR文件是其中的一個(gè)重要步驟。而這一步驟,實(shí)際上包含了Java開發(fā)中最最基本而淺顯的道理:

結(jié)論 當(dāng)我們加載一個(gè)JAR包到CLASSPATH時(shí),實(shí)際上是獲得了JAR中所有對(duì)JDK的額外支持。

我們的程序就像一個(gè)金字塔形狀。位于最底部的當(dāng)然是JVM,提供運(yùn)行Java程序的基礎(chǔ)環(huán)境,包括對(duì)整個(gè)Java程序的編譯運(yùn)行。在這個(gè)之上的是JDK,JDK是構(gòu)建在JVM之上的基本的對(duì)象行為的定義(我們?cè)诖罱ㄩ_發(fā)環(huán)境時(shí)所安裝的JDK就是這個(gè))。而再往上,是一個(gè)具備層次結(jié)構(gòu)的JAR層,所有被加載到CLASSPATH中的JAR文件都搭建在JDK層次之上,它們之間可能形成互相依賴,但不管怎么說,它們的作用都是提供JDK以外的功能支持。最后,在金字塔尖的,才是我們?nèi)粘>帉懙膽?yīng)用程序,它將依賴于金字塔低端的所有程序。這樣一個(gè)結(jié)構(gòu)如圖2-3所示。

圖2-3 Java應(yīng)用的金字塔結(jié)構(gòu)

仔細(xì)觀察一下處于中間的JAR層,這個(gè)層次的組成結(jié)構(gòu)與其他的層次不同。它是由一塊塊磚頭堆砌而成,上層的磚塊搭建在下層的磚塊之上。如果我們把其中的每一塊磚都比作一個(gè)JAR文件,它們之間也就形成了明顯的具備層次的依賴關(guān)系。

這個(gè)層次中的任何JAR文件本身可能并不為最終的程序提供具體的功能實(shí)現(xiàn),但它卻為我們編寫程序提供了必要的支持。如果查看一個(gè)標(biāo)準(zhǔn)的J2EE程序運(yùn)行時(shí)所依賴的CLASSPATH中的JAR包,會(huì)發(fā)現(xiàn)我們所熟悉的那些“框架”,實(shí)際上都蘊(yùn)涵其中。我們?cè)谶@里給出一個(gè)最簡(jiǎn)單的示例程序在Eclipse中的CLASSPATH截圖,如圖2-4所示。

圖2-4 Eclipse中的CLASSPATH示例

從圖中我們看到,JRE System Library是整個(gè)應(yīng)用程序最基本的運(yùn)行環(huán)境。而無論是Struts2還是Spring,它們都以JAR文件的形式被加載到程序運(yùn)行所依賴的CLASSPATH中,并為我們的應(yīng)用程序使用。如果我們用更加通俗的話來表述這一現(xiàn)象,則是:

結(jié)論 框架只是一個(gè)JAR包而已,其本質(zhì)是對(duì)JDK的功能擴(kuò)展。

當(dāng)我們說一個(gè)程序使用了Spring框架,隱藏在背后的潛臺(tái)詞實(shí)際上是說,我們把Spring的分發(fā)包加入到CLASSPATH,并且在程序中使用了其功能??蚣埽鋵?shí)就是這么回事!就是如此簡(jiǎn)單!

到現(xiàn)在為止,框架似乎還沒有任何在我們的知識(shí)范疇以外的東西,它們的本質(zhì)是如此一致,以至于我們很容易遺忘把一個(gè)JAR文件加入到CLASSPATH中的初衷:解決在某個(gè)領(lǐng)域的開發(fā)中所碰到的困境。正如我們?cè)谝婚_始使用的那個(gè)例子一樣,框架作為一個(gè)JAR包,實(shí)際上是許許多多解決各種問題的類和方法的集合。當(dāng)然,更多時(shí)候,它們包含了編寫這些JAR包的作者所創(chuàng)造的許多最佳實(shí)踐。

結(jié)論 框架是一組程序的集合,包含了一系列的最佳實(shí)踐,作用是解決某個(gè)領(lǐng)域的問題。

只有解決問題才是所有框架的共同目標(biāo)。框架的產(chǎn)生就是為了解決一個(gè)又一個(gè)在開發(fā)中所遇到的困境。不同的框架,只是為了解決不同領(lǐng)域的問題。所以,對(duì)于廣大程序員來說,千萬不要為了學(xué)習(xí)框架而學(xué)習(xí)框架,而是要為了解決問題而學(xué)習(xí)框架,這才是一個(gè)程序員的正確學(xué)習(xí)之道。

2.3 最佳實(shí)踐

一切程序的編寫,都需要遵循特定的規(guī)范。這里所說的規(guī)范,往往是建立在運(yùn)行環(huán)境之上的一系列概念和實(shí)現(xiàn)方法的基本定義,并被歸納為一個(gè)完整的體系。例如,我們使用Java來進(jìn)行Web開發(fā),所需要遵循的最基本的規(guī)范就是我們所熟悉的Servlet標(biāo)準(zhǔn)、JSP標(biāo)準(zhǔn),等等。

建立在標(biāo)準(zhǔn)和規(guī)范之上的,是各種針對(duì)這些標(biāo)準(zhǔn)和規(guī)范的實(shí)現(xiàn)。這些實(shí)現(xiàn)構(gòu)成了程序運(yùn)行的基本環(huán)境。例如,Tomcat有對(duì)Servlet標(biāo)準(zhǔn)的實(shí)現(xiàn)方式,而Websphere則有不同的實(shí)現(xiàn)方式。然而它們?cè)诒举|(zhì)上都實(shí)現(xiàn)了Servlet標(biāo)準(zhǔn)所規(guī)定的接口,從而讓我們的應(yīng)用程序可以透明地使用這些API,而無須關(guān)心真正的Web容器內(nèi)部的實(shí)現(xiàn)機(jī)理。

我們所編寫的程序,總是建立在一系列的規(guī)范和基本運(yùn)行環(huán)境之上。面對(duì)紛繁復(fù)雜的業(yè)務(wù)需求,不同的程序員可以按照自己的意愿來編寫程序,因此,即使為了表達(dá)相同的業(yè)務(wù)功能,不同的程序代碼之間的差異性也是很大的。程序的差異性有時(shí)候會(huì)給人以創(chuàng)新的靈感,但是更多的時(shí)候會(huì)造成麻煩。因?yàn)椴町愋栽酱螅S護(hù)起來就越麻煩。出于對(duì)可維護(hù)性和可讀性的要求,我們所希望的程序最好能從宏觀層面上看上去是一致的,使得每一個(gè)程序員都能夠讀懂并合理運(yùn)用,這才是我們的目標(biāo)。這一目標(biāo),我們習(xí)慣上稱之為最佳實(shí)踐。

結(jié)論 最佳實(shí)踐(Best Practice),實(shí)際上是無數(shù)程序員在經(jīng)過了無數(shù)次的嘗試后,總結(jié)出來的處理特定問題的特定方法。如果我們把每個(gè)程序員的自由發(fā)揮看作是一條通往成功的路徑,最佳實(shí)踐就是其中的最短路徑,它能夠極大地解放生產(chǎn)力。

所有這些最佳實(shí)踐,最終又以一個(gè)個(gè)JAR包的形式蘊(yùn)含在框架之中,對(duì)我們的應(yīng)用程序提供必要的支持,因此我們有必要在這里探尋一些最最基本的最佳實(shí)踐,從更深的層次了解框架存在的意義和框架的設(shè)計(jì)初衷。在之后的章節(jié)中,我們會(huì)反復(fù)提及這些最佳實(shí)踐,因?yàn)樗鼈儾粌H能夠指導(dǎo)我們進(jìn)行程序開發(fā),它們本身也蘊(yùn)含在Struts2的內(nèi)部。

最佳實(shí)踐 永遠(yuǎn)不要生搬硬套任何最佳實(shí)踐,真理之鎖永遠(yuǎn)只為最合適的那把鑰匙開啟。

這是一條凌駕于任何最佳實(shí)踐之上的最佳實(shí)踐。在使用框架編寫程序時(shí),程序員最容易犯的毛病就是對(duì)某項(xiàng)技術(shù)或者某個(gè)框架絕對(duì)迷信,并將它生搬硬套于任何程序開發(fā)之中。應(yīng)用程序永遠(yuǎn)服務(wù)于具體的業(yè)務(wù)場(chǎng)景,對(duì)于不同的業(yè)務(wù)需求,我們的解決方案也會(huì)有所區(qū)別,自然也會(huì)涉及不同的框架選擇。

在實(shí)際開發(fā)中,我們遇到的許多編程開發(fā)的問題都是沒有固定答案的哲學(xué)取向問題。所以,往往沒有“最好”的答案,只有“最合適”的答案。這是在面對(duì)多種解決方案進(jìn)行取舍時(shí)的一個(gè)基本準(zhǔn)繩。

最佳實(shí)踐 始終保證程序的可讀性、可維護(hù)性和可擴(kuò)展性。

可讀性、可維護(hù)性和可擴(kuò)展性,就像三腳架的三個(gè)支撐腳,缺一不可。任何對(duì)程序的重構(gòu),實(shí)際上都圍繞著這三個(gè)基本原則進(jìn)行,而它們也同時(shí)成為衡量程序?qū)懙煤脡牡淖罨緲?biāo)準(zhǔn)。代碼的不斷重構(gòu)、框架的產(chǎn)生實(shí)際上都來自于這三個(gè)程序內(nèi)在屬性的驅(qū)動(dòng)。

我們之前已經(jīng)反復(fù)提到了程序的可維護(hù)性和可擴(kuò)展性。事實(shí)上,程序的可讀性也是程序所應(yīng)具備的必不可少的基本屬性,失去了可讀性的程序,其可維護(hù)性和可擴(kuò)展性也就無從談起了。這三大原則從方法論的角度規(guī)定了一切最佳實(shí)踐都不能違背這三大程序的基本屬性。否則,我們遲早會(huì)為一些蠅頭小利而舍棄程序開發(fā)的源頭根本。當(dāng)一個(gè)程序失去了可讀性、可維護(hù)性和可擴(kuò)展性,它也就失去了生命力。

最佳實(shí)踐 簡(jiǎn)單是美(Simple is Beauty)。

簡(jiǎn)單是美是一種指導(dǎo)思想,它其實(shí)包含兩個(gè)層次的意思。第一層意思是消除重復(fù)(Don't repeat yourself),這是一個(gè)顯而易見的代碼重構(gòu)標(biāo)準(zhǔn)。第二層意思則是要求我們化繁入簡(jiǎn)(Heavy to Light),用盡量少的代碼語言來表達(dá)盡量多的邏輯意義。

簡(jiǎn)單是美,將最佳實(shí)踐的要求細(xì)化到了方法論的層面。然而,無論我們的程序如何簡(jiǎn)單,都應(yīng)該始終記得,簡(jiǎn)單但必須可讀,簡(jiǎn)單但必須可擴(kuò)展。切忌為了一些細(xì)節(jié),而忘記更大的原則。

最佳實(shí)踐 盡可能使用面向?qū)ο蟮挠^點(diǎn)進(jìn)行編程。

我們可以看到,這個(gè)層面的最佳實(shí)踐,已經(jīng)從基本準(zhǔn)則和指導(dǎo)思想轉(zhuǎn)向了具體的編程層面。雖然面向?qū)ο笞陨硪仓皇且环N編程的指導(dǎo)思想,然而它卻是和程序設(shè)計(jì)與實(shí)現(xiàn)息息相關(guān)并且對(duì)程序編寫影響最大的一條編程準(zhǔn)則。

面向?qū)ο筮@個(gè)概念本身就是一個(gè)非常耐人尋味的問題。要討論面向?qū)ο蟮母拍?、設(shè)計(jì)和方法論,恐怕一天一夜都講不完。在本章之初,我們從“對(duì)象”這個(gè)概念入手,通過對(duì)“對(duì)象”內(nèi)部結(jié)構(gòu)的分析,試圖向讀者展示面向?qū)ο缶幊讨械囊恍┲匾碚摗Wx者對(duì)這些理論不應(yīng)停留在死記硬背的層面,而是要將它們?nèi)谌氲娇蚣艿脑O(shè)計(jì)理念中去理解。同時(shí),這些理論也將成為我們判別框架和設(shè)計(jì)方案優(yōu)劣的重要標(biāo)準(zhǔn)。

最佳實(shí)踐 減少依賴(消除耦合)。

之前在分析框架的本質(zhì)時(shí)已經(jīng)提到,任何Java程序總是依賴于其運(yùn)行環(huán)境(JVM層)和支持應(yīng)用程序的JAR層。加入到CLASSPATH中JAR越多,就意味著程序?qū)ν獠凯h(huán)境的依賴度越高,對(duì)外部環(huán)境的依賴度越高,就意味著程序本身越難以脫離特定的外部環(huán)境進(jìn)行單元測(cè)試。因此,減少甚至消除依賴,也成為許多框架所追求的目標(biāo)。

Struts2在這一點(diǎn)上做得尤為成功。Struts2不但實(shí)現(xiàn)了Web框架與Web容器之間的解耦合,還在此基礎(chǔ)之上實(shí)現(xiàn)了各個(gè)編程元素之間的有效溝通。在之后的章節(jié)中,我們會(huì)深入探究Struts2在這條最佳實(shí)踐上所做出的努力。

2.4 Web開發(fā)的基本模式

到此為止,我們花了大量的篇幅介紹了許許多多與Web開發(fā)完全無關(guān)的東西。無論是面向?qū)ο蟮母拍?、框架的本質(zhì)內(nèi)容還是我們開發(fā)中應(yīng)當(dāng)遵循的最佳實(shí)踐,它們都是程序員需要培養(yǎng)的內(nèi)在修養(yǎng)。接下來,我們將話題真正轉(zhuǎn)入Web開發(fā),來看看在Web開發(fā)中應(yīng)該遵循什么樣的最佳實(shí)踐。

2.4.1 分層開發(fā)模式

之前我們討論了Web開發(fā)中幾條基本的最佳實(shí)踐,它們會(huì)成為貫穿本書始終的指導(dǎo)思想。明確了指導(dǎo)思想,我們有必要從方法論的角度來探討一下Web開發(fā)的一些基本模式。

從宏觀上來說,Web開發(fā)模式中最最重要的一條是分層開發(fā)模式。分層開發(fā)模式是指,在開發(fā)J2EE程序時(shí),將整個(gè)程序根據(jù)功能職責(zé)進(jìn)行縱向劃分。一個(gè)比較典型并為大家所熟知的劃分方法是將整個(gè)程序分為:表示層、業(yè)務(wù)層和持久層,如圖2-5所示。

圖2-5 分層開發(fā)模式示意圖

不同的層次,實(shí)際上承擔(dān)了不同的功能職責(zé):

表示層(Presentation Layer)—負(fù)責(zé)處理與界面交互相關(guān)的功能

業(yè)務(wù)層(Business Layer)—負(fù)責(zé)復(fù)雜的業(yè)務(wù)邏輯計(jì)算和判斷

持久層(Persistent Layer)—負(fù)責(zé)將業(yè)務(wù)邏輯數(shù)據(jù)進(jìn)行持久化存儲(chǔ)

分層開發(fā)模式是技術(shù)層面的“分而治之”設(shè)計(jì)思想的一種體現(xiàn)。而蘊(yùn)含在其內(nèi)部的驅(qū)動(dòng)力還是我們反復(fù)強(qiáng)調(diào)的:程序的可讀性和可擴(kuò)展性。出于可讀性考慮,把不同功能職責(zé)的代碼分開,能夠使程序流程更加清晰明了;出于可擴(kuò)展性考慮,也只有把相類似的功能歸結(jié)為一個(gè)縱向?qū)哟?,才使得我們?cè)谶@個(gè)層次之上研究通用的解決方案成為可能。

分層開發(fā)模式,從邏輯上是將開發(fā)職責(zé)分派到不同的對(duì)象之上去執(zhí)行的一種設(shè)計(jì)思想。回顧我們?cè)诿嫦驅(qū)ο鬁\談的章節(jié)中所提到的對(duì)象協(xié)作關(guān)系,也正是分層開發(fā)模式的理論依據(jù)。

既然是職責(zé)分派,我們就不得不分清什么是職責(zé)、什么樣的對(duì)象適合什么樣的職責(zé)。如此一來,有關(guān)分層開發(fā)模式的討論就變成了一個(gè)典型的哲學(xué)問題。凡是哲學(xué)問題,都會(huì)出現(xiàn)正反兩派。分層開發(fā)模式所涉及的爭(zhēng)論主題主要包括兩個(gè)方面:第一,分層開發(fā)到底有無必要?第二,對(duì)于一個(gè)J2EE程序到底分為多少層進(jìn)行開發(fā)比較合適?

我們先來探討第一個(gè)問題:對(duì)一個(gè)程序?qū)嵤┓謱娱_發(fā)到底有無必要?分層開發(fā)模式,為程序的可擴(kuò)展性提供了可能性,但是當(dāng)問題的作用域變小時(shí),分層開發(fā)模式反而成為一種累贅。有許多程序員會(huì)抱怨,一個(gè)簡(jiǎn)單的邏輯功能,動(dòng)輒就要十幾個(gè)文件配合上百行代碼來完成。此時(shí),我們不禁要問:我們的開發(fā)真的需要分層嗎?分層開發(fā)到底為我們帶來了多少好處呢?針對(duì)這一問題,我們不妨來看看Struts2的一個(gè)官方的FAQ,如圖2-6所示。

圖2-6 Struts的FAQ

非常明顯,當(dāng)問題的作用域發(fā)生變化時(shí),解決問題的方法也要相應(yīng)做出改變。所以,分層開發(fā)模式,對(duì)于大型企業(yè)應(yīng)用或者產(chǎn)品級(jí)的應(yīng)用程序開發(fā)是有著重要意義的;然而當(dāng)一個(gè)應(yīng)用程序足夠小,并且需求的變更處于可控的范圍之內(nèi)時(shí),我們對(duì)于分層開發(fā)模式的選擇應(yīng)該謹(jǐn)慎。這就是所謂的“殺雞焉用牛刀”。

我們?cè)賮砜纯吹诙€(gè)問題:對(duì)于一個(gè)J2EE程序,到底要分多少層進(jìn)行開發(fā)比較合適?這是一個(gè)與整個(gè)應(yīng)用程序構(gòu)架相關(guān)的話題。有許多程序員贊同分層開發(fā)模式,不過他們都希望將層次分得盡量簡(jiǎn)單,崇尚“簡(jiǎn)單是美”的原則。對(duì)于這一問題,實(shí)際上也沒有絕對(duì)正確的答案。因?yàn)?span id="m8nmi8s" class="bold">一切脫離了業(yè)務(wù)實(shí)際的架構(gòu)設(shè)計(jì)都是虛幻的。我們只能在實(shí)踐中不斷總結(jié),并將前人的許多經(jīng)驗(yàn)作為我們進(jìn)行開發(fā)層次劃分的重要依據(jù),選擇適合于實(shí)際業(yè)務(wù)需求的開發(fā)層次,才是程序開發(fā)的最佳實(shí)踐。

這些有關(guān)分層開發(fā)的哲學(xué)問題的討論,每個(gè)程序員都有自己的見解。然而從框架的角度,我們也能看出一些框架的設(shè)計(jì)者對(duì)于某個(gè)開發(fā)層次的理解,因?yàn)?span id="ekwbxma" class="bold">我們最最熟悉的這些著名的框架,實(shí)際上就是為了應(yīng)對(duì)各個(gè)開發(fā)層次的編程問題而設(shè)計(jì)的解決方案。比如說:

Struts2是表示層的框架;Spring是業(yè)務(wù)層的框架;Hibernate是持久層的框架。

在本書中,我們所有討論的重點(diǎn)實(shí)際上是圍繞著表示層的解決方案 — Struts2進(jìn)行的。筆者花了那么多筆墨,才把Struts2這位主人公引出來的目的,是希望讀者能夠站在全局的高度來審視Struts2,也只有這樣,才能夠真正學(xué)好每一個(gè)開源框架。

2.4.2 MVC模式

在分層開發(fā)模式的前提下,每一個(gè)層次都可以單獨(dú)研究,并尋找合適的解決方案和最佳實(shí)踐。對(duì)于表示層,有一種稱之為MVC模式的最佳實(shí)踐被廣泛使用,并在此基礎(chǔ)上創(chuàng)建了許多基于這種模式的開發(fā)框架。

MVC模式實(shí)際上是眾多經(jīng)典的Java開發(fā)模式中的一種。它的基本原理是通過元素分解,來處理基于“請(qǐng)求-響應(yīng)”模式的程序中的各種問題。

M (Model)—數(shù)據(jù)模型

V (View)—視圖展現(xiàn)

C(Control)—控制器

任何一個(gè)B/S應(yīng)用,其本質(zhì)實(shí)際上是一個(gè)“請(qǐng)求-響應(yīng)”的處理過程的集合體。那么MVC模式是如何被提煉出來并成為一個(gè)模式的呢? 我們來模擬一個(gè)“請(qǐng)求-響應(yīng)”的過程,如圖2-7所示。

圖2-7 請(qǐng)求-響應(yīng)模式

在整個(gè)請(qǐng)求-響應(yīng)過程中,有哪些元素是必不可少的呢?

數(shù)據(jù)模型

在圖中,就是順著箭頭方向進(jìn)行傳輸?shù)臄?shù)據(jù),它們是程序的核心載體,也是貫穿程序運(yùn)行的核心內(nèi)容。

對(duì)外交互

在圖中,對(duì)外交互表現(xiàn)為一個(gè)“頭”和一個(gè)“尾”。“頭”指的是請(qǐng)求發(fā)起的地方,沒有請(qǐng)求,一切之后的所有內(nèi)容都無從談起。“尾”指的是邏輯執(zhí)行完成后,對(duì)外展現(xiàn)出來的執(zhí)行結(jié)果。在傳統(tǒng)意義上,我們利用HTML擴(kuò)展的技術(shù)(如JSP等)來實(shí)現(xiàn)對(duì)外交互,在展現(xiàn)結(jié)果時(shí),我們還需要完成一定的展現(xiàn)邏輯,比如錯(cuò)誤展示、分支判斷,等等。

程序的執(zhí)行和控制

實(shí)際上它不僅是接受請(qǐng)求數(shù)據(jù)的場(chǎng)所,也是處理請(qǐng)求的場(chǎng)所。在請(qǐng)求處理完畢之后,它還要負(fù)責(zé)響應(yīng)跳轉(zhuǎn)。這個(gè)部分可能會(huì)存在著不同的表現(xiàn)形式。以前,我們用JSP和Servlet,后來用Struts1或者Struts2的Action。而這一變化,實(shí)際上包含了我們不斷對(duì)程序進(jìn)行重構(gòu)的過程。

上面這3大元素,在不同的年代被賦予了不同的表現(xiàn)形式。例如,在很久以前,我們使用Servlet或者JSP來編寫程序跳轉(zhuǎn)的控制過程,有了Struts1.X后,我們使用框架所定義的Action類來處理。這些不同的表現(xiàn)形式有的受到時(shí)代的束縛,表現(xiàn)形式非常落后,有的甚至已經(jīng)不再使用。但是我們忽略這些外在的表現(xiàn)形式就可以發(fā)現(xiàn),這不就是我們已經(jīng)熟悉的MVC嗎?

數(shù)據(jù)模型—Model

對(duì)外交互—View

程序的執(zhí)行和控制—Control

MVC的概念就這么簡(jiǎn)單,這些概念其實(shí)早已深入我們的內(nèi)心,而我們所缺乏的是將其本質(zhì)挖掘出來的能力。我們來看看如圖2-8所示的這幅流行了很多年的講述MVC模型的圖。

圖2-8 MVC模型圖

在這幅圖中,MVC三個(gè)框框各司其職,結(jié)構(gòu)清晰明朗。這也成為我們進(jìn)行編程開發(fā)的最強(qiáng)有力的理論武器,我們需要做的,只是為這些框框賦予不同的表現(xiàn)形式。實(shí)際上,框架就是這么干的!而框架的高明之處,僅僅在于它不僅賦予這些元素正確而恰當(dāng)?shù)谋憩F(xiàn)形式,同時(shí)解決了當(dāng)元素運(yùn)行起來時(shí)所碰到的各種問題。因此,我們始終應(yīng)該做到:程序時(shí)時(shí)有,概念心中留。只要MVC的理念在你心中,無論程序怎么變,都能做到萬變不離其宗。

2.5 表示層的困惑

當(dāng)表示層有了MVC模式,程序開發(fā)就會(huì)變得有章可循。至少,我們不會(huì)像無頭蒼蠅一樣無從入手。MVC模式很直觀地規(guī)定了表示層的各種元素,只要能夠通過恰當(dāng)?shù)某绦虮憩F(xiàn)形式來實(shí)現(xiàn)這些元素,我們實(shí)際上已經(jīng)在履行最佳實(shí)踐了。

至此,我們不妨返璞歸真,忘記所謂的框架,用最簡(jiǎn)單的方式來實(shí)現(xiàn)一個(gè)簡(jiǎn)單的MVC雛形。在這個(gè)過程中,我們不妨回到框架的本質(zhì)問題上,思考一下究竟一個(gè)框架為表示層解決了什么樣的編程難題,難道框架只是實(shí)現(xiàn)MVC這三大元素那么簡(jiǎn)單而已?

我們選擇Registration(注冊(cè))作為業(yè)務(wù)場(chǎng)景。首先,我們需要一個(gè)JSP頁(yè)面來呈現(xiàn)用戶注冊(cè)的各個(gè)字段、一個(gè)User類來表示用戶實(shí)體以及一個(gè)RegistrationServlet類來處理注冊(cè)請(qǐng)求。相關(guān)實(shí)現(xiàn)源碼如代碼清單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獲取參數(shù)
                  String name = req.getParameter("name");
                  String birthdayString = req.getParameter("birthday");
                  // 做必要的類型轉(zhuǎn)化
                  Date birthday = null;
                  try {
                      birthday = new SmpleDateFormat("yyyy-MM-dd").
          parse(birthdayString);
                  } catch (ParseException e) {
                  e.printStackTrace();
                  }
                  // 初始化User類,并設(shè)置字段到user對(duì)象中去
                  User user = new User();
                  user.setName(name);
                  user.setBirthday(birthday);
                  // 調(diào)用業(yè)務(wù)邏輯代碼完成注冊(cè)
                  UserService userService = new UserService();
                  userService.register(user);
                  req.getRequestDispatcher("/success.jsp").forward(req, resp);
              }
          }

除了上述這3段源代碼外,我們還需要建立起JSP頁(yè)面中的form請(qǐng)求與Servlet類的響應(yīng)之間的關(guān)系。這一關(guān)系,是在web.xml中維護(hù)的,如代碼清單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段代碼是如何構(gòu)成MVC的雛形的。

Model(數(shù)據(jù)模型)—User.java

View(對(duì)外交互)—registration.jsp

Control(程序執(zhí)行和控制)—RegistrationServlet.java

URL Mapping(請(qǐng)求轉(zhuǎn)化)—web.xml

我們可以看到MVC的實(shí)現(xiàn)似乎并不復(fù)雜。在不借助額外的框架幫助的前提下,只要基本知曉JSP和Servlet標(biāo)準(zhǔn)(它們是使用Java進(jìn)行Web開發(fā)的規(guī)范和標(biāo)準(zhǔn)),任何程序員都可以像模像樣地實(shí)現(xiàn)MVC模式,因?yàn)閺脑砩现v,MVC只是一個(gè)概念,我們只需要把這個(gè)概念中的各個(gè)元素賦予相應(yīng)的程序?qū)崿F(xiàn)即可。

不過程序終究是一個(gè)動(dòng)態(tài)的執(zhí)行過程。一旦程序開始運(yùn)行,上面的這些程序?qū)崿F(xiàn)就會(huì)開始遭遇種種困境。這些困境主要來源于兩個(gè)方面:其一,出于程序自身的可讀性和可維護(hù)性考慮,需要通過重構(gòu)來解決程序的復(fù)雜性困境。其二,出于業(yè)務(wù)擴(kuò)展的需求,需要通過框架級(jí)別的功能增強(qiáng)來解決可擴(kuò)展性困境。

問題1 當(dāng)瀏覽器發(fā)送一個(gè)Http請(qǐng)求,Web容器是如何接收這個(gè)請(qǐng)求并指定相應(yīng)的Java類來執(zhí)行業(yè)務(wù)邏輯并返回處理結(jié)果的?

這個(gè)問題是使用Java進(jìn)行Web開發(fā)的核心問題之一,我們將這個(gè)問題簡(jiǎn)稱為URL Mapping問題。這個(gè)問題的本質(zhì)實(shí)際上來源于Http協(xié)議與Java程序之間的匹配和交互。Web開發(fā)經(jīng)過了多年的發(fā)展,這一核心的哲學(xué)問題也經(jīng)歷了多次重大變革,有的崇尚由繁至簡(jiǎn),有的則從形式多樣化入手。

在上面的例子中,我們可以看到使用web.xml來表達(dá)URL Mapping關(guān)系遇到的困境:當(dāng)系統(tǒng)變大,這種配置上的重復(fù)操作會(huì)讓web.xml變得越來越大而難以維護(hù)。不僅如此,web.xml的配置也無法為URL Mapping建立起合適的規(guī)則引擎。

由此,解決URL Mapping問題的核心在于建立一套由Http協(xié)議中的URL表達(dá)式到Java世界中類對(duì)象的規(guī)則匹配引擎。額外的,這種規(guī)則匹配最好比較靈活而簡(jiǎn)單又不失必要的可維護(hù)性。

問題2 Web應(yīng)用是典型的“請(qǐng)求-響應(yīng)”模式的應(yīng)用,數(shù)據(jù)是如何順利流轉(zhuǎn)于瀏覽器和Java世界之間的?面對(duì)Http協(xié)議與Java世界數(shù)據(jù)形式的不匹配性,我們?nèi)绾文軌蛟诹鬓D(zhuǎn)時(shí)做到數(shù)據(jù)類型的自動(dòng)轉(zhuǎn)化?

這個(gè)問題伴隨著問題1而來,數(shù)據(jù)請(qǐng)求與數(shù)據(jù)返回相當(dāng)于是基于“請(qǐng)求-響應(yīng)”模式的Web程序的輸入和輸出。數(shù)據(jù)的本質(zhì)是存儲(chǔ)于其中的信息,只不過數(shù)據(jù)在不同的地方有不同的表現(xiàn)形式。例如,在瀏覽器中,數(shù)據(jù)總是以字符串形式展現(xiàn)出來,表現(xiàn)出“弱類型”的特征;在Java世界,數(shù)據(jù)則體現(xiàn)為一個(gè)個(gè)結(jié)構(gòu)化的Java對(duì)象,表現(xiàn)出“強(qiáng)類型”的特征。于是,就需要有一個(gè)工具能夠幫助我們解決在數(shù)據(jù)流轉(zhuǎn)時(shí)的數(shù)據(jù)形式的相互轉(zhuǎn)化。

在上面的例子中,我們可以看到RegistrationServlet中,我們編寫了額外的代碼,把頁(yè)面上傳遞過來的日期值轉(zhuǎn)化為Java中的Date對(duì)象。在參數(shù)的數(shù)量和Java對(duì)象越來越復(fù)雜的情況下,這種額外的代碼就會(huì)變成一種災(zāi)難,甚至成為我們開發(fā)的主要瓶頸之一。

解決數(shù)據(jù)流轉(zhuǎn)問題的方案是使用表達(dá)式引擎。將表達(dá)式引擎插入到程序邏輯執(zhí)行之前,我們就能從復(fù)雜的對(duì)象轉(zhuǎn)化中解放出來,從而進(jìn)一步簡(jiǎn)化開發(fā)流程。

問題3 Web容器是一個(gè)典型的多線程環(huán)境,針對(duì)每個(gè)Http請(qǐng)求,Web容器的線程池會(huì)分配一個(gè)特定的線程進(jìn)行處理。那么如何保證在多線程環(huán)境下,處理請(qǐng)求的Java類是線程安全的對(duì)象?如何保證數(shù)據(jù)的流轉(zhuǎn)和訪問都是線程安全的?

這個(gè)問題與問題1一樣,也是Web開發(fā)中的核心問題之一,因?yàn)樗婕癢eb開發(fā)中最為底層的處理機(jī)制問題。在上面的例子中,我們使用的是基于Servlet標(biāo)準(zhǔn)的方式進(jìn)行編程,擴(kuò)展Servlet用于處理Http請(qǐng)求。然而恰恰就是這種編程模型,是一種非線程安全的編程模型,因?yàn)镾ervlet對(duì)象是一個(gè)非線程安全的對(duì)象。也就是說,如果我們?cè)赿oPost方法中訪問RegistrationServlet中所定義的局部變量,就會(huì)產(chǎn)生線程安全問題(第4章會(huì)重點(diǎn)介紹線程安全問題產(chǎn)生的來龍去脈)。

傳統(tǒng)的表示層框架對(duì)于這個(gè)問題的處理方式是采用規(guī)避問題的方式。既然Servlet對(duì)象不是一個(gè)線程安全的對(duì)象,那么我們就干脆禁止在Servlet對(duì)象的方法中訪問Servlet對(duì)象的內(nèi)部變量。這種鴕鳥算法固然是一種有效的方案,但它卻不是一種合理的方案。最致命的一點(diǎn)是,它是一種非語法檢查級(jí)別的禁止,因此也就無法從根本上杜絕程序員犯這樣的錯(cuò)誤。

另外一種解決方案就是在整個(gè)請(qǐng)求周期中引入ThreadLocal模式,通過ThreadLocal模式的使用,將整個(gè)過程的對(duì)象訪問都線程安全化,徹底解決多線程環(huán)境下的數(shù)據(jù)訪問問題(有關(guān)ThreadLocal模式的方方面面,我們?cè)诤罄m(xù)章節(jié)中會(huì)詳細(xì)介紹)。ThreadLocal模式的引入對(duì)于Web層框架的影響是深遠(yuǎn)并且顛覆性的,因?yàn)樗鼮榭蚣軘[脫Web容器的依賴鋪平了道路,意味著我們可以通過合理的設(shè)計(jì),在脫離Servlet等Web容器元素的環(huán)境中進(jìn)行編程。

問題4 Controller層作為MVC的核心控制器,如何能夠在最大程度上支持功能點(diǎn)上的擴(kuò)展?

問題4來源于我們對(duì)程序本身的自然屬性(可讀性和可擴(kuò)展性)的需求。這一內(nèi)在需求實(shí)際上也驅(qū)動(dòng)著我們著手在整個(gè)MVC的構(gòu)架級(jí)別設(shè)計(jì)更為成熟有效的自擴(kuò)展方案。

從一個(gè)更加宏觀的角度來幫助我們理解這個(gè)問題,我們來舉一個(gè)制藥工廠生產(chǎn)藥品的例子。一個(gè)工廠在進(jìn)行批量生產(chǎn)時(shí),總是會(huì)引入“生產(chǎn)線”的概念。生產(chǎn)線能夠把整個(gè)制藥過程劃分成若干道工序,當(dāng)原材料經(jīng)過每一道工序,最終就會(huì)成為一個(gè)可出廠銷售的藥品。某一天,由于市場(chǎng)推廣的原因,需要改變藥品的包裝,那么我們對(duì)這條生產(chǎn)線的要求就是它能夠改變“包裝”這道工序的流程,更改成新的包裝。

在上面的例子中,我們可以看到并沒有一個(gè)“生產(chǎn)線”的概念。這種情況下,我們?nèi)蘸髮?duì)于邏輯功能的擴(kuò)展就變得困難重重。雖然我們發(fā)現(xiàn),RegistrationServlet或許和其他所有的Servlet有著非常類似的執(zhí)行步驟:接收參數(shù)、進(jìn)行類型轉(zhuǎn)換、調(diào)用業(yè)務(wù)邏輯接口執(zhí)行邏輯、返回處理結(jié)果。然而我們卻缺乏一條可以任意配置調(diào)度的生產(chǎn)線將這個(gè)過程規(guī)范起來。

解決這個(gè)問題從直觀上來講似乎很容易:沒有生產(chǎn)線,我們建一條生產(chǎn)線就行了。而事實(shí)上,“造輪子”實(shí)在是一件費(fèi)時(shí)費(fèi)力的事情,因?yàn)槲覀円紤]的方面實(shí)在太多。這時(shí)我們就不得不借鑒許多前輩的經(jīng)驗(yàn)了,尋找某些事件定義的框架,遵循框架的定義規(guī)范來進(jìn)行編程將是我們解決這個(gè)問題的主要途徑。

問題5 View層的表現(xiàn)形式總是多種多樣的,隨著Web開發(fā)技術(shù)的不斷發(fā)展,MVC如何在框架級(jí)別提供一種完全透明的方式來應(yīng)對(duì)不同的視圖表現(xiàn)形式?

這一問題是基于View(視圖)技術(shù)的不斷發(fā)展,造成傳統(tǒng)的基于HTML的視圖已經(jīng)不能滿足所有的需求而提出的。當(dāng)今,越來越多新的視圖技術(shù)被用于Web開發(fā)中,例如,模板技術(shù)、JSON數(shù)據(jù)流、Stream數(shù)據(jù)流、Flash展現(xiàn)等等。

在上面的例子中,我們可以看到負(fù)責(zé)視圖層跳轉(zhuǎn)的RegistrationServlet是通過硬編碼方式完成程序執(zhí)行跳轉(zhuǎn)的。這種方式不但無法支持多種新的視圖技術(shù),同時(shí)也無法使我們從復(fù)雜的視圖跳轉(zhuǎn)的硬編碼中釋放出來。

解決這個(gè)問題的最有效途徑是把不同的視圖技術(shù)進(jìn)行分類,針對(duì)不同的分類封裝不同的視圖跳轉(zhuǎn)邏輯,而最重要的一步是將這兩者與之前我們所提到的生產(chǎn)線有機(jī)結(jié)合起來。

問題6 MVC模式雖然很直觀地為我們規(guī)定了表示層的各種元素,但是如何通過某種機(jī)制把這些元素有機(jī)整合在一起,從而成為一個(gè)整體呢?

這個(gè)問題非常宏觀,卻是我們不得不去面對(duì)的一個(gè)問題。MVC雖然在概念上被規(guī)定下來,在實(shí)現(xiàn)上卻需要一個(gè)完整的機(jī)制來把這些元素都容納在一起。通常情況下,我們往往把這種機(jī)制稱之為配置元素。配置元素是構(gòu)成程序的重要組成部分,它把各種形式的程序通過某種配置規(guī)則聯(lián)系在一起。之前我們提到的URL Mapping實(shí)際上也屬于配置規(guī)則的一種,視圖的跳轉(zhuǎn)也是配置規(guī)則的一種。只有當(dāng)這種配置規(guī)則被建立起來,MVC模式才能真正運(yùn)作起來。

這一系列配置元素在框架內(nèi)部往往被定義成統(tǒng)一的可以被框架識(shí)別的數(shù)據(jù)結(jié)構(gòu)并在系統(tǒng)初始化的時(shí)候進(jìn)行緩存。而這些被緩存了的對(duì)象,也成為主程序的控制流在MVC框架中各個(gè)元素之間進(jìn)行流轉(zhuǎn)的依據(jù)。

如果從元素的表現(xiàn)形式上來看配置元素和控制流的關(guān)系,我們實(shí)際上可以看到整合過程的兩個(gè)層面:數(shù)據(jù)結(jié)構(gòu)和流程控制。所謂的框架,我們也只是在這兩個(gè)層面上做文章,一方面規(guī)定好這些配置元素的定義,另一方面指定程序運(yùn)轉(zhuǎn)的流程,從而控制和整合散落在各處的表示層元素。

2.6 如何學(xué)習(xí)開源框架

正確的學(xué)習(xí)方法不僅能夠事半功倍,也能夠使我們更加接近真理。在了解了框架的本質(zhì)和Web開發(fā)模式之后,我們來討論一下學(xué)習(xí)開源框架的基本方法。

在這里為大家總結(jié)了一些正確的學(xué)習(xí)方法和最佳實(shí)踐,這些不僅是筆者多年開發(fā)中的心得體會(huì),也汲取了網(wǎng)絡(luò)上的大家之言,希望對(duì)初學(xué)者或者正在為學(xué)習(xí)開源框架犯愁的朋友帶來一些啟示。這些學(xué)習(xí)方法,不僅適用于Struts2,同樣適用于許多其他的開源框架。

最佳實(shí)踐 閱讀、仔細(xì)閱讀、反復(fù)閱讀每個(gè)開源框架自帶的Reference。

這是學(xué)習(xí)框架最為重要,也是最開始最需要做的事情。不幸的是,事實(shí)上,絕大多數(shù)程序員對(duì)此并不在意,并且總是以種種理由作為借口不仔細(xì)閱讀Reference。

程序員的常見借口之一:英語水平跟不上,英文文檔閱讀起來太吃力。針對(duì)這樣的借口,我們需要指出,閱讀英文文檔是每個(gè)程序員必須具備的基本素質(zhì)之一,這就和調(diào)試程序需要耐心一樣,對(duì)一個(gè)程序員來說非常重要。當(dāng)然,閱讀英文文檔這一基本素質(zhì)是一點(diǎn)一滴積累培養(yǎng)起來的,對(duì)于那些閱讀起來實(shí)在覺得吃力的朋友,筆者的建議是結(jié)合中文的翻譯版本一起看。國(guó)內(nèi)有許多開源組織,例如滿江紅的開源支持者已經(jīng)為大家精心做了許多很有價(jià)值的翻譯,例如Spring、Hibernate等都有對(duì)應(yīng)的中文翻譯文檔。但是大家必須注意,看中文文檔,必須和英文文檔對(duì)照,因?yàn)闆]有人可以確保翻譯能夠百分之百正確,語義的不匹配會(huì)給你帶來極大的誤導(dǎo),通過對(duì)照,才能夠?qū)⒄`解降到最低。

程序員的常見借口之二:Reference太長(zhǎng),抓不住重點(diǎn)。在這里,筆者給出的建議是:耐心,耐心,還是耐心!從Reference的質(zhì)量而言,其實(shí)大多數(shù)開源框架的Reference都是非常優(yōu)秀的,基本包含了框架的方方面面。尤其是Struts2,由于歷史原因,Struts2的Reference基本上都是一個(gè)一個(gè)的專題Wiki文章拼起來的文檔,每篇文章都有一個(gè)固定的主題,不僅包含原理解析、注意事項(xiàng),有的還包含源碼解析和示例講解。閱讀Reference可能會(huì)非常枯燥,但是從價(jià)值的角度看,對(duì)Reference的閱讀往往是對(duì)大家?guī)椭畲蟮?。因此,筆者對(duì)閱讀Reference的建議是,多看幾遍。第一遍,你可以采取瀏覽(scan)的方式,目的是了解框架的整體架構(gòu)的大致功能。第二遍,挑重點(diǎn)章節(jié)仔細(xì)閱讀,并且輔以一定的代碼實(shí)踐,目的是徹底掌握某個(gè)分支領(lǐng)域的知識(shí)。第三遍,帶著問題閱讀,在文檔中尋找答案。

筆者之所以強(qiáng)烈推薦大家仔細(xì)閱讀開源框架自帶的Reference,主要基于以下的兩個(gè)原因:

權(quán)威性

這些自帶的Reference多數(shù)出自這些開源框架的作者或者開發(fā)人員之手。還有誰能夠比他們自己更了解他們自己編寫的產(chǎn)品呢?自己寫的程序,到底有哪些優(yōu)點(diǎn),如何使用,自己肯定是最最清楚的,所以要說到權(quán)威性,不可能有任何文檔比自帶的Reference更加權(quán)威。

正確性

自帶的Reference幾乎很少犯錯(cuò),所以不會(huì)給你帶來什么誤導(dǎo)信息。不僅如此,許多Reference已經(jīng)為你總結(jié)了框架使用過程中的許多最佳實(shí)踐。所以我們沒有理由不直接通過這些Reference來獲得第一手的資料。

最佳實(shí)踐 精讀網(wǎng)絡(luò)教程。

對(duì)于很多初學(xué)者來說,他們對(duì)看Reference這種學(xué)習(xí)方式的接受程度很低。相反,他們會(huì)去轉(zhuǎn)而學(xué)習(xí)一些網(wǎng)絡(luò)教程。一般而言,這些學(xué)習(xí)材料的實(shí)際價(jià)值要比Reference低很多。主要原因在于,作者在編寫這些教程時(shí),多數(shù)都會(huì)加入他們自己的學(xué)習(xí)思路,而忽略了框架本身所期望達(dá)到的程序開發(fā)最佳實(shí)踐,甚至?xí)o很多讀者以:“程序就是這么寫的”的誤導(dǎo)。所以,對(duì)于網(wǎng)絡(luò)上的絕大多數(shù)網(wǎng)絡(luò)教程,需要讀者有足夠的甄別能力,否則很容易被帶入歧途。

網(wǎng)絡(luò)上還有很多原版教程,例如《XXX in Action》系列。《XXX in Action》系列的書籍在市場(chǎng)上深受好評(píng)。然而,這些系列的書籍有些內(nèi)容也帶有作者個(gè)人的感情色彩。當(dāng)然,每個(gè)作者在編寫書籍或撰寫教程的過程中都會(huì)夾帶自己的感情色彩,這本不是什么壞事,不過既然我們已經(jīng)有了Reference作為閱讀的主體了,對(duì)這類書籍,我們需要采取的態(tài)度是“精讀”。

很多網(wǎng)絡(luò)教程,尤其是中文的網(wǎng)絡(luò)教程,基本上都是網(wǎng)友的經(jīng)驗(yàn)之談,也有寫成系列文章的。對(duì)于網(wǎng)絡(luò)教程,筆者的建議是:帶著問題去讀,去搜索你的答案,而不要當(dāng)作核心文檔來閱讀。在找到答案之后,也需要通過實(shí)踐來反復(fù)驗(yàn)證,因?yàn)樵S多解決方案可能只是臨時(shí)的,并不是程序開發(fā)中的最佳實(shí)踐。

最佳實(shí)踐 搭建環(huán)境運(yùn)行每個(gè)開源框架自帶的sample項(xiàng)目。

每個(gè)開源框架基本上都會(huì)自帶有sample項(xiàng)目。以Struts2為例,在Struts2的分發(fā)包的apps目錄下就有多個(gè)sample項(xiàng)目,如圖2-9所示。

圖2-9 Struts2自帶的sample項(xiàng)目

Struts2是一個(gè)典型的Web層框架,所以所有Struts2的sample項(xiàng)目都以war包的形式給出,大家可以將這些war包的任何一個(gè)復(fù)制到你的Web容器的運(yùn)行目錄下,啟動(dòng)Web容器就可以訪問這些sample項(xiàng)目。

千萬不要小看這些sample項(xiàng)目,我們可以從這些項(xiàng)目中獲取許多重要的知識(shí)和信息。有些知識(shí)恐怕連Reference都不曾提及。這些原生態(tài)的東西,使得我們完全無須舍近求遠(yuǎn)地到網(wǎng)絡(luò)上去到處尋找例子,只要學(xué)習(xí)這些例子,就足以掌握開源框架的種種特性了。

我們可以就其中的三個(gè)sample項(xiàng)目進(jìn)行舉例分析。

struts2-blank-2.2.1.war

一般而言,名為xx-blank-xxx.war的sample項(xiàng)目是一個(gè)開源框架的一個(gè)最小可運(yùn)行范例。所以,如果大家仔細(xì)學(xué)習(xí)這個(gè)war包中的內(nèi)容,至少可以發(fā)現(xiàn)組成一個(gè)Struts2程序的最小元素到底有哪些。在其中的WEB-INF/lib目錄下,我們能夠找到Struts2程序運(yùn)行所需要依賴的JAR包的最小集合(如圖2-10所示),我們還能從中學(xué)習(xí)Struts2的各種基礎(chǔ)配置的編寫等。

圖2-10 Struts2所依賴的基本JAR包的最小集合

struts2-portlet-2.2.1.war

這個(gè)sample項(xiàng)目告訴我們?cè)赑ortal環(huán)境下的Struts2的應(yīng)用應(yīng)該如何編寫。通過與struts2-blank-2.2.1.war這個(gè)項(xiàng)目的比較,大家可以發(fā)現(xiàn),Struts2在應(yīng)對(duì)不同的應(yīng)用服務(wù)器環(huán)境方面的不同。

struts2-showcase-2.2.1.war

這個(gè)sample項(xiàng)目是Struts2特性的一個(gè)大雜燴,包含了絕大多數(shù)的Struts2的特性示例。這個(gè)sample項(xiàng)目對(duì)于大家閱讀Reference是非常有幫助的。比如說,大家在閱讀文檔時(shí)看到了“文件上傳”的章節(jié),那么大家就可以參考這個(gè)項(xiàng)目中的upload子目錄中的相關(guān)的類和配置。這相當(dāng)于一邊看文檔,一邊已經(jīng)有一個(gè)現(xiàn)成的可以運(yùn)行的例子輔助你進(jìn)行學(xué)習(xí)。所以,這個(gè)項(xiàng)目與Reference的搭配是相得益彰、互為補(bǔ)充的,可以作為大家學(xué)習(xí)Struts的最佳資源。

最佳實(shí)踐 自己寫一個(gè)sample項(xiàng)目親身體驗(yàn)。

這一點(diǎn)其實(shí)不用多說,大家也應(yīng)該明白。不過筆者還是見過不少程序員,眼高手低,整天吹噓這個(gè)框架的優(yōu)點(diǎn),那個(gè)框架的優(yōu)勢(shì),但如果讓他自己動(dòng)手用這些框架寫一段程序,又變得手足無措。

實(shí)踐是檢驗(yàn)真理的唯一標(biāo)準(zhǔn)。只有自己親自動(dòng)手去實(shí)踐,才能說明你真正掌握了某種技術(shù),理解了某個(gè)框架的特性。在編寫自己的sample項(xiàng)目時(shí),大家可以針對(duì)不同的特性,人為設(shè)置好業(yè)務(wù)場(chǎng)景(例如,使用“登錄”作為一個(gè)基本的業(yè)務(wù)場(chǎng)景),在實(shí)踐中不斷地重構(gòu)你的代碼,從而領(lǐng)悟框架開發(fā)中的最佳實(shí)踐,提升自己的開發(fā)水平。

最佳實(shí)踐 帶著問題調(diào)試(Debug)開源框架的源碼。

如果大家對(duì)某個(gè)開源框架的使用已經(jīng)比較熟練,對(duì)其內(nèi)部的原理也基本掌握,或許你就會(huì)對(duì)其中的某些設(shè)計(jì)原理和設(shè)計(jì)思想產(chǎn)生興趣。這個(gè)時(shí)候,通過開源框架的源碼來尋找問題的答案不失為一個(gè)很好的進(jìn)一步學(xué)習(xí)的途徑。

在學(xué)習(xí)開源框架的源碼時(shí),筆者的建議是當(dāng)程序運(yùn)行在Debug模式的狀態(tài)下,對(duì)源碼進(jìn)行調(diào)試,在Debug的過程中,查看在開源框架的內(nèi)部到底運(yùn)行了哪些類,它們的執(zhí)行順序是怎樣的以及這些類中臨時(shí)變量的執(zhí)行狀態(tài)。筆者堅(jiān)決反對(duì)逐個(gè)package地去閱讀源碼,這毫無意義。因?yàn)槌绦虮旧硎且粋€(gè)整體,程序之所以成為程序,其本質(zhì)在于它是動(dòng)態(tài)的、運(yùn)行的。如果我們逐一去閱讀源碼,就相當(dāng)于把一個(gè)完整的整體進(jìn)行肢體分解,那么我們將永遠(yuǎn)無法看到一個(gè)完整的動(dòng)態(tài)執(zhí)行過程。學(xué)習(xí)源碼,最重要的一點(diǎn)在于抓住一個(gè)程序在運(yùn)行過程中某一時(shí)刻某個(gè)關(guān)鍵類的運(yùn)行狀態(tài)和最終狀態(tài),而這些都能通過調(diào)試源碼來實(shí)現(xiàn),這才是閱讀源碼的最佳實(shí)踐。

2.7 小結(jié)

本章討論的話題是非常重要的,因?yàn)槿魏渭?xì)節(jié)都無法脫離基本概念而存在。如果我們要探尋Struts2的細(xì)節(jié),就必須了解Struts2作為一個(gè)框架存在的基本意義。本章從面向?qū)ο蟮幕靖拍钫勂穑接懥丝蚣艿谋举|(zhì),揭示了Web開發(fā)過程與框架之間的依存關(guān)系、Web開發(fā)中的一些最佳實(shí)踐,并由此提出Web開發(fā)中的一些核心問題。最后,筆者還給出了正確學(xué)習(xí)Struts2的方法供讀者參考。這些學(xué)習(xí)方法是非常有價(jià)值的,建議有經(jīng)驗(yàn)的程序員也看一看。

讀完本章,大家不妨帶著本章提出的這些核心問題,到本書其他的章節(jié)去尋找答案。等到所有的問題都迎刃而解之時(shí),或許大家對(duì)于框架和Web開發(fā)的理解也將更上一層樓。

回顧本章的所有內(nèi)容,大家對(duì)下面這些問題是否有了一個(gè)大致的答案呢?

對(duì)象有哪三種構(gòu)成模式?

對(duì)象有哪些關(guān)系模型?

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

在整個(gè)Web開發(fā)的過程中,我們應(yīng)該牢記哪些最佳實(shí)踐?

什么是MVC模式?MVC模式對(duì)于Web開發(fā)的主要作用是什么?

在Web開發(fā)中,我們將遇到哪些主要的困境?

Struts2運(yùn)行所依賴的最少的JAR文件資源的組合有哪些構(gòu)成?

如何正確學(xué)習(xí)一個(gè)開源框架?

主站蜘蛛池模板: 布拖县| 芷江| 晋江市| 建湖县| 荣昌县| 广水市| 灵台县| 白朗县| 甘孜县| 邯郸县| 中山市| 洪雅县| 会理县| 吉安市| 海伦市| 泽州县| 襄汾县| 伊宁市| 巴塘县| 葫芦岛市| 旬邑县| 柳州市| 邵东县| 广东省| 宁安市| 普格县| 永宁县| 平阳县| 丰顺县| 承德县| 呼玛县| 会泽县| 伊金霍洛旗| 布尔津县| 建宁县| 大关县| 弥渡县| 邢台市| 普洱| 泌阳县| 南阳市|