在定義了架構愿景之后,團隊中的所有人員應該對待開發的軟件有一定的了解了。但是,面對一個龐大的軟件系統,接下來要做些什么呢?分而治之的思想是計算機領域非常重要的思想,因此我們也從這里開始入手。要進行應用軟件的設計,分層是非常重要的思想,掌握好分層的思想,設計出的軟件是可以令人賞心悅目的。由于這一章的重要性和特殊性,本章的內容分為上下兩節,并不采取模式描述語言的方式。
分層只是將系統進行有效組織的方式。
本章特別針對于企業應用進行討論,但其中大部分的內容都可以應用在其它的系統中,或為其它的系統所參考。
在企業應用中,有兩個非常重要的概念:業務邏輯和持久性。可以說,企業應用是圍繞著業務邏輯進行開展的。例如報銷、下訂單、貨品入庫等都是業務邏輯。從業務邏輯的底層實現來看,業務邏輯其實是對業務實體進行組織的過程。這一點對于面向對象的系統才成立,因為在面向對象的系統中,識別業務實體,并制定業務實體的行為是非常基礎的工作,而不同的業務實體的組合就形成了業務邏輯。
還有另一個重要的概念是持久性。企業應用中大部分的數據都是需要可持久化的。因此,基礎組織支持持久性就顯得非常的重要。目前最為通行的支持持久性的機制是數據庫,尤其是關系性數據庫-RDBMS。
除此之外,在企業應用中還有其它的重要概念,例如人機交互。
為了能夠更有效的對企業中的各種邏輯進行組織,我們使用層技術來實現企業應用。層技術在計算機領域中有著悠久的歷史,計算機的實現中就引用了分層的概念。TCP/IP的七層協議棧也是典型的分層的概念。分層的優勢在于:
上層的邏輯不需要了解所有的底層邏輯,它只需要了解和它鄰接的那一層的細節。我們知道TCP/IP協議棧就是通過不同的層對數據進行層層封包的,不同層間的耦合度明顯降低。通過嚴格的區分層次,大大降低了層間的耦合度。
某一層次的下級層可以有不同的實現。例如同樣的編程語言可以在不同的操作系統和不同的機器中運行。
和第三條類似的,同一個層次可以支持不同的上級層。TCP協議可以支持FTP、HTTP等應用層協議。
綜合上面的考慮,我們把企業應用分為多個層次。企業應用到底應該分為幾種層次,目前還沒有統一的意見。
在前些年的軟件開發中,兩層結構占有很重要的位置。例如在銀行中應用很廣的大型主機/終端方式,以及Client/Server方式。兩層的體系結構一直到現在還廣泛存在,但是兩層結構卻有著很多的缺點,例如客戶端的維護成本高、難以實現分布式處理。隨著在兩層結構的終端用戶和后端服務間加入更多的層次,多層的結構出現了。
經典的三層理論將應用劃分為三個層次:
表示層(Presentation Layer),用于處理人機交互。目前最主流的兩種表示層是Windows格式和WebBrowser格式。它主要的責任是處理用戶請求,例如鼠標點擊、輸入、HTTP請求等。
領域邏輯層(Domain Logic Layer),模擬了企業中的實際活動,也可以認為是企業活動的模型。
數據層(Data source Layer),處理數據庫、消息系統、事務系統。
在實際的應用中,三層結構有一些變化。例如,在Windows的.NET系統中,把應用分為三個層次:表示層(Presentation Layer)、業務層(Business Layer)、數據訪問層(Data Access Layer),分別對應于經典的三層理論中的三個層次。值得一提的是,.NET系統中表示層可以直接訪問數據訪問層,即記錄集技術。在ADO.NET中,這項技術已經非常成熟,并通過表示層中的某些數據感知組件,實現非常友好的功能。這種越層訪問的技術通常被認為是不被允許的,因為它可能會破壞層之間的依賴關系。而在Windows平臺中,嚴格遵守準則就意味著需要大量額外的工作量。因此,我們看到準則也不是一成不變的。
在J2EE的環境中,三層結構演變為五層的結構。在表示層這里,J2EE將其分為運行在客戶機上的用戶層(Client Layer),以及運行在服務端上的Web層(Presentation Layer)。這樣做的主要理由是Web Server已經成為J2EE中非常核心的技術,例如JSP和Java Servlet都和它有關系。Web層為用戶層提供表示邏輯,并對用戶的請求產生回應。
業務層(Business Layer)并沒有發生變化,仍然是處理應用核心邏輯之處。而數據層則被劃分為兩個層次:集成層(Integration Layer)和資源層(Resource Layer)。其中,資源層并非J2EE所關心的內容,它可能是數據庫或是其它的老系統,集成層是重要的層次,包括事務處理,數據庫映射系統。
實例
這一章的的組織方式和之前的模式有一些差別。我們先從一個例子來體會架構設計中分層的重要性。

