應用程序框架 Application Framework

轉自:http://www.cnblogs.com/BigTall/archive/2007/12/06/985101.html

應用程序框架設計之前言

要做一個應用程序框架的念頭Bigtall在幾年前就有了,因爲在工作中發覺很多方面非常的不順手,幾乎每一個環節都存在這樣或者那樣的問題:

  • 公司不同項目組做的設計是完全不同的風格,而且設計做不細,導致項目計劃越來越流於形式
  • 各層代碼凌亂,從後臺的java或者c#到前臺的html,天馬行空,隨心所欲
  • 數據庫結構和文檔不匹配,要不是莫名其妙的多、少字段,要不就是些莫名其妙的名字

如果深入到設計方面,就會發現雖然做過很多的項目,但是幾乎沒有可以參考的成功案例,所有的設計哪怕是同類型項目,也幾乎要重頭開始;編碼方面就更加麻煩了,我最深刻的印象就是從業以來我編制了無數的字符串處理函數,有c/c++版本的,有C#版本,有java版本的,還有delphi和早已忘記的vb,當然,更少不了時下流行的js了(這還是好的呢)。一旦到了項目規模較大的時候,我們甚至會在代碼流程中迷路,不知道調用從哪裏來,不知道邏輯要往哪裏跳。

這麼多的問題,是廣大的處於CMM 1級的軟件企業共同存在的問題,當然也會出現在那些CMM2345實際還是CMM1的軟件企業中。加入管理手段(比如真正去實施CMM規範)可以解決大部分的問題,準確地說是可以解決幾乎所有的管理問題並緩解大部分的技術問題,但是解決不了所有的問題。

以前bigtall在的公司曾經出現過這種論調“技術是不重要的,市場和管理纔是最重要的”,最後公司走入了低谷,好不容易聚集的人才也散了。別人的教訓就是自己的經驗,bigtall後來進行深刻的反思,認爲這裏有一個“度”的問題,我們先看兩個極端“只有市場和管理但是沒有技術”“只有技術沒有市場和管理”的後果,一個是“守不住”,一個是“推不了”。其實兩者是相輔相成的,技術可以把市場的門檻推高,市場和管理可以讓技術更順利地發展。所以一個公司沒有“最好”的技術,只有“最適合”的技術。一個公司的技術目標應該也只應該是“不斷降低公司的成本”。

最明顯的就是項目系統設計的重用問題,無論你是CMM多少級,如果沒有一個統一的軟件架構,重用就會打折扣甚至根本就無法重用。就像南方人北方人溝通要用普通話,今人和古人溝通就必須用相同的文字(順便鄙視並可憐一下朝鮮、韓國和越南)一樣,前後的項目要能夠溝通交流,他們就需要一個相同的結構。所以“程序框架”就是公司項目之間的“普通話”,也是項目內部的“潤滑劑”。

這裏要明確一下“程序框架(Framework)”和“軟件架構(Architecture)”之間的區別,簡單地說,程序框架是一個可以實際應用的代碼集合,而軟件架構可以看作是一種抽象的設計。另外,兩者的關注面也不一樣,程序框架熱衷於解決實際運行過程中的問題,甚至會提供工具包或者輔助模塊(比如權限系統、日誌系統等),而軟件架構則集中精力在各個部分(模塊、組件等)之間的關係。兩者其實是關於同一個事情的不同層次的東西。

我們可以來看一下如果存在程序框架會讓文章開頭的幾個問題解決多少:

  1. 如果有程序框架,因爲項目的WBS幾乎是一致的,所以上一個項目的項目計劃可以直接拿過來使用,而且經過幾個項目之後,這個項目計劃的模板會越來越細,越來越實用。
  2. 設計過程中,除了需求之外,概要設計、詳細設計的重用性會很高。
  3. 因爲結構一致,代碼混亂性會降低到可以接受的程度,而且可以重用上一個項目的大部分代碼。而且邏輯清晰,使得代碼相對較小,不容易在代碼中迷失。因爲代碼邏輯簡單有序,所以測試起來會很容易。
  4. 降低管理成本,除了項目計劃之外,測試計劃,QA計劃等等都可以重用,如果用CMM規範,項目評審的速度和質量都會有提高。

