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

第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文件資源的組合有哪些構成?

如何正確學習一個開源框架?

主站蜘蛛池模板: 建始县| 赤城县| 博罗县| 东台市| 林州市| 仪陇县| 田林县| 柳州市| 夹江县| 孝昌县| 全南县| 丰镇市| 梧州市| 江源县| 琼结县| 内乡县| 梁河县| 溧阳市| 泾川县| 汨罗市| 法库县| 垫江县| 呼和浩特市| 南皮县| 迁西县| 黔西县| 德安县| 剑川县| 保定市| 剑川县| 美姑县| 金川县| 白河县| 仲巴县| 桑日县| 会泽县| 乾安县| 福州市| 买车| 黄冈市| 象州县|