上圖是一個業務處理系統的軟件架構圖。在上圖中,我們把軟件分為四個層次。這種層次結構類似于我們前面所談的J2EE的分層。但是和J2EE不同的是,缺少了一個Web Server層,這是因為目前我們還沒有任何對Web Server的需要。
在資源層上,我們有三種資源:數據庫、平臺服務、UI。數據庫是企業應用的基礎,而這里的平臺服務指的是操作系統系統或第三方軟件提供的事務管理器的功能。值得注意的是,這里的事務管理器指的并不是數據庫內部支持的事務,而是指不同的業務實體間事務處理,這對于企業應用有著很重要的意義。因為對于一個企業應用來說,常常需要處理跨模塊、跨軟件、甚至跨平臺的會話(Session),這時候,單純的數據庫支持的事務往往就難以勝任了。這方面的例子包括微軟的MTS和Oracle的DBLink。當然,如果說,在你處理的系統中,可以使用數據庫事務來處理大部分的會話的話,那就可以避免考慮這方面的設計了。除了典型的事務管理器,平臺還能夠提供其它的服務。對于大部分的企業應用來說,都是集成了多個的平臺服務,平臺服務對架構的設計至關重要。但是從分層的角度上考慮,上層的設計應該盡可能的和平臺無關。和使用平臺服務類似的,一般來說,企業應用都不會從頭設計界面,大部分情況下都會使用現有的的UI資源。比如Window平臺的MFC中的界面部分。因此,我們把被使用的UI資源也歸到資源層這個層次上。
資源層的上一層是集成層。集成層主要完成兩項工作,第一項是使用資源層的平臺服務,完成企業應用中的事務管理。有些事務處理機制已經提供了比較好封裝機制,直接使用資源層的平臺服務就可以了。但是對于大多數的應用來說,平臺提供的服務往往是比較簡單的,這時候集成層就派上大用場了。第二項是對上一層的對象提供持久性機制。可以說,這是一組起到過渡作用的對象。它實際上使用的是資源層的數據庫功能,并為上一層的對象提供服務。這樣,上一層的業務對象就不需要直接同數據庫打交道。對于那些底層使用關系型數據庫,編程中使用面向對象技術的系統來說,目前比較常見的處理持久性的做法是對象/關系映射(OR Mapping)。
在這個層次上,我們可以做一些擴展,來分析層的作用。假設我們的系統需要處理多個數據庫,而不同數據庫之間的處理方式有一定的差異。這時候,層的作用就顯示出來了。我們在集成層中支持對多個數據庫的處理,但對集成層以上的層次提供統一的接口。對于業務層來說,它不知道,也不需要知道數據庫的差別。目前我們自己開發了集成層中的持久類,但是隨著功能的擴展,原有的類無法再支持新增加的功能了,新的解決方案是購買商用程序。為了盡可能的保持對業務層次的影響,我們仍然使用原有的接口,但是具體的實現已經不同了,新的代碼是針對新的商業程序來實現的。而對業務層來說,最理想的狀況是不需要任何的改變。當然現實中不太可能出現如此美好的情況,但可以肯定的一點是,引入層次比不引入層次要好的多。
以上列舉的兩個例子都是很好的解決了耦合度的問題。關于分層中的耦合度的問題,我們在下面還會討論。
業務層的設計比較簡單,暫時只是把它實現為一組的業務類。類似的,表示層的設計也沒有做更多的處理。表示層的類是繼承自資源層的。這是一種處理的方法,當然,也可以是使用關系,這和具體的實現環境和設計人員的偏好都有關系,并不是唯一的做法。在對軟件的大致架構有了一個初步了解之后,我們需要進一步挖掘需求,來細化我們的設計。在前面的設計中,我們對業務層的設計過于粗糙了。在我們的應用中,還存在一個舊系統,這個系統中實現了應用規則,從應用的角度來看,這些規則目前仍然在使用,但新的系統中會加入新的規則。在新系統啟用后,舊的系統中的規則處理仍然需要發揮它的作用,因此不能夠簡單的把所有的規則轉移到新系統中。(有時候我們是為了節省成本而不在新系統中實現舊系統的邏輯)。我們第二步的架構設計的細化過程中將會加入對新的要求的支持。
在細化業務層的過程中,我們仍然使用層技術。這時候,我們把原先的業務層劃分為兩個子層。對于大多數的企業應用來說,業務層往往是最復雜的。企業對象之間存在著錯綜復雜的聯系,企業的流程則需要用到這些看似獨立的企業對象。我們希望在業務層中引入新的機制,來達到組織業務類的目的。業務層的組織需要依賴于具體的應用環境,金融行業的應用和制造行業的應用就有著巨大的差距。這里,我們從一般性的設計思考的角度出發來看待我們的設計:

