OO思維
經常看到不少人抱怨Java EE/J2EE中配置太復雜,煩瑣,不簡單易學,其實所謂簡單易學是取決於你是否有OO思維方式。
表現層的界面表單中通常是一些離散數據,也就是單個字段數據,通過Struts等框架提供ActionForm以及標簽庫,將這些單個字段數據封裝起來和業務層的Domain Model進行了映射,因此,表現層的主要編程工作就是映射配置。
持久層是將Domain Model對象保存到數據庫中,過去使用JDBC,我們要逐個打開這些Model對象,然後每個字段逐個保存到數據庫中,如果說表現層框架是實現離散數據封裝,那麼持久層實現的是反方向:拆封。Hibernate是一個持久層O/R mapping框架,也就是在對象和關系數據庫之間進行映射的框架,EJB的CMP也是類似道理,因此,持久層的主要編程工作也是映射配置。
表現層和持久層這種配置工作就如同打包郵寄一樣:你首先要將你的單件用一個箱子包裝起來,達到目的地,這個箱子被打開,單件被逐步取出。表現層和持久層這樣做的目的是保證中間業務層完全面向對象,他們都是和一個個對象打交道,而不是單件數據字段。
在一個真正面向對象的系統中,表現層和持久層主要工作就是配置,而且是映射mapping的配置。下面的問題就是:如何解決映射配置簡單而且易用,如果擁有正確的指導配置的思維,那麼配置工作就容易簡單多,否則,就倍感配置復雜。
其實那些感覺Java配置復雜的人其實他並沒有完整的OO思維。為什麼這麼說呢?以ORM(Hibernate)配置簡易方式說明:
配置的簡要之道
首先,配置是映射配置,顧名思義,也就是在兩者之間做協調,牽線搭橋,說白了,就是做紅娘,但和做紅娘又有些區別,做紅娘可以要求雙方做些改變,互相遷就,但是做映射配置,則不能這樣,因為那樣做就可能做出和需求要求不一樣的東西。
以持久層映射配置來說:雙方是指Domain Model對象和關系數據表,如果感覺在兩者之間配置映射很困難,雙方做些改變,但是有可能 需求不答應,你一旦為協調而作出的改變可能偏離需求實現的目標,最後作出的系統面貌全非,根本不是客戶所需要的。
那麼怎麼辦?很顯然,緊扣需求,反映需求的那一方堅決不要變動,那麼Domain Model和關系數據表哪一方反映需求呢?按照OO分析,當然 是Domain Model,Model對象我們是依據Evans Model等模型驅動設計MDD概念設計出來,他們是需求的代表。
很顯然,我們的映射配置必須順著Model對象這個思維來配,對於名詞式的Model,關聯無外乎是其主要關系,當然還有繼承,因此,象Hibernate 這些映射配置語法也是面向這些主要對象關系的。
表現層配置也是同樣的道理,需要將Domain Model配置成界面表單,在實際中,我們有可能采取的是通過界面收集需求,因此,這個映射配置過程也是考驗Model對象是否提煉正確與否,有可能發現Model不能實現一些界面需求功能,這時反過來必須修改我們的Model,而不是僅僅在表現層這個技術層面做些補救措施就糊弄過去。
Java EE/J2EE系統開發過程 敏捷的迭代是必然的。沒有一個天才能夠一步到位提煉出兼顧界面和數據表以及需求的統一模型出來。
總之,完成一個真正面向對象的Java EE/J2EE系統,必須抓住領域建模和具體框架熟練配置兩點,只有這樣才能保證Java項目成功實施。最關鍵的是提煉出反映出業務系統的領域模型:Domain Model,完成業務建模後,就是依賴Struts/Hibernate等配置分配將Model 映射到界面和數據庫,其實就是將業務模型移植到計算機領域並能夠正確運行。
高聚合和低關聯
如果一個系統都被設計成相互沒有任何不包含的單個對象,很顯然是不能正確反映實際需求的,萬事萬物都是有其部分組成的,例如窗戶由玻璃和框架組成,人是由胳膊 腿等身體部分組成,現實世界中,事物之間總是存在關系,聚合和組成是最常見的。
例如訂單,一個訂單Orders中由客戶名稱和地址,訂購的產品品種和數量,客戶名稱和地址我們可以抽象為Customer來代表,產品我們使用Product來代表,由於一個訂單中可能訂購了多個產品,很顯然,一個訂單對象中應該有多個Product對象,而且每個Product的數量不一樣,我們將Product和其數量再抽象包裝成OrderLine訂單條目對象,這樣,訂單中包含多個訂單條目,而且訂單條目只有依賴某個訂單,是其組成部分,是一種強聚合關系,不是普通的聚合或關聯關系。而Customer和Order之間是一種聚合關系,如果訂單沒有客戶信息,就不成為訂單了。
下面再以用戶User這個對象為例,用戶User可能擁有很多動態屬性,一些屬性需要運行時動態確定,用戶和動態屬性是一個整體和部分的聚合關系;每個用戶都必然屬於某個部門,因此,用戶和部門屬性對象之間也是一個整體和部分的聚合關系,這兩種聚合關系不同之處在於:前者一個用戶可能有多個動態屬性,是1:N關系;部門Dept和用戶User之間是1:N關系,一個部門中可能有多個用戶,反過來說,對於用戶User來說:它和部門Dept之間是N:1關系。
通過以上建模過程,我們基本搞清楚兩件事:這個領域中存在哪些模型對象?按照Evans的DDD理論,哪些是實體,哪些是值對象;然後我們必須搞清楚那些聚合關系,他們是整體部分的關系,用來共同組成一個完整對象的。
持久層Hibernate聚合實現
在持久層我們需要做的主要工作就是將上述Domain Model 進行持久化映射配置,以User為例,User是一個實體,我們配置User.hbm.xml如下:
< hibernate-mapping> < class name="sample.model.User" table="testuser">
< id name="userId" type="java.lang.String" >
< generator class="assigned"/>
< /id>
< property name="username" type="java.lang.String">
< column name="name" />
< /property>
< !--表示和部門Dept之間是一種多對一關系 -->
< many-to-one cascade="save-update" name="dept"
class="sample.model.Dept" column="categoryId" />
< !-- 表示和用戶屬性UserPropperty之間是一種1對多關系-->
< bag name="userProps" inverse="true" cascade="all" >
< key column="userId" />
< one-to-many class="sample.model.UserProperty" />
< /bag>
< /class>
< /hibernate-mapping>
在User的映射配置文件中,我們很自然地表達了上節Model之間的聚合關系,通過Hibernate配置,我們將模型對象之間的關系可以持久化保存到數據庫中了,也就是可以永久維持這種關系,實際上,現實世界中也是這樣的,部分和整體的關系是一直存在,除非這個整體這個對象不存在,而且修改部分對象內部值,必須通過整體這個對象。
在user配置中,我們並沒有去做任何關系數據表testuser的設計和設定,因為我們知道,當User.hbm.xml配置完成後,這個J2EE系統部署發布到J2EE容器中時,Hibernate會根據這個配置自動創建數據表testuser,數據表的建立已經是一個部署調試階段的、技術層面的具體工作。
Hibernate重要的父子關系
Hibernate在處理User和UserProperty這樣一對多的父子關系時,具體實現起來要有一些具體細節必須注意,而且Hibernate2和Hibernate3兩個版本是不一樣的:
當我們需要只通過一句話session.save(user)或session.update(user)就能完成User和它其中多個Userproperty都能自動保存或更新時(必須指定cascade="all" 或save-update),尤其是update(user)更新時,其子集合userProps屬性中可能有一些Userproperty是修改過的,一些Userproperty則是新增的,對於新增要使用insert語句;而對於修改則使用update語句,當我們籠統地調用一句update(user)時,那麼Hibernate是如何判斷這個user中子集合中哪些是修改?哪些是新增的?
Hibernate是通過主鍵來判斷的,也就是說,通過UserProperty的主鍵來判斷該對象是修改?還是新增。最關鍵的是:這個主鍵必須由Hibernate自動產生,如果你想自己指定子對象UserProperty主鍵,那麼就有可能很多麻煩,這個麻煩是出其的麻煩,無法判斷具體原因。所以,在簡單方便的道路上邁錯一步就是萬丈深淵。下面是UserProperty的映射配置:
< hibernate-mapping> < class name="sample.model.UserProperty" table="userprops">
< id name="propId" type="java.lang.String" >
< generator class="uuid.hex"/>< !-- 不能為assigned-->
< /id>
< property name="name" />
< property name="value" />
< !-- 為提升性能而設定 -->
< many-to-one name="user" column="userId" not-null="true"/>
< /class>
< /hibernate-mapping>
注意:以上配置只適合Hibernate 3.0以上版本,如果是Hibernate 2,那麼必須在 :
< id name="propId" type="java.lang.String" >
中加入unsaved-value="null",而且這個值是null還是0或-1,取決你的主鍵類型:
< id name="propId" type="java.lang.String" unsaved-value="null">
是不是感覺Hibernate2太麻煩了!在Hibernate3中,就沒有這個規定了,所以,如果當初使用Hibernate2來實現J2EE的oo簡潔實現之道,還存在技術上的困難和難點。
Hibernate2和Hibernate3在處理父子關系上,還有一個不同就是lazy設定上:Hibernat2缺省lazy是false,當通過load將User獲取以後,在session關閉以後,你可以直接通過user.getUserProps()方法獲得其中子集合;而Hibernate3則不行了,缺省lazy是true,在session關閉情況下,只有兩種方式獲得子集合
1. Open session in view,也就是在表現層一直打開持久層的session,這不但違背分層不干擾原則,而且造成數據庫連接一直打開,一旦出錯,有可能造成內存洩漏死機等問題。
2.在load父對象User時,調用Hibernate.initialize(user.getUserProps());強行裝載所有的子對象,這樣問題是:我們再也無法通過簡單一句load生成父對象User及其所有內部部分,而無須照顧其內部關系。
板橋實踐中總結方法是:根據當初EJB CMP的讀取模式,采取JDBC來讀取整個User及其部分子集合,缺點也是必須在Dao語句中打開User(破壞封裝),根據其內部結構從數據庫中獲取數據,這樣的好處是:我們可以使用統一的Hibernate模板來進行任何一個模型的持久化(不必為每個模型寫一套DAO實現類),而無須關心其內部結構了。
注意:Spring+Hibernate采取的是Open session in view方案,這也是這種架構在系統復雜時發生性能問題一個原因,J道性能板塊有多個這樣的求救貼。
使用Hibernate映射配置另外一個注意點就是:使用雙向關系可以提高性能,但是Evans DDD告訴我們,建模時盡量搞 單向關系,不要用雙向,這兩者有矛盾之處,實際中,我們如果使用Hibernate作為持久層框架,那麼就采取雙向,性能很重要啊,否則後果很嚴重,這種設計和性能不匹配也是目前面向對象領域需要解決的另外一個問題。
通過在子對象UserPropery配置中引入many-to-one ,然後在父對象User配置中規定inverse="true" 來實現雙向,Hibernate會通過和insert或update一條SQL語句完成關系設定。
表現層Struts聚合實現
前面我們完成了Hibernate的映射配置,下面是表現層的映射配置,這是使用標簽庫來實現,我們使用Struts的標簽庫來實現:在界面主要實現下圖效果:
當進行用戶User資料增刪改查時,需要一個如圖錄入頁面,部門是通過下來菜單選擇,用戶屬性UserPropery是通過一行行屬性名稱和屬性值輸入的,主要是在user.jsp中完成:
< html:form action="/userSaveAction.do" method='post'> < html:hidden property="action" />
< !-- 下拉菜單選擇部門,通過使用Struts的Action串聯,產生deptListForm新ActionForm-->
< html:select property="dept.deptId" >
< logic:notEmpty name="deptListForm" >
< html:optionsCollection name="deptListForm" property="list" value="deptId" label="name"/>
< /logic:notEmpty>
< /html:select>
< br>
UserId:< html:text property="userId" />
< br>
Username:< html:text property="username" />
< table>
< tr>< td>屬性Id< /td>< td>屬性名稱< /td>< td>屬性值< /td>< /tr>
< tr>< td>
< html:hidden property="userProp[0].propId" />
< /td>< td>
< html:text property="userProp[0].name" />
< /td>< td>
< html:text property="userProp[0].value" />
< /td>< /tr>
< tr>< td>
< html:hidden property="userProp[1].propId" />
< /td>< td>
< html:text property="userProp[1].name" />
< /td>< td>
< html:text property="userProp[1].value" />
< /td>< /tr>
< tr>< td>
< html:hidden property="userProp[2].propId" />
< /td>< td>
< html:text property="userProp[2].name" />
< /td>< td>
< html:text property="userProp[2].value" />
< /td>< /tr>
< /table>
< br>< input type='submit' value='submit'>< /input>
< /html:form>
相應的UserActionForm和User Model內容差不多,不同之處:為接受多個動態屬性的輸入,需要設定一個特定的方法:
public class UserForm extends ModelForm { .....
public UserProperty getUserProp(int index) {
return (UserProperty)((List)userProps).get(index);
}
.....
}
增刪改查和批量查詢根據JdonFramework的簡化可迅速配置實現,這裡不再描述,整個項目的代碼結果如下圖:也就是10個類左右,而且都是和業務有關,簡要,扣主題,整個案例代碼是免費自由下載,作為JdonFramework應用源碼下載之一的sample。
總結
一個真正面向對象的JavaEE或J2EE系統,應該是一個圍繞領域模型的多層架構,以面向對象OO思維進行領域模型提煉和重構,繼續以OO思維進行表現層和持久層的配置實現,才能尋找到一條Java系統快速有效高質量的解決之道。