何時使用分層技術?
分層技術實際上是把技術復雜化了。和以往簡單的CS結構的系統(tǒng)不同,分層往往需要使用特定的技術平臺來實現(xiàn)。當然,不使用這些技術平臺也是可能的,但是效果可能就沒有那么好了。支持分層技術的平臺有很多,包括目前主流的J2EE和.NET。甚至在不同廠商的開發(fā)平臺上,要求也不一樣。使用分層技術實現(xiàn)的多層架構,成本要比普通的CS架構高得多。
這就產(chǎn)生了一個非常現(xiàn)實的問題-并不是所有的軟件都適合采用分層技術的。一般來說,小型的軟件使用分層并沒有太大的意義,因為分層導致的成本超過它所能帶來的好處。在一般的CS結構中,可以把界面控制、邏輯處理和數(shù)據(jù)庫訪問都放在一塊兒。這種設計方式在純粹的多層主義者看來簡直就是十惡不赦。但是對于小型的軟件而言,這并不是什么大不了的事情。因為從表示層到數(shù)據(jù)層的整套功能都被囊括在一個功能塊中,同樣能夠實現(xiàn)較好的封裝。而且,如果結構設計的足夠好,也能夠避免表示層、業(yè)務層和數(shù)據(jù)層之間出現(xiàn)過高的耦合度。因此,除非確實需要,不然沒有必要使用分層技術。
尤其在處理一些特殊的項目時,嚴格的區(qū)分三層結構并不理想。比如在快速開發(fā)windows界面的應用時,往往會用到一些對數(shù)據(jù)庫敏感的控件,這種處理方法跨越了三個層次,但是卻很實用,成本也比較低。又比如一些框架,給出了從界面層到數(shù)據(jù)庫的綜合的解決方案,和windows的應用類似,嚴格的三層技術也不適用于這種情況。
如何使用分層技術?
從某種意義上來看,層其實是一個粗粒度的組件。就像我們使用組件技術是為了對系統(tǒng)進行一種劃分一樣,層的一個很大的作用也是如此。其目的是為了系統(tǒng)更容易被理解,不同的部分能夠被較容易的替換。
使用分層技術的依據(jù)是軟件開發(fā)人員的實際需要。如果你是在使用某些優(yōu)秀的面向對象的軟件開發(fā)平臺的話,那它們一般都會建議(或是強制)你使用某一種分層機制。這是你采用分層技術的一大參考。
對于大多數(shù)有一定經(jīng)驗的軟件團隊而言,一般都會積累一些軟件開發(fā)經(jīng)驗。其中包含了很多在某些特定的領域中使用的基礎的類或組件。這些元素構成了一個系統(tǒng)的通用層次。這個層次也是分層時需要考慮的。例如一些應用軟件中使用的一些通用的Currency對象或是Organization對象。分析模式一書對此類的對象進行了充分細致的闡述。這個層次一般被稱為跨領域層(cross-domain layer),或稱為工具層(utility layer)。
目前的很多軟件都采用了數(shù)據(jù)庫映射技術。數(shù)據(jù)庫映射層對于企業(yè)應用系統(tǒng)非常的重要,因此也需要納入考慮之列。數(shù)據(jù)庫映射技術用起來簡單,但是要實現(xiàn)可不容易。如果不是非常有必要,盡可能使用現(xiàn)成的框架,或是采用其中部分的設計思路。試圖構建一個大而全的映射層次的代價是非常高昂的,認識不到這一點會帶來很大的麻煩。數(shù)據(jù)庫映射技術的知識,我們在下文中還有專門的篇幅來討論。
如何存放數(shù)據(jù)(狀態(tài))?
在學習EJB的過程中,最先要理解的一定是有狀態(tài)和無狀態(tài)的概念??梢哉f,整個概念是多層體系的核心。為什么這么說呢?這里的狀態(tài)指的是類的狀態(tài),例如類的屬性、變量等。由于狀態(tài)的不同,類也表現(xiàn)出差異來。而對于多層結構的軟件,創(chuàng)建和銷毀一個類的開銷是很大的,如果該軟件支持分布式的話尤為如此。所以如果系統(tǒng)的不同層次間進行頻繁的調(diào)用-創(chuàng)建一個類,再銷毀一個類。這種做法是非常消耗資源的。在應用系統(tǒng)的設計中,一般不單獨使用COM,就是這個原因。所以我們很自然的想到了一種經(jīng)典的設計-緩沖池。把對象存放在緩沖池中,當需要的時候從池中取出一個,當不需要的時候再把對象放入池中。這種設計思路能夠大幅度的提高效率。但是這對能夠放在池中的對象也提出了苛刻的要求-所有的對象必須是無差異的,也就是無狀態(tài)的。只有這樣才能夠實現(xiàn)緩沖池。
一般來說,對象緩沖池的技術是用在中間的業(yè)務層上的。既然中間業(yè)務層上不能夠保留有狀態(tài),那就出現(xiàn)了一個狀態(tài)轉移的問題。這里有兩種的選擇,一種是前移,把狀態(tài)移到用戶端,最典型的是使用cookie。這種選擇一般是由于狀態(tài)和用戶端有關,不需要長時間保存。另一種選擇是后移,把狀態(tài)移到數(shù)據(jù)層,由數(shù)據(jù)庫來實現(xiàn)持久性狀態(tài),當需要時才把狀態(tài)提交給業(yè)務層。這種方式是企業(yè)應用軟件中采用最多的,但是也增大了數(shù)據(jù)庫的負擔。
處理好接口
由于使用了分層技術,因此原先那種在CS結構中類之間存在復雜關系就有必要重新評估了。一般層間的耦合度不宜過大。因此需要慎重的設計層之間的類調(diào)用方式。一些分布式軟件體系(例如J2EE)對層之間的調(diào)用方式以接口的形式給出了要求。同時,不同層之間僅僅知道目標層的接口,而不知道目標層的具體實現(xiàn)。EJB的home接口和remote接口就是這樣。在COM+體系中,也需要在設計類的同時,把接口公布出來,以供客戶方使用。
在設計層間的接口時,除了考慮開發(fā)平臺的約束之外,還有一點是開發(fā)人員必須考慮的。那就是業(yè)務需要。業(yè)務層中往往有非常多的對象和方法,它們之間的關系也非常的負責,但對于其它的層次來說,它并不關心這些細節(jié)。因此業(yè)務層公布的接口必須要簡單,而且和實現(xiàn)無關。因此,可以使用設計模式的Facade模式來簡化層間的接口。這種做法非常有效,EJB中的SessionBean和EntityBean區(qū)分就含有這種設計思路。
同樣的,不同層之間的數(shù)據(jù)傳遞也存在問題。如果不同層的物理節(jié)點在一起還好辦,如果不在一起,那就需要使用到分布式技術了。因為不同機器的內(nèi)存地址編碼是不同的,如果接口之間采用對象引用的方式,那一定會出現(xiàn)問題。因此會將對象打包成字符串,發(fā)送到目標機器后再還原為對象。所有的分布式平臺都提供了對這種技術的支持,這里就不多說了。但是這種實現(xiàn)技術會對我們的設計思路產(chǎn)生影響,少量的數(shù)據(jù)直接使用字符串來傳遞,數(shù)據(jù)量大的話,就需要使用封裝了數(shù)據(jù)的對象。這類對象的設計需要非常的小心。在設計過程中可以參照開發(fā)平臺提供的一些標準做法。同樣的,數(shù)據(jù)的請求的頻率也是難點之一。過于頻繁的操作來自后端的數(shù)據(jù)會加大系統(tǒng)的開銷。因此,在設計調(diào)用方法時同樣需要結合實際應用來考慮。
兼顧效率
一般來說,純粹的面向對象設計者設計出的軟件都會比較完美。但是需要付出一定的代價。在一些大的軟件平臺上編程的時候,往往需要利用到平臺的一些機制。最典型的就是平臺的事務機制(最典型的包括J2EE平臺的JTS,以及COM+平臺的MTS),但是事務機制的實現(xiàn)往往需要平臺大量對象的支撐。這種情況下,創(chuàng)建一個支持事務的對象的開銷是很大的。處理這種問題有一種變通的辦法,就是僅僅對需要事務支撐的對象提供事務支持。這就意味著,一個單獨的業(yè)務實體類,可能需要根據(jù)是否支持事務分為兩種類:對該業(yè)務實體的select方法不需要事務的支持,只有update和delete方法才需要有事務的支持。這是不符合純面向對象設計者的觀點的。但是這種做法卻可以獲得比較優(yōu)秀的效率。
應該承認,這種提高效率的做法加大了復雜度。因為對于客戶端來說,它們并不關心具體的實現(xiàn)技術。要求客戶端在某一種情況下調(diào)用這個類,在其它情況下又調(diào)用另一個類,這種做法既不符合面向對象的設計思路,也增大了層間耦合度及復雜性。因此,我們可以考慮使用接口或是外觀類(參見設計模式一書中的facade模式),把具體的實現(xiàn)封裝起來,而只把用戶關心的部分提供給用戶。這方面的技巧我們在下面的章節(jié)中還會提到。
以迭代的方式進行分層
軟件設計中的迭代做法同樣可以適用于分層。根據(jù)自己的經(jīng)驗,在一開始就定義好所有的層次是很難的。除非有著非常豐富的經(jīng)驗,都則實現(xiàn)和原先的設計總有或大或小的差距。因此調(diào)整勢在必行。每一次的迭代都能夠對分層技術進行改進,并為后一個項目積累了經(jīng)驗。
這里的分層迭代不可以過于頻繁,每一次的迭代都是對架構的重大修改,都是需要投入人力的,而且會影響到軟件開發(fā)的進度。但是成功的迭代的效果是非常明顯的,能夠在接下來的開發(fā)周期中起到穩(wěn)定架構,減少代碼量,提升軟件質量的功效。注意,不要讓新潮技術成為分層迭代的推動力。這是開發(fā)人員都常犯的毛病,這并不是什么缺點,只能稱為一種職業(yè)病吧。分層迭代的推動力應該源自于需求的演進以及現(xiàn)有架構的不穩(wěn)定已經(jīng)妨礙了軟件進一步的開發(fā)。因此這需要團隊中的技術主管對技術有著非常好的把握。
重構能夠對迭代有所幫助。嗅出代碼中隱藏的壞味道并加以改進。應該說,迭代是一種比較激烈的做法,更好的做法是在開發(fā)中不斷的對架構、層次進行調(diào)整。但這對團隊、技術、方法、過程都有著很高的要求。因此迭代仍然是一種主要的改進手段。
分層的思路還可以適用于層的內(nèi)部。層內(nèi)的細分并沒有固定的方式,其驅動因素往往是出于封裝性和重用的考慮。例如,在EJB體系中的業(yè)務層中,實體Bean負責實現(xiàn)業(yè)務對象,因此一個應用往往擁有大量的實體Bean。而用戶端并不需要了解每一個的實體Bean,對它們來說,只要能夠完全一些業(yè)務邏輯就可以了,但完成這些業(yè)務邏輯則需要和多個實體Bean打交道。因此EJB提供了會話Bean,來負責把實體Bean封裝起來,用戶只知道會話Bean,不知道實體Bean的存在。這樣既保證了實體Bean的重用性,又很好的實現(xiàn)了封裝。
在前面的章節(jié)中,我們提到一個接口設計的例子。為什么我們提倡接口的設計呢?Martin Fowler在他的分析模式一書中指出,分析問題應該站在概念的層次上,而不是站在實現(xiàn)的層次上。什么叫做概念的層次呢?簡單的說就是分析對象該做什么,而不是分析對象怎么做。前者屬于分析的階段,后者屬于設計甚至是實現(xiàn)的階段。在需求工程中有一種稱為CRC卡片的玩藝兒,是用來分析類的職責和關系的,其實那種方法就是從概念層次上進行面向對象設計。因此,如果要從概念層次上進行分析,這就要求你從領域專家的角度來看待程序是如何表示現(xiàn)實世界中的概念的。下面的這句話有些拗口,從實現(xiàn)的角度上來說,概念層次對應于合同,合同的實現(xiàn)形式包括接口和基類。簡單的說吧,在概念層次上進行分析就是設計出接口(或是基類),而不用關心具體的接口實現(xiàn)(實現(xiàn)推遲到子類再實現(xiàn))。結合上面的論述,我們也可以這樣推斷,接口應該是要符合現(xiàn)實世界的觀念的。
在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 (); } |
可以看到,為了表示現(xiàn)實世界中人(這里其實指的是員工的概念)、工程師、銷售員、經(jīng)理的概念,代碼根據(jù)人的自然觀點設計了繼承層次結構,并很好的實現(xiàn)了重用。而且,我們可以認定該接口是相對穩(wěn)定的。我們再來看看實現(xiàn)部分:
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; } |
這是其中一種被稱為內(nèi)部標示(Internal Flag)的實現(xiàn)方法。這里我們只是舉出一個例子,實際上我們還有非常多的解決方法,但我們并不關心。因為只要接口足夠穩(wěn)定,內(nèi)部實現(xiàn)發(fā)生再大的變化都是允許的。如果對實現(xiàn)的方式感興趣,可以參考Matrin Fowler的角色建模的文章或是我在閱讀這篇文章的一篇筆記。
通過上面的例子,我們可以了解到,接口和實現(xiàn)分離的最大好處就是能夠在客戶端未知的情況下修改實現(xiàn)代碼。這個特性對于分層技術是非常適用的。一種是用在層和層之間的調(diào)用。層和層之間是最忌諱耦合度過高或是改變過于頻繁的。設計優(yōu)秀的接口能夠解決這個問題。另一種是用在那些不穩(wěn)定的部分上。如果某些需求的變化性很大,那么定義接口也是一種解決之道。舉個不恰當?shù)睦?,設計良好的接口就像是我們?nèi)粘J褂玫娜f用插座一樣,不論插頭如何變化,都可以使用。
最后強調(diào)一點,良好的接口定義一定是來自于需求的,它絕對不是程序員絞盡腦汁想出來的。
在各個層的設計中,可能比較令人困惑的就是數(shù)據(jù)映射層了。由于篇幅的關系,我們不可能在這個問題上討論太多,只能是拋磚引玉。如果有機會,我們還可以來談談這方面的話題。
面向對象技術已經(jīng)成為軟件開發(fā)的一種趨勢,越來越多的人開始了解、學習和使用面向對象技術。而大多數(shù)的面向對象技術都只是解決了內(nèi)存中的面向對象的問題。但是鮮有提到持久性的面向對象問題。
面向對象設計的機制與關系模型有很大的不同,這造成了面向對象設計與關系數(shù)據(jù)庫設計之間的不匹配。面向對象設計的基本理論包括耦合、聚合、封裝、繼承、多態(tài),而關系數(shù)據(jù)模型的理論則完全不同,它的基本原理是數(shù)據(jù)庫的三大范式。最明顯的一個例子是,Order對象包括一組的OrderItem對象,因此我們需要在Order類中設計一個容器(各個編程語言都提供了一組的容器對象及相關操作以供使用)來存儲OrderItem,也就是說Order類中的指針指向OrderItem。假設Order類和OrderItem分別對應于數(shù)據(jù)庫的兩張表(最簡單的映射情況),那么,我們要實現(xiàn)二者之間的關系,是通過在OrderItem表(假設名稱一樣)增加指向Order表的外鍵。這是兩種完全不同的設置。數(shù)據(jù)映射層的作用就是向用戶端隱藏關系數(shù)據(jù)庫的存在。
自己開發(fā)一個對象/關系映射工具是非常誘人的。但是應該考慮到,開發(fā)這樣一個工具并不是一件容易的事,需要付出很大的成本。尤其是手工處理數(shù)據(jù)一致性和事務處理的問題上。它比你想象的要難的多。因此,獲取一個對象/關系映射工具的最好途徑是購買,而不是開發(fā)。
分層對現(xiàn)代的軟件開發(fā)而言是非常重要的概念。也是我們必須學習的知識。分層的總體思路并沒有什么特別的地方,但是要和自己的開發(fā)環(huán)境、應用環(huán)境結合起來,你還需要付出很多的努力才行。
在完成了分層之后,軟件架構其實已經(jīng)清晰化了。
本博客為學習交流用,凡未注明引用的均為本人作品,轉載請注明出處,如有版權問題請及時通知。由于博客時間倉促,錯誤之處敬請諒解,有任何意見可給我留言,愿共同學習進步。