首先,我們看到,業務層被重新組織為兩個層次。增加層次的主要考慮是設計的重用性。從我們前面對層的認識,我們知道。較高的層次可以重用較低的層次。因此,我們把業務層中的一部分類歸入較低的層次,以供較高的層次使用。降低類層次的主要思路是分解行為、識別共同行為、抽取共性。
在Martin Fowler的分析模式中提供了一種將操作層和知識層分離的處理方法:

Action、Accountability、Party屬于較高層次的操作層(Operational Layer),這一層次的特點是針對于特定的應用。但是觀察Accountability和Party,有很多相似的職責。也就是說,我們對于知識的處理并不合適。因此,Martin Fowler提出了新的層次――屬于較低層次的知識層(Knowledge Layer)。操作層中可重用的知識被整理到知識層。從而實現對操作層的共性抽取。注意到雖然圖中的層次(Level)的概念和層(Layer)有所差別,但是思路是基本一致的。
另一種分層方法是利用繼承:

該圖中也是來自于分析模式一書。不同的部門有著差異點和共性,將共性提取到父類中是繼承的基本概念。這時候我們可以把父類看作是較低的層次,而把子類看作是較高的層次。對于一組層次結構很深的類來說,也可以從某一個水平線上分離層次。
最后一種方法需要分解并抽象對象的行為。C++的STL中為不同的數據類型和不同的容器實現了Iterator的功能。它的實現是典型的降低層次的行為。我們不考慮它對不同數據類型的支持,因為它使用了比較獨特的模板(Template)技術,只考慮它對不同的容器的實現。首先是對共性的分析,不論是數組(Array)還是向量(Vector),要執行遍歷操作,都需要知道三個條件:容器的起始點、容器的長度、匹配值。這就是一種抽象。通過這種方式,就可以統一不同容器的接口。
以上我們討論了細分層次的好處和實現策略。下面我們回到前例中,繼續討論層中的各個部件。
業務實體指的是企業中的一些單獨的對象。例如訂單、客戶、供應商等。由于業務實體可以被很多的業務流程(業務會話)所使用,因此我們把業務實體放在業務實體層中。企業中的業務實體大多是需要持久性的。因此在我們的設計中,業務實體將持久性的職責委托給下一個層次中的持久性包,而不是直接調用數據庫方法。另一種常用的方法是使用繼承――業務實體繼承自持久類。EJB中的Entity Bean就是典型的業務實體,它可以支持自動的持久性機制。
在我們的系統中,規則是一個尷尬的存在。部分的規則處于舊系統中,并使用舊系統的持久性機制。而新系統中有需要支持新的規則。對于上層的用戶來說,并不需要知道新舊系統的規則。如果我們在使用規則時做一個新舊規則的判斷并調用不同的系統函數,那就顯得太傻了。我們的設計對上層而言應該是透明的。
所以我們總共設計了三個包來實現規則的處理。包裝器包裝了舊系統中的規則,這樣做處于幾點考慮:首先,舊系統是面向過程的,因此我們需要用包裝器將函數(或過程)封裝到類中。其次,對舊系統的使用需要額外的程序(或平臺服務)的支持,因此需要單獨的類來處理。最后,我們可以使用Adapter模式[GOF 94]將新舊系統不同的接口給統一起來,以使他們能夠一起工作。新的規則類實現了新的規則,較好的做法是定義一個虛協議,具體的表現形式可以是虛基類或接口。然后由其子類實現虛協議。再結合前面介紹的把舊規則的接口轉換為新的接口的方法,就能夠統一規則接口。從圖中我們看到,業務實體需要使用規則,通過統一的接口,業務實體就能夠透明的使用新舊兩種規則。
在定義了規則和標準的規則接口之后,上一層的規則控制器就可以通過調用規則,來實現不同規則組合。因此這個規則控制器就類似于應用規則。在業務交易處理中需要調用規則控制器來實現部分的功能。
請注意,這里討論的思路雖然非常的簡單、清晰,但是現實中的處理卻沒有這么容易。因為為新舊規則制定統一的接口實在是太難了。要能夠使接口在未來也相對的穩定更是難上加難。而在應用已經部署之后,對已發布的接口的任何一個小改動都意味著很高的成本。在這個問題上,并沒有什么好的策略,經驗是最重要的。在實際中的一個比較實用的方法是為接口增加一些額外的參數。即便可能這個參數當前并沒有使用到,但是如果為了有可能使用的話,那還是加上吧。舉個例子,對于商業應用軟件而言,數據庫,或者說是關系數據庫一定是不可缺少的部分。大量的操作都需要和數據庫結合起來,因此,可以考慮在方法中加入一個數據庫連接字符串的參數。雖然這對于很多方法而言是沒什么必要的,但是為將來的實踐提供了一個可擴展的空間。國內的某個知名的ERP軟件的設計就采用了這種思路。
本章的主要精力都放在實例的研究上,在有了一個基本的概念之后,我們再回來談談分層的一些原則和需要注意的問題。
上篇我們用了大量的篇幅來觀察了一個實際的例子,相信大家已經對分層有了一個比較具體的概念了。在這一篇中我們就對分層在實踐中可能會遇到的問題做一個討論。分層在架構設計中是一種非常常見的,但是又很不容易用好的技術。因此我們這里花了很大的氣力來討論它。
由于這是一篇介紹軟件設計技術的文章,為了盡可能讓更多的人理解,本應該盡可能不涉及到過于具體的技術或平臺。但是這個目標可能很難實現,因為軟件設計是沒辦法脫離具體的實現技術的。因此本文能夠做到的是盡可能的不涉及具體的編碼細節。
何時使用分層技術?
分層技術實際上是把技術復雜化了。和以往簡單的CS結構的系統不同,分層往往需要使用特定的技術平臺來實現。當然,不使用這些技術平臺也是可能的,但是效果可能就沒有那么好了。支持分層技術的平臺有很多,包括目前主流的J2EE和.NET。甚至在不同廠商的開發平臺上,要求也不一樣。使用分層技術實現的多層架構,成本要比普通的CS架構高得多。
這就產生了一個非常現實的問題-并不是所有的軟件都適合采用分層技術的。一般來說,小型的軟件使用分層并沒有太大的意義,因為分層導致的成本超過它所能帶來的好處。在一般的CS結構中,可以把界面控制、邏輯處理和數據庫訪問都放在一塊兒。這種設計方式在純粹的多層主義者看來簡直就是十惡不赦。但是對于小型的軟件而言,這并不是什么大不了的事情。因為從表示層到數據層的整套功能都被囊括在一個功能塊中,同樣能夠實現較好的封裝。而且,如果結構設計的足夠好,也能夠避免表示層、業務層和數據層之間出現過高的耦合度。因此,除非確實需要,不然沒有必要使用分層技術。
尤其在處理一些特殊的項目時,嚴格的區分三層結構并不理想。比如在快速開發windows界面的應用時,往往會用到一些對數據庫敏感的控件,這種處理方法跨越了三個層次,但是卻很實用,成本也比較低。又比如一些框架,給出了從界面層到數據庫的綜合的解決方案,和windows的應用類似,嚴格的三層技術也不適用于這種情況。
如何使用分層技術?
從某種意義上來看,層其實是一個粗粒度的組件。就像我們使用組件技術是為了對系統進行一種劃分一樣,層的一個很大的作用也是如此。其目的是為了系統更容易被理解,不同的部分能夠被較容易的替換。
使用分層技術的依據是軟件開發人員的實際需要。如果你是在使用某些優秀的面向對象的軟件開發平臺的話,那它們一般都會建議(或是強制)你使用某一種分層機制。這是你采用分層技術的一大參考。
對于大多數有一定經驗的軟件團隊而言,一般都會積累一些軟件開發經驗。其中包含了很多在某些特定的領域中使用的基礎的類或組件。這些元素構成了一個系統的通用層次。這個層次也是分層時需要考慮的。例如一些應用軟件中使用的一些通用的Currency對象或是Organization對象。分析模式一書對此類的對象進行了充分細致的闡述。這個層次一般被稱為跨領域層(cross-domain layer),或稱為工具層(utility layer)。
目前的很多軟件都采用了數據庫映射技術。數據庫映射層對于企業應用系統非常的重要,因此也需要納入考慮之列。數據庫映射技術用起來簡單,但是要實現可不容易。如果不是非常有必要,盡可能使用現成的框架,或是采用其中部分的設計思路。試圖構建一個大而全的映射層次的代價是非常高昂的,認識不到這一點會帶來很大的麻煩。數據庫映射技術的知識,我們在下文中還有專門的篇幅來討論。
如何存放數據(狀態)?
在學習EJB的過程中,最先要理解的一定是有狀態和無狀態的概念。可以說,整個概念是多層體系的核心。為什么這么說呢?這里的狀態指的是類的狀態,例如類的屬性、變量等。由于狀態的不同,類也表現出差異來。而對于多層結構的軟件,創建和銷毀一個類的開銷是很大的,如果該軟件支持分布式的話尤為如此。所以如果系統的不同層次間進行頻繁的調用-創建一個類,再銷毀一個類。這種做法是非常消耗資源的。在應用系統的設計中,一般不單獨使用COM,就是這個原因。所以我們很自然的想到了一種經典的設計-緩沖池。把對象存放在緩沖池中,當需要的時候從池中取出一個,當不需要的時候再把對象放入池中。這種設計思路能夠大幅度的提高效率。但是這對能夠放在池中的對象也提出了苛刻的要求-所有的對象必須是無差異的,也就是無狀態的。只有這樣才能夠實現緩沖池。
一般來說,對象緩沖池的技術是用在中間的業務層上的。既然中間業務層上不能夠保留有狀態,那就出現了一個狀態轉移的問題。這里有兩種的選擇,一種是前移,把狀態移到用戶端,最典型的是使用cookie。這種選擇一般是由于狀態和用戶端有關,不需要長時間保存。另一種選擇是后移,把狀態移到數據層,由數據庫來實現持久性狀態,當需要時才把狀態提交給業務層。這種方式是企業應用軟件中采用最多的,但是也增大了數據庫的負擔。
處理好接口
由于使用了分層技術,因此原先那種在CS結構中類之間存在復雜關系就有必要重新評估了。一般層間的耦合度不宜過大。因此需要慎重的設計層之間的類調用方式。一些分布式軟件體系(例如J2EE)對層之間的調用方式以接口的形式給出了要求。同時,不同層之間僅僅知道目標層的接口,而不知道目標層的具體實現。EJB的home接口和remote接口就是這樣。在COM+體系中,也需要在設計類的同時,把接口公布出來,以供客戶方使用。
在設計層間的接口時,除了考慮開發平臺的約束之外,還有一點是開發人員必須考慮的。那就是業務需要。業務層中往往有非常多的對象和方法,它們之間的關系也非常的負責,但對于其它的層次來說,它并不關心這些細節。因此業務層公布的接口必須要簡單,而且和實現無關。因此,可以使用設計模式的Facade模式來簡化層間的接口。這種做法非常有效,EJB中的SessionBean和EntityBean區分就含有這種設計思路。
同樣的,不同層之間的數據傳遞也存在問題。如果不同層的物理節點在一起還好辦,如果不在一起,那就需要使用到分布式技術了。因為不同機器的內存地址編碼是不同的,如果接口之間采用對象引用的方式,那一定會出現問題。因此會將對象打包成字符串,發送到目標機器后再還原為對象。所有的分布式平臺都提供了對這種技術的支持,這里就不多說了。但是這種實現技術會對我們的設計思路產生影響,少量的數據直接使用字符串來傳遞,數據量大的話,就需要使用封裝了數據的對象。這類對象的設計需要非常的小心。在設計過程中可以參照開發平臺提供的一些標準做法。同樣的,數據的請求的頻率也是難點之一。過于頻繁的操作來自后端的數據會加大系統的開銷。因此,在設計調用方法時同樣需要結合實際應用來考慮。
兼顧效率
一般來說,純粹的面向對象設計者設計出的軟件都會比較完美。但是需要付出一定的代價。在一些大的軟件平臺上編程的時候,往往需要利用到平臺的一些機制。最典型的就是平臺的事務機制(最典型的包括J2EE平臺的JTS,以及COM+平臺的MTS),但是事務機制的實現往往需要平臺大量對象的支撐。這種情況下,創建一個支持事務的對象的開銷是很大的。處理這種問題有一種變通的辦法,就是僅僅對需要事務支撐的對象提供事務支持。這就意味著,一個單獨的業務實體類,可能需要根據是否支持事務分為兩種類:對該業務實體的select方法不需要事務的支持,只有update和delete方法才需要有事務的支持。這是不符合純面向對象設計者的觀點的。但是這種做法卻可以獲得比較優秀的效率。
圖1 將單個的業務實體分為不同的實現

