何時使用分層技術?
分層技術實際上是把技術復雜化了。和以往簡單的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方法才需要有事務的支持。這是不符合純面向對象設計者的觀點的。但是這種做法卻可以獲得比較優秀的效率。
應該承認,這種提高效率的做法加大了復雜度。因為對于客戶端來說,它們并不關心具體的實現技術。要求客戶端在某一種情況下調用這個類,在其它情況下又調用另一個類,這種做法既不符合面向對象的設計思路,也增大了層間耦合度及復雜性。因此,我們可以考慮使用接口或是外觀類(參見設計模式一書中的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表的外鍵。這是兩種完全不同的設置。數據映射層的作用就是向用戶端隱藏關系數據庫的存在。
自己開發一個對象/關系映射工具是非常誘人的。但是應該考慮到,開發這樣一個工具并不是一件容易的事,需要付出很大的成本。尤其是手工處理數據一致性和事務處理的問題上。它比你想象的要難的多。因此,獲取一個對象/關系映射工具的最好途徑是購買,而不是開發。
分層對現代的軟件開發而言是非常重要的概念。也是我們必須學習的知識。分層的總體思路并沒有什么特別的地方,但是要和自己的開發環境、應用環境結合起來,你還需要付出很多的努力才行。
在完成了分層之后,軟件架構其實已經清晰化了。
本博客為學習交流用,凡未注明引用的均為本人作品,轉載請注明出處,如有版權問題請及時通知。由于博客時間倉促,錯誤之處敬請諒解,有任何意見可給我留言,愿共同學習進步。