要做一個應用程序框架並不是一件容易的事情,一是這方面發展很快,而且水平相對較高,如果沒有獨到的解決方案,可能自己的框架一出來就過時了。二是應用程序框架涉及的方面較多,考慮清楚並不是一件容易的事情。三則是開發一個應用程序框架需要耗費大量的時間,也就是意味着公司需要花費(你的工資×開發時間+其他)這麼多的金錢。

對於小公司來說,除非有着清醒的認識,否則開發自己的應用程序框架可能是自尋死路。bigtall建議小公司如果.net開發,則直接用Petshop4的框架,如果java開發,則直接使用appfuse進行開發,如果想要一個新的語言,則直接用Ruby On Rails開發吧。對於php沒有建議,不太熟。

現在如果要開發一個應用程序框架,至少需要滿足如下的幾個條件:

  • 分層(廢話)
  • 易於測試
  • 易於擴展,並跟現有的其他系統進行無縫整合。一般要具有AOP、IOC、DI能力。
  • 易於部署
  • 具有權限,用戶管理能力
  • 有組織架構、工作流、規則引擎支持
  • 有頁面流轉,狀態管理能力
  • MOF或者MDA能力
  • SOA能力
  • 界面技術的抽象能力(如WebForm,HTML,Ajax,Xml,Rich Web等)

bigtall的應用程序框架設計系列文章準備從簡單開始,一步一步記錄並呈現給大家bigtall開發框架的整個過程。文章列表如下:

  1. 多層應用之間的數據傳遞: 分層和層間數據傳遞(上)、分層和層間數據傳遞(下)
    多層架構已經成爲了標準,但是數據在穿越各個層次的時候,它的形態有什麼變化和規律呢?
  2. 界面層的分析和抽象
    界面層承載了應用程序所有的可視部分,究竟裏邊還有多少我們需要關注但是卻忽略了的事實?
  3. MOF/MDA的支持和程序的設計規範
    如果人在根本上是不可靠的,那麼就儘可能不用人去做。但是到達這一步,我們還需要告訴機器一些什麼東西?
  4. 工具!我的工具!
    bigtall設計的小工具
  5. 待續


應用程序框架設計之二:分層和層間數據傳遞(上)

上一篇:應用程序框架設計之前言

還記得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的問題。

應用程序框架設計之二:分層和層間數據傳遞(下)

上一篇:應用程序框架設計之二:分層和層間數據傳遞(上)

看了上篇之後大家的留言,好多人覺得DO分這麼多形態,給這麼多名詞,可能在實際中沒有用處。其實相比.net而言,java在架構上的功力要深厚許多,要談架構如果避開java不談的話,就會膚淺許多。這一點上net可能還要許多年才能趕上(如果不加倍努力,恐怕永遠就落後於java了)。至於說VO、BO、PO沒有人分那麼仔細,恐怕只是大家自己沒有意識到自己在使用吧。正好下篇要對流行的架構進行分析,bigtall就斗膽show一下分析結果了。

針對在DO的形態轉換問題,bigtall選擇了幾個流行的架構進行了分析,主要就是想要看看他們是怎麼做的,這幾個架構分別是Petshop 4.0, Struts, Tapestry, Spring MVC。

首先我們看Petshop4,項目中包含22個子項目,我們按照三層架構的層次分類對這些子項目歸類:

顯示層:WEB  CacheDependencyFactory ICacheDependency  TableCacheDependency

業務層:Model BLL IBLLStrategy IMessaging  MessagingFactory MSMQMessaging OrderProcessor

存儲層: DALFactory IDAL DBUtility  OracleDAL    SQLServerDAL

權限相關的獨立部分:SQLProfileDAL  ProfileDALFactory OracleProfileDAL IProfileDAL Membership Profile