應該承認,這種提高效率的做法加大了復雜度。因為對于客戶端來說,它們并不關心具體的實現技術。要求客戶端在某一種情況下調用這個類,在其它情況下又調用另一個類,這種做法既不符合面向對象的設計思路,也增大了層間耦合度及復雜性。因此,我們可以考慮使用接口或是外觀類(參見設計模式一書中的facade模式),把具體的實現封裝起來,而只把用戶關心的部分提供給用戶。這方面的技巧我們在下面的章節中還會提到。
以迭代的方式進行分層
軟件設計中的迭代做法同樣可以適用于分層。根據自己的經驗,在一開始就定義好所有的層次是很難的。除非有著非常豐富的經驗,都則實現和原先的設計總有或大或小的差距。因此調整勢在必行。每一次的迭代都能夠對分層技術進行改進,并為后一個項目積累了經驗。
這里的分層迭代不可以過于頻繁,每一次的迭代都是對架構的重大修改,都是需要投入人力的,而且會影響到軟件開發的進度。但是成功的迭代的效果是非常明顯的,能夠在接下來的開發周期中起到穩定架構,減少代碼量,提升軟件質量的功效。注意,不要讓新潮技術成為分層迭代的推動力。這是開發人員都常犯的毛病,這并不是什么缺點,只能稱為一種職業病吧。分層迭代的推動力應該源自于需求的演進以及現有架構的不穩定已經妨礙了軟件進一步的開發。因此這需要團隊中的技術主管對技術有著非常好的把握。
重構能夠對迭代有所幫助。嗅出代碼中隱藏的壞味道并加以改進。應該說,迭代是一種比較激烈的做法,更好的做法是在開發中不斷的對架構、層次進行調整。但這對團隊、技術、方法、過程都有著很高的要求。因此迭代仍然是一種主要的改進手段。
層內的細分
分層的思路還可以適用于層的內部。層內的細分并沒有固定的方式,其驅動因素往往是出于封裝性和重用的考慮。例如,在EJB體系中的業務層中,實體Bean負責實現業務對象,因此一個應用往往擁有大量的實體Bean。而用戶端并不需要了解每一個的實體Bean,對它們來說,只要能夠完全一些業務邏輯就可以了,但完成這些業務邏輯則需要和多個實體Bean打交道。因此EJB提供了會話Bean,來負責把實體Bean封裝起來,用戶只知道會話Bean,不知道實體Bean的存在。這樣既保證了實體Bean的重用性,又很好的實現了封裝。
面向接口編程
在前面的章節中,我們提到一個接口設計的例子。為什么我們提倡接口的設計呢?Martin Fowler在他的分析模式一書中指出,分析問題應該站在概念的層次上,而不是站在實現的層次上。什么叫做概念的層次呢?簡單的說就是分析對象該做什么,而不是分析對象怎么做。前者屬于分析的階段,后者屬于設計甚至是實現的階段。在需求工程中有一種稱為CRC卡片的玩藝兒,是用來分析類的職責和關系的,其實那種方法就是從概念層次上進行面向對象設計。因此,如果要從概念層次上進行分析,這就要求你從領域專家的角度來看待程序是如何表示現實世界中的概念的。下面的這句話有些拗口,從實現的角度上來說,概念層次對應于合同,合同的實現形式包括接口和基類。簡單的說吧,在概念層次上進行分析就是設計出接口(或是基類),而不用關心具體的接口實現(實現推遲到子類再實現)。結合上面的論述,我們也可以這樣推斷,接口應該是要符合現實世界的觀念的。
在Martin Fowler的另一篇著作中提到了這樣一個例子,非常好的解釋了接口編程的思路:
interface Person { public String name(); public void name(String newName); public Money salary (); public void salary (Money newSalary); public Money payAmount (); public void makeManager (); } interface Engineer extends Person{ public void numberOfPatents (int value); public int numberOfPatents (); } interface Salesman extends Person{ public void numberOfSales (int numberOfSales); public int numberOfSales (); } interface Manager extends Person{ public void budget (Money value); public Money budget (); } |
可以看到,為了表示現實世界中人(這里其實指的是員工的概念)、工程師、銷售員、經理的概念,代碼根據人的自然觀點設計了繼承層次結構,并很好的實現了重用。而且,我們可以認定該接口是相對穩定的。我們再來看看實現部分:
public class PersonImpFlag implements Person, Salesman, Engineer,Manager{ // Implementing Salesman public static Salesman newSalesman (String name){ PersonImpFlag result; result = new PersonImpFlag (name); result.makeSalesman(); return result; }; public void makeSalesman () { _jobTitle = 1; }; public boolean isSalesman () { return _jobTitle == 1; }; public void numberOfSales (int value){ requireIsSalesman () ; _numberOfSales = value; }; public int numberOfSales () { requireIsSalesman (); return _numberOfSales; }; private void requireIsSalesman () { if (! isSalesman()) throw new PreconditionViolation ("Not a Salesman") ; }; private int _numberOfSales; private int _jobTitle; } |
這是其中一種被稱為內部標示(Internal Flag)的實現方法。這里我們只是舉出一個例子,實際上我們還有非常多的解決方法,但我們并不關心。因為只要接口足夠穩定,內部實現發生再大的變化都是允許的。如果對實現的方式感興趣,可以參考Matrin Fowler的角色建模的文章或是我在閱讀這篇文章的一篇筆記。
通過上面的例子,我們可以了解到,接口和實現分離的最大好處就是能夠在客戶端未知的情況下修改實現代碼。這個特性對于分層技術是非常適用的。一種是用在層和層之間的調用。層和層之間是最忌諱耦合度過高或是改變過于頻繁的。設計優秀的接口能夠解決這個問題。另一種是用在那些不穩定的部分上。如果某些需求的變化性很大,那么定義接口也是一種解決之道。舉個不恰當的例子,設計良好的接口就像是我們日常使用的萬用插座一樣,不論插頭如何變化,都可以使用。
最后強調一點,良好的接口定義一定是來自于需求的,它絕對不是程序員絞盡腦汁想出來的。
數據映射層
在各個層的設計中,可能比較令人困惑的就是數據映射層了。由于篇幅的關系,我們不可能在這個問題上討論太多,只能是拋磚引玉。如果有機會,我們還可以來談談這方面的話題。
面向對象技術已經成為軟件開發的一種趨勢,越來越多的人開始了解、學習和使用面向對象技術。而大多數的面向對象技術都只是解決了內存中的面向對象的問題。但是鮮有提到持久性的面向對象問題。
面向對象設計的機制與關系模型有很大的不同,這造成了面向對象設計與關系數據庫設計之間的不匹配。面向對象設計的基本理論包括耦合、聚合、封裝、繼承、多態,而關系數據模型的理論則完全不同,它的基本原理是數據庫的三大范式。最明顯的一個例子是,Order對象包括一組的OrderItem對象,因此我們需要在Order類中設計一個容器(各個編程語言都提供了一組的容器對象及相關操作以供使用)來存儲OrderItem,也就是說Order類中的指針指向OrderItem。假設Order類和OrderItem分別對應于數據庫的兩張表(最簡單的映射情況),那么,我們要實現二者之間的關系,是通過在OrderItem表(假設名稱一樣)增加指向Order表的外鍵。這是兩種完全不同的設置。數據映射層的作用就是向用戶端隱藏關系數據庫的存在。
自己開發一個對象/關系映射工具是非常誘人的。但是應該考慮到,開發這樣一個工具并不是一件容易的事,需要付出很大的成本。尤其是手工處理數據一致性和事務處理的問題上。它比你想象的要難的多。因此,獲取一個對象/關系映射工具的最好途徑是購買,而不是開發。
總結
分層對現代的軟件開發而言是非常重要的概念。也是我們必須學習的知識。分層的總體思路并沒有什么特別的地方,但是要和自己的開發環境、應用環境結合起來,你還需要付出很多的努力才行。
在完成了分層之后,軟件架構其實已經清晰化了。下面的討論將圍繞著如何改進架構,如何使架構穩定方面的問題進行。
關于作者

|
|

|
林星,辰訊軟件工作室項目管理組資深項目經理,有多年項目實施經驗。辰訊軟件工作室致力于先進軟件思想、軟件技術的應用,主要的研究方向在于軟件過程思想、Linux集群技術、OO技術和軟件工廠模式。您可以通過電子郵件 iamlinx@21cn.com和他聯系。 |