2006年4月10日
#
原文
關于"貧血模型"的討論幾乎沒有停止過,在openfans.org的開發(fā)過程中,我們也討論了很久,我覺的有很多東西應該記下來:
明確一下意思先:
DAO:數(shù)據(jù)操作對象,會操作數(shù)據(jù)庫
持久層:能提供對象持久化服務的一系列組件或服務
領域對象:描述領域模型的對象,是通過業(yè)務分析進行系統(tǒng)建模的產(chǎn)物
貧
血模型:就是domain
object只有屬性的getter/setter方法的純數(shù)據(jù)類,所有的業(yè)務邏輯完全由一個所謂的Manager來完成(又稱
TransactionScript),這種模型下的domain object被Martin Fowler稱之為“貧血的domain
object”
常見的類基本結構如下:
一個業(yè)務數(shù)據(jù)類叫做Item,
一個DAO接口類叫做ItemDao
一個DAO接口實現(xiàn)類叫做ItemDaoHibernateImpl
一個業(yè)務邏輯類叫做ItemManager(或者叫做ItemService).
觀察上面的幾個類很容易發(fā)現(xiàn)問題:
1:Item和ItemManager實際是操作與數(shù)據(jù)的關系,實際完成的就是經(jīng)典OO中的一個對象的能力;
2:當有許多Item時 類組變得很龐大,產(chǎn)生很多 xxxDao xxxImpl xxxManager 其中包含大量重復代碼;
按<<重構>>的觀點,上述代碼存在以下臭味:
1:重復的代碼?? xxxDao xxxImpl xxxManager(通常)
2:霰彈式修改,一個變化影響多個類,類之間不夠高內(nèi)聚 item變化-->Dao,Impl,Manager均要變動
3:依戀情結,兩個類之間互相作用過多 item<->Manager
4:平行繼承體系,當增加一個新類時總是要增加另一個類
5:夸夸其談未來性,在沒有任何暗示的情況下考慮擴展? Dao,實際HibernateImpl可能n年內(nèi)是唯一的Dao實現(xiàn)
6:純稚的數(shù)據(jù)類,只有數(shù)據(jù)的類? item
我覺的 貧血模型 是系統(tǒng)分析設計方向性錯誤的產(chǎn)物:
1:沒有進行領域建模---以數(shù)據(jù)表結構為中心,而不是業(yè)務模型為中心的思考方式,使設計人員選擇Item為考慮問題的出發(fā)點
2:將DAO與持久層混淆---我們需要的一種持久化服務,DAO緊緊是提供數(shù)據(jù)操作能力而已,Hibernate是一種高級的服務(他已經(jīng)包含了DAO,而不是相反),已經(jīng)完成了所有的持久層服務.
3:過于強調(diào)低偶合---將一些本來一些提供單一職責的內(nèi)容分散在多個單元中使 客戶端 依賴更多的接口,而忘記了高內(nèi)聚原則.
4:Spring的能力限制---由于Spring現(xiàn)階段不支持對于領域模型的服務注入,使設計人員將操作和數(shù)據(jù)分開,并將領域變?yōu)镈ataOnly的.
? (Spring2.0將在很大程度上解決這個問題)
?
我認為良好的解決方案:
? 首先領域建模,建立領域模型-->合并前面所說的Item和ItemManager成為 domainItem;對于數(shù)據(jù)庫服務,
? 1:如果考慮領域層包含數(shù)據(jù)操作能力,則建立DAO并選擇其它好的DAO方案比如IBATIS或Hibernate之類的組件;
? 2:如果考慮將數(shù)據(jù)庫(或其他存儲界質(zhì))存儲考慮在領域之外成為持久層,
? ??? a:則或者對持久層框架同時建模,同時選擇合適的組件為持久層服務提供存儲服務(包括DAO--亦可選擇IBATIS/Hibernate組件),
? ??? b:或者直接使用Hibernate/JDO等框架實現(xiàn)持久化服務,領域層直接使用持久層服務,對領域對象進行持久化和反持久化(從持久層獲取以持久化的對象).
?
其他:
?
實際上,作為一種解決方案,所謂"貧血模型"的具體使用,并不會有太大的問題,尤其是使用一些代碼生成工具或已經(jīng)做好相應的基本框架時,很多軟件的核心價
值都在于對客戶提供的服務,而其內(nèi)部則成為黑盒,我們只要合理的解決業(yè)務問題,就是"王道"了,對于代碼的臭味,可以慢慢重構--這也需要成本呀.?
?
再其他:
有人說,我們的業(yè)務就是CRUD,領域模型只有數(shù)據(jù)類就足夠了.我覺的這是搞錯了方向------只有CRUD時,只有處理CRUD的那些類才有必要進行建模(他們才是領域模型),而所謂的User\Item等數(shù)據(jù)類則完全沒有必要進行建模,更不要談領域了.
貧血之外:
實際上,軟件\OO方法的外延大的很,更多問題與數(shù)據(jù)庫存儲無關(但也有貧血問題),所以建模才是根本,OO方法的原則才是我們必須掌握的.
2006年2月5日
#
SQLServer一個bug終于被我碰上了
我有一個表使用字符類型存儲數(shù)字值,想進行匯總計算:
sum(case when isnumeric(FieldName)=0 then 0 else cast (FieldName as numeric) end)
簡單試了一下沒有問題,可是今天數(shù)據(jù)中有一個 ’2.1234567E7‘ isnumeric返回1 cast 返回錯誤
嗚嗚。。。。
怎么辦......
2006年1月23日
#
當設置一個Schedule的startDate早于 new Date(),并且調(diào)度周期又觸發(fā)于startDate和new Date()之間時,就會立即觸發(fā)當前job。簡單的解決方式是將startDate設為new Date().
2006年1月21日
#
1:abator下載:http://ibatis.apache.org/abator.html
2:將abator安裝到eclipse中
3:此時可以新建一種文件類型:Abator for iBATIS Configuration File,建立一個
4:在 jdbcConnection 中設置要mapping的數(shù)據(jù)庫的jdbc連接
classPathEntry 是你的jdbc driver類路徑
5:javaModelGenerator,sqlMapGenerator,daoGenerator
分別設置 java dataObject、sql mapping文件和 DAO 接口、實現(xiàn)類的生成位置:targetPackage
目標包,targetProject:eclipse項目
6:daoGenerator 中可以設置屬性 type: ibatis 或 spring 指定生成的dao實現(xiàn)類是使用com.ibatis.dao.client.template.SqlMapDaoTemplate
還是
org.springframework.orm.ibatis.support.SqlMapClientDaoSupport
7: table 中 tableName 指定要處理的表名
可以有多個table
8:table中可以包含子元素 generatedKey: 使Insert方法可以返回值--由指定的column mapping
9:generatedKey中的sqlStatement屬性可以是獲取sequence的SQL,也可以是獲取自增值的SQL
比如:Oracle的 select theSequence.nextVal from dual
SQLServer的 SELECT @@IDENTITY as column_name
10:保存文件,選中文件,右鍵菜單選擇Generate iBATIS Artifacts! ok...
使用abtor生成的iBatis代碼出現(xiàn)xml解析錯誤的解決方案
如果按上述方式生成的代碼有xml解析錯誤: 請下載
這個注意,該文件名為Abator.rar.txt實際是一個rar文件,只是上傳服務器有文件類型限制 所以只好加了擴展名txt。
請去掉.txt后解壓。
使用
org.apache.ibatis.abator.core_0.5.1.jar
替換調(diào)你的 eclipse\plugins 的同名文件 即可。
然后重新生成代碼。 OK 應該可以咯....
我改了一點代碼,需要可以留言。
2006年1月20日
#
一個popupMenus Extensions,
objectContribution:objectClass*: org.eclipse.core.resources.IFile
在action的Class代碼中:
public void selectionChanged(IAction action, ISelection selection) {
StructuredSelection ss = (StructuredSelection) selection;
this.selectedFile==(IFile)ss.getFirstElement(); //此處拋出異常
}
上述代碼的異常非常奇怪:
根據(jù)的的跟蹤,ss.getFirstElement()返回值是File,該類實現(xiàn)了IFile接口,
而且我用 ss.getFirstElement().getClass().isAssignableFrom(IFile.class)返回是false;
真是奇怪!---有人知道為什么嗎?
另外在實踐eclipse plugin開發(fā)過程中也有幾個心得:(肯定能用,但未必最佳)
1、如果開發(fā)plugin,所有的依賴庫都要包含到 Plug-in Dependencies 中;而不能只是引入到工程中。
2、如何輸出到console:
MessageConsole mc=new MessageConsole("****",null);
IConsole[] cs=new IConsole[1];
cs[0]=mc;
ConsolePlugin.getDefault().getConsoleManager().addConsoles(cs);
mc.activate();
PrintStream out=new PrintStream( mc.newOutputStream());
out.println("*******.");
3、如何獲取依賴工程的輸出路徑:
selectedProject:當前工程---由用戶選擇
String[] ps= selectedProject.getRequiredProjectNames();
IWorkspace w= selectedProject.getProject().getWorkspace();
for(int i=0;i<ps.length;i++){
IResource r=w.getRoot().findMember(ps[i]);
try{
IJavaProject jp=new JavaProject((IProject)r,null);
File source=new File(jp.getProject().getLocation().append(jp.getOutputLocation().removeFirstSegments(1)).toOSString());
//作你的事情.....
}catch(Exception e){
//不是javaProject
e.printStackTrace();
}
4、如何使用進度Dialog:
Shell shell = new Shell();
ProgressMonitorDialog dialog = new ProgressMonitorDialog(shell);
IRunnableWithProgress thread = new SomeRunner(shell);
dialog.run(true, false, thread);
//=============================
private class SomeRunner implements IRunnableWithProgress {
public void run(IProgressMonitor monitor)throws InvocationTargetException, InterruptedException {
monitor.beginTask("一些信息", 數(shù)值-總工作量);
for(;;){
// 一些工作
monitor.worked(數(shù)值-已完成工作量); //實際中,我得情況不太相符,不明白,但差不多 :(
monitor.setTaskName("一些信息");
// 一些工作
}
monitor.done();
}
}
2006年1月19日
#
這個題目可能有些危言悚聽,使用Spring的同行不用害怕,只是因為工作中碰到一些問題,才偶然想到這個問題。
首先要明確兩個問題:
1、系統(tǒng)隔離原則:
即系統(tǒng)間依賴應該是清晰的,不因為一個系統(tǒng)的故障影響其他系統(tǒng),甚至整個系統(tǒng)。
2、簡單應用習慣:
普通程序員只會處理checked Exception,負責任的程序員會處理被調(diào)用的函數(shù)可能拋出的異常(根據(jù)源碼或javadoc)
我們碰到一個情況:
使用Spring的 init 特性初試化一個bean--一個使用Qutarz的調(diào)度程序。初試化過程中會拋出RuntimeException,從而造成Spring容器的整個初試化失敗。首先我們修改了程序,捕獲了所有異常,隨后在編碼指南中加入了一句話:"所有使用Spring-init機制初試化的類必須在init中捕獲所有異常:Exception"。因為只有如此才能保證整個系統(tǒng)不會因為局部問題而完全癱瘓。
我覺得這是Spring對異常處理的一個悖論:
正方:底層異常都封裝成Runtime的,經(jīng)過幾次包裝--->簡單應用習慣--->異常被拋出領域層(init場景下:由Spring處理)
反方: 一個類的失敗可以造成整個系統(tǒng)的失敗---->Spring被RuntimeException宕掉---->不符合系統(tǒng)隔離原則
當然充分的測試可能可以解決這個問題:但是要注意,測試只能證明系統(tǒng)有bug,不能證明系統(tǒng)沒有bug。
解決方案:
1、好的設計習慣:將應該隔離的系統(tǒng)隔離開-->使用不同的Spring配置文件.
2、接口層錯誤處理:在接口層應該盡量對可以處理的異常進行處理,然后以合理的方式傳遞給上層.
PS:在與人交互的系統(tǒng)中都應該給最終用戶合理的錯誤提示,所以表現(xiàn)層應該盡量捕獲非業(yè)務的RuntimeException給最終用戶更好的操作感受。
公孫龍,六國時辯士也。疾名實之散亂,因資材之所長,為“守白”之論。假物取譬,以“守白”辯,謂白馬為非馬也。
以馬作為進行問題域進行建模,已知存在白馬這種類型。顯然存在馬的超類,并且馬類包含一個屬性-顏色,是否需要建立白馬的子類呢?顯而易見的是,當馬的顏色屬性是白色時,馬的一些實例表達了一個白馬的特殊實例群(由此我們可以得知:白馬顯然是馬),根據(jù)里氏替換原則,子類型必須能夠替換掉它們的基類型,顯然在分析了馬的行為模式以后,我們可以得出結論:白馬可以替換馬。----!難道真的要建立白馬、黑馬、X馬的子類嗎?
我認為可以從以下幾方面進行分析。
1、類的職責(很大程度上等同于服務能力,操作方式):
設計一個類,首先要從類職責的分析入手,一個類要承擔響應的職責,反過來說同樣的職責應該由同樣的類承擔,否則會造成類泛濫,實例孤單的狀況。如果領域內(nèi)馬和白馬承擔同樣的職責,應該只建立馬一個類,不應該只見樹不見林,造成不抽象的類的產(chǎn)生。
2、類的行為模式(當類承擔響應職責時,如何擴展):
分析一個類要從類的行為模式入手,既然一個類要承擔責任,其承擔的責任表現(xiàn)方式是否一樣呢呢?這就是她的行為模式,這也是里氏替換原則主要起作用的地方,如果兩個類的職責相當,但行為模式不同則不能成為超類和子類的關系(比如"著名"的正方形不是長方形問題)。馬類,作為超類,基于一些特殊的行為方式:吃草,跑...,對于白馬她的行為模式和馬是一樣的,并沒有不同,所以白馬是馬而非馬的同根繼承子類。對于長方形和正方形都是具有相同計算面積算法(職責)的四方形的子類。
-------以下是關于此事的一些擴展分析-----------
3、子類的產(chǎn)生:
何時需要產(chǎn)生子類呢:是對其父類的職責進行擴展,白馬沒有對父類的職責進行擴展,所以不是馬的子類。首先子類要擴展超類,其次子類不能重寫或廢除超類的職責。
4、屬性,狀態(tài)的區(qū)別(類的域)
對于一些類,在狀態(tài)不同時,會有不同的表現(xiàn)(狀態(tài)機模式),所以,類的getter,setter的部分包含兩種不同的特性,對于屬于狀態(tài)的部分,是我們要仔細分析的,而"白"馬則屬于屬性類(非狀態(tài))的域, 一般來講,一個類的實例要能提供相應的差異服務(由于狀態(tài)不同)最好使用不變模式[生存周期狀態(tài)不變]或狀態(tài)機[生存周期有狀態(tài),但狀態(tài)不由調(diào)用者控制]來實現(xiàn)。
5、抽象類和接口
由于java的單根繼承特性,很多設計人員不敢定義抽象類為繼承樹根,一定要先定義馬的接口,在建立抽象馬,作為一種"準規(guī)范"無可厚非,但我認為這是不愿承擔責任的表現(xiàn),有行為的基類應該可以(必須?)從類定義開始,避免白馬類(一旦馬成為接口,白馬的產(chǎn)生就更加"名正言順"了)的出現(xiàn).將來如果發(fā)生變化可以通過重構(導出接口和使用委托),解決問題。
6、對象的創(chuàng)建(組裝)和使用應該分開
既然對象的狀態(tài)如此重要,屬性有有很大程度的不變性(白馬在構造時就用該是白的,并且一生不變),而騎馬的人不必要求馬的屬性(!),所以,我們應該將馬的構造和使用分開,使領域模型更清晰。使用一些Ioc容器,比如Spring就能很好的解決這些問題。
7、分析問題的領域
說了這么多,有一個問題;如果有一個馬的研究機構,專門對不同顏色的馬進行專題研究,馬的顏色可能會對馬的行為有很大影響,例如戰(zhàn)馬如果是黃色(綠色,哈哈)更利于偽裝,此時"白"可能是一個很關鍵的問題,顏色會影響到不同的偽裝策略,此時將白馬作為馬的一個子類則是必須的!所以問題域不同,類的設計就不同,生活中的問題域比較清晰(生物學家和廚師對馬的理解不同),而軟件建模時往往問題域混雜,這也是OO設計時比較困難的問題,所以分析問題域也是非常重要的設計問題。
過去一直在donews混跡,聽朋友推薦,此處高手云集,我也來湊湊熱鬧。:-)