大家注意業務層的Model,裏邊定義了項目中使用到的所有數據對象,典型的BO。因爲asp.net的組件化設計思想,導致沒有明確的VO概念(被分散在諸如textBox1.Text中了)。但是我們看WEB項目中的AddressForm自定義控件代碼:

    public partial class AddressForm : System.Web.UI.UserControl {
        public AddressInfo Address {
            get { ....
                string firstName = WebUtility.InputText(txtFirstName.Text, 50);
                ......
                return new AddressInfo(firstName, lastName, address1, address2, city, state, zip, country, phone, email);
            }
            set {
                if(value != null) {
                    ...
                    if(!string.IsNullOrEmpty(value.FirstName))
                        txtFirstName.Text = value.FirstName;
                    ...
                }
            } 
        }

    }

分明就是一個典型的VO到BO之間相互映射的代碼。同樣我們看同一project下的CheckOut.aspx.cs也存在類似的轉換代碼:從WEB界面控件中提取數據,構建OrderInfo,最終傳入SQLServerDAL或者OracleDAL的Order類中,大家可以看到如下的代碼:

        public void Insert(OrderInfo order) {...
            orderParms[0].Value = order.UserId;
            ...
            orderParms[19].Value = order.AuthorizationNumber.Value; 
            ...
}

這個同樣是一個典型的BO到PO的轉換過程,只不過我們用類似Hashtable的結構代替了自定義的PO對象而已。

參考文獻:Microsoft .NET Pet Shop 4 架構與技術分析

接下來我們來看Struts。所有的WEB提交數據被放置到所謂的ActionForm對象中,很多人爲了方便,直接自定義了一個類似Hashtable的結構來做通用的ActionForm了。這個ActionForm就是我們所說的VO。然後ActionForm傳遞給Action進行處理,一般Action都會把ActionForm內容作一次校驗,然後構建BO,傳遞到Service層進行處理,Service層進行處理之後,調用DAO對象存儲。因爲java程序基本都使用了hibernate或者ibatis等模塊,所以BO到PO的轉換被封裝掉了。

這裏很多人使用struts或者其他java框架的時候,經常在Action中添加了過多的業務邏輯代碼,把原本屬於界面層後端的Action做成了業務層的東西,然後圖方便對Service層代碼只是做一個簡單的轉發調用,類似boolean XXService(XXBO bo) { return dao.save(bo); },實在是大錯特錯了。

說明:bigtall並不認同參考文獻中認爲的Action屬於業務邏輯層。我認爲業務邏輯層判斷的一個標準是不加修改或者加一個簡單的wrap,就可以暴露服務作爲SOA。Action顯然不滿足這樣的要求。退一步如果非要說Action屬於業務邏輯層,那也只能是一個專門針對struts的Service封裝接口,不合適包含大量的業務邏輯代碼。

Struts返回數據到界面層的方法是通過把BO填入到一個Hashtable結構,由界面jsp直接使用其值,就跟asp用法一樣。

參考文獻:Struts,MVC 的一種開放源碼實現

Tapestry框架是一個和asp.net採用了相似設計思想的組件化的web框架。一個web請求提交到服務器的時候,tapestry把請求中的內容填入到頁面對應的BasePage派生類對象的屬性中,這是一個自動的VO填充過程(類似asp.net中把用戶輸入的內容填充到對應的TextBox對象的Text屬性中)。然後這個BasePage派生類對象把自己的屬性最終填充成一個BO,傳遞到Service層,Service層調用DAO對象通過Hibernate或者ibatis存儲到數據庫中。

返回數據到界面層使用ognl表達式,基本原理類似把BO或者VO填入Hashtable結構,然後酌情用ognl表達式選取。比asp/jsp用法要利索一些,因爲是組件化,所以很整齊。

參考文獻:瞭解 Tapestry,第 1 部分瞭解 Tapestry,第 2 部分

Spring作爲No.1的AOP框架,靈活性和可擴展性是它最大的優點。在Spring MVC框架中,web請求通過參數HttpServletRequest(類似一個Hashtable結構)存放所有的用戶請求數據,傳遞給Controller處理。如果Controller是從SimpleFormController派生而來,則可以在jsp中使用bind機制自動把提交數據填充到一個指定的對象中(也就是VO了),否則就要手工從HttpServletRequest中獲取。在Controller中可以把數據傳遞給Service層處理了。Service層的處理和其它java框架相同。

返回數據到界面層可以使用很多種方法,看使用不同的ViewResolver而定,可以用jsp,也可以用freemarker腳本或者velocity腳本,也可以自己定義一種新型的界面層描述。

