應用程序框架設計之二:分層和層間數據傳遞(上)
上一篇:應用程序框架設計之前言
還記得97年左右開始的胖客戶機和瘦客戶機之爭嗎?之后又是CS和BS之爭,然后又是兩層和多層之爭...,十年之后的今天我們再回過頭看這些爭論,一切似乎看起來都那么理所應當:程序怎么能不分層啊?可是再想一下,原來我們用了整整十年的時間才達成了一個程序架構要多層的共識(效率多低啊)!
要分層,當然基本就是三層了,其實多層的基礎也是三層:界面層、業務邏輯層、存儲層。多層只不過在三層的基礎上把每一層或多或少再拆分出一些來而已,總的來說沒有什么大的變化。本系列文章中討論都以三層為基本概念。
本文著重討論的不是如何分層和層的定義,而是在分層情況下,討論層與層之間的數據傳遞問題。現在的程序很少仔細地去分析層與層之間的數據傳遞問題,通常都是一個對象從界面生成開始一路穿過,直接保存到數據庫(最顯著的標志當然就是xxxID了)。這樣的做法對程序傷害很大。
首先我們從一個簡單的例子開始:應用程序的添加用戶功能。界面很簡單,如下:
要為這個界面設計數據結構通常也很簡單,class LoginInfo{ public String name; public String password; } 就好了,然后我們在form提交的時候new一個并且填充好LoginInfo結構,就save(loginInfo)到數據庫里邊了,最常的做法還會加入一個int loginInfoID字段。我們把這種類似LoginInfo可以直接存儲到數據庫中的數據結構命名為Persistence Object,簡稱PO。嗯,看起來從頭到腳用一個數據結構并沒有什么問題啊!
問題會來的,bigtall來改變一下需求,通常我們需要給用戶密碼輸入兩次,所以界面修改如下:
這樣,form提交到服務器的數據結構就應該是這樣:class LoginInfo2{ public String name; public String password; public String password2; },然后服務器做的第一件事情就是比較password和password2是否相等,然后new一個LoginInfo結構,把name和password填充到里邊,然后保存到數據庫。我們同時把LoginInfo結構修改成這樣class LoginInfo{ public int loginInfoID; public String name; public String password; } 。
大家可以看到,隨著需求的變化,原來的“PO直通車”演化成了兩個結構,我們把LoginInfo2類似的界面層和其它層溝通的數據結構叫做View Object,簡稱VO。是不是這樣就夠了?當然不是,我們再來修改一下需求,給系統加入權限功能,所以這個添加用戶實際上應該修改成這樣:
我們需要繼續做一些改進(或者叫做“重構”吧),首先修改VO,同時我們把命名也規范一下:
class LoginInfoVO{public String name; public String password; public String password2; public String[] roles;},
然后把以前的LoginInfo拆分成三個類:
class LoginInfoBO{public String name; public String password; public RoleInfo[] roles;}
class LoginInfoPO{public int loginInfoID; public String name; public String password;}
class RoleInfoPO{public int loginInfoID; public String role;}。
至此,我們順利地引出了三個概念:View Object(VO)、Business Object(BO)、Persistence Object(PO)。他們分別是三層結構的顯示層、業務邏輯層和存儲層內部使用的數據結構,它們還有一個統稱,叫做數據傳輸對象Data Object(DO)。我們也可以把VO,BO和PO看成是DO在不同階段的不同表示形態。當一個DO從顯示層開始穿越整個系統的時候,它的形態和結構就開始變化,從VO轉變到BO,最終到PO,但是這個過程不一定是可逆的,這個過程如果反向,從PO->BO->VO,很可能就對應不同的對象了。比如當輸入錯誤的時候,回饋頁面可能就需要增加一個錯誤信息提示。雖然實際使用的時候,我們經常會忽略這種細微的差異性,實際上這個錯誤信息,只對顯示層有意義。
DO的轉換規律一般可以總結為如下的幾個類型,實際變化則可以是各種類型的組合:
- 屬性內容的減少
屬性內容的增減在DO不同形態之間的轉變時候經常會發生。比如上例中添加用戶LoginInfo對象的VO轉換到BO的時候,就需要丟棄“重復輸入密碼”的屬性。有些VO對象甚至根本不需要轉換成BO。在BO轉換成PO的時候同樣也會有屬性內容減少的情況出現,比如“部門”這類樹狀層次結構對象,因為運行效率的因素,也許會需要BO中有“下級部門列表”,實際存儲到數據庫的時候,PO只需要一個“上級部門ID”就可以了。
- 對象內容的填充或者增加
屬性內容同樣會有可能增加,但是在系統處理DO轉換的時候,屬性增加可能就意味著需要進行額外的查詢和填充,比如我們使用“用戶名”和“密碼”進行登錄的時候,最終系統需要通過數據庫查詢得到并且存儲“用戶ID”,以此來保證用戶的唯一性。又比如提交的數據存在校驗錯誤,我們可能需要重新刷新該頁面,并且增加新屬性“ErrorMessage”,以便把它顯示在界面上,提醒用戶注意。
- 對象的拆分和組合
我們可以看上面最后一個“添加用戶”的例子,一個LoginInfo的BO轉化為PO的時候被拆分成了2個對象,一個存放基本的用戶信息,一個存放對應的Role信息。通常對象拆分的時候,常常需要填充或者補足新對象的內容;而對象合并的時候,常常出現內容減少的情況。
- 對象或者屬性類型的變化
出現對象屬性類型的變化在VO到BO的轉換中比較常見,比如把用戶輸入的生日轉化為一個真正的DateTime類型。
- 屬性名稱的變化
屬性名稱在轉換過程中會有變化,一般這種情況應該盡可能不要出現,但是在項目重構的時候出現的概率較大。
除了DO不同形態之間的轉換規律之外,不同形態內部還有不同的工作要做:
- 校驗
“不要相信任何用戶的輸入”,這是設計程序跟用戶進行交互操作時候永遠需要遵守的一個原則。也就是所有的外部輸入都需要進行正確性的校驗。校驗器是分為兩個層次,一個是屬性層次的校驗,比如“年齡”只能0到150之間有效。另外一個是對象層次的校驗,或者說跨屬性層次的校驗,比如“年份輸入閏年的時候,2月可以有29日”等。
校驗并不是一個單純的問題,幾乎所有的業務邏輯校驗基本都需要一次完整的貫穿所有層次的調用。代價頗大。這個也是為什么我們在顯示層做很多事先校驗,而一旦進入業務邏輯層的時候,校驗就經常會被“事后校驗”代替了,人們會使用拋出異常的方法來代替“事前檢查”。
突然想起來有一句閑話要講。這個分析過程其實在一年前就完成了,那個時候正好沸沸揚揚的SOA滿天飛,當把這個DO形態分析完畢之后,回頭看SOA發現它并不屬于表現層,而是屬于業務邏輯層,換句話說它使用的DO必須是BO而不是VO。而所謂的SOA也不過就是分布的業務邏輯層而已。
因為以下的部分要花費較多的時間查找,bigtall怕文章擱久餿了,也怕各位看官等得太久,就分兩部分發吧。下篇我們著重分析現net平臺和java平臺的幾個架構在DO形態上的對比,還要談一個實用的問題,是不是需要對象ID的問題。
---
2009-8-12 更正DO誤作DTO的問題。

公眾號:老翅寒暑
浙公網安備 33010602011771號