參考文獻:一步一步開發Spring Framework MVC 應用程序

從以上簡單幾個架構的分析,我們可以明顯看出VO/BO/PO的相互轉換過程。但是都有一個特點,就是對VO轉BO有明確的處理和包裝,但是對BO轉VO忽略掉了,直接使用暴露BO對象,使用ognl或者其他技術直接取值。asp.net的WebForm相對複雜一點,但是也同樣避開了VO的問題,但是賦值放到了類代碼裏邊,靈活性相對少了一些。而BO轉PO的問題,都傾向於用類似ORM的模塊來處理。

DO形態之間的轉換講了一大半,但是一個很實際的問題需要我們來面對,就是數據庫ID的暴露問題。根據我們的理論,ID實際是屬於PO的東西(以下簡稱POID),其實VO和BO中並不需要這個POID,另外就是暴露這個POID之後會存在很多的隱患,一旦程序檢查不嚴格,很容易被人假造一個請求去修改不應該的數據。但是我們真的可以拋棄POID不用嗎?bigtall同樣用一個例子來說明。

bigtall依舊使用上篇的LoginInfo的例子,不過這次的場景是查詢特定的LoginInfo並修改之。這個場景包含了如下的幾個過程:

  1. 輸入查詢條件LoginInfoQuery到服務層,並返回LoginInfoBO[]對象數組。
  2. 展示LoginInfoBO的數據在界面層,並等待修改
  3. 保存界面層提交的修改之後的LoginInfoBO到數據庫

這裏就暴露了一個問題,如何讓系統瞭解第3步和第2步的LoginInfoBO就是同一個對象?同樣問題也存在BO和PO的轉換中,如何把特定的BO轉化爲特定的PO?這個也就是我們現在爲什麼擺脫不掉這個POID的根本原因了。一句話,沒有POID,我們無法解決對象映射的問題。

真的我們只能通過POID來實現對象映射嗎?不是!我們有很多方法可以解決這個問題,只是不如直接使用POID來的方便。比如我們是不是可以用一個Hashtable來保存VP、BO和PO映射關係?當然可以,但是我想我們可以用更好的方法,因爲這個問題歸根到底就是對象唯一標識(以下簡稱OID)的問題。

要解決這個問題,我們需要兩個條件:一是對象有一個唯一的標識序號OID,二是保存VO和BO、BO和PO對象之間的唯一標識映射關係。直接使用POID可以很容易滿足這兩個條件,但是帶來了極大的程序風險,一旦界面層保存的POID被非法修改的話,程序對這方面的防範很困難,而且很多程序根本就是完全假設界面層POID是可靠的。但是如果程序應用在金融、財務等領域,操作人員就會極有可能有動機去修改這個界面層(尤其是瀏覽器中)的POID。而且從一般情況下他們會很容易推卸責任(程序bug嘛!要賠償也是軟件開發商賠償)。所以,可靠的做法就是避免把POID當作通用的OID,而是給每一個對象分配一個OID,同時保證OID之間的簡單映射關係。

bigtall給出的OID設計是這樣的:所有的DO對象都繼承接口IIdentitable,接口IIdentitable有唯一的屬性OID,對象構造的時候,由ClassFactory或者自身的構造函數自動給OID賦值,賦值的算法是這樣的:使用session id的簡單轉換作爲key,把POID加入一個校驗位(記得身份證號碼最後的X嗎?)之後的新POID用DES算法加密,這個加密之後的結果就作爲BO的OID,如果需要,同樣的步驟可以用作BOID到VOID的轉換中。用這個算法可以保證不同用戶的不同次登錄的session id是完全不一樣的,所以無法通過簡單複製獲得OID。其次要配合檢查程序,避免用戶查詢到不屬於自己業務範疇的數據,並儘可能對操作對象進行權限檢查。

至此,bigtall把DO的形態變化講完了,其實還有另外一個重要的概念,DO的設計。這個設計重要嗎?答案是很重要!請看bigtall的“應用程序框架設計之三:數據傳遞對象的類型和設計”。

---

2009-8-12 更正DO誤作DTO的問題。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章