終結DbHelper鬼畫符1:Scrum實戰演示
這個系列,記錄實際工作中基礎類庫開發的思維方式、類設計過程。額外的,也會提及實戰中如何使用Scrum類似的過程來組織自己的工作,以及在這類工作中如何使用Tdd方式。
這個世界充斥著一種叫做DbHelper的東西,微軟的Entlib已經堅持不懈更新多年了,滿世界都能夠搜索到不知是誰發布的各類DbHelper類的代碼,許多程序員都有自己的具備完全或不完全自主知識產權版本。這其實是件非常痛苦的事情,因為過多的選擇總會令人迷茫。
于是在某一天,我決定終結這種鬼畫符。
我首先思考,為什么要用Ado.net,為什么要用DbHelper?其實大家有許多選擇,Linq To Sql、Aef是一層包裹,社區有許多Orm工具,不過大多數程序員仍然使用原生的Ado.net。原因有兩種:性能問題,編程復雜度問題。從這個角度而言,說明微軟自己的Entlib、Linq To Sql和Ado.net Entity,這類努力并沒有帶來合用的東西。
進一步,為什么需要DbHelper?想少寫一些代碼也就是復用代碼,同時希望在多種數據庫之間無縫切換,這兩點是原生的動力。不過,這不是回答,真正的回答是,Ado.net本身的設計比較混亂。
一個良好的設計,應保證用戶,也就是使用這個體系的程序員,能夠非常簡易的使用。這體現在兩個方面,第一、概念要盡可能的少。第二、代碼要盡可能的少。理解這一點其實也是非常容易的,程序員為最終用戶提供的應用,需要易于理解、需要操作的步驟盡可能的少,這是對應的用戶體驗問題。
框架的設計和開發,最終用戶是程序員,也應該遵循同樣的原則。
我想做的事情,包括兩個層面,第一、易于理解:程序員只需要理解連接、命令、DbReader三者,就能夠便捷的應用Ado.net開發,這需要屏蔽DbProviderFactory、DbProviderFactories、DbDataAdapter和DataSet、DataTable之類概念。第二、簡單的定義實體和實體集合對象,從而在大多數情況下無需直接寫數據庫訪問代碼。
當然,這里的前提,是維持Ado.net的性能水平。
幸運的是,這項工作遠沒有想象中那樣困難。幾個月前,經過5個工作日,相對比較輕松的達成了上述目標。兩個地球人都沒有想到的方式,第一、自己定義一個繼承于DbConnection的類,解決消滅DbProviderFactory的問題;第二,使用索引器實現實體對象和DataRow的互相轉換,從而避免使用反射,確保性能。
結合代碼生成器自行創建實體類,多數情況下,無需編寫任何數據訪問代碼。
我們遇到的第一個問題,是DbProviderFactory的問題。
Db系列的類基本上是抽象類,舉例來說,SqlConnection和OleDbConnection都是繼承于DbConnection的,為了盡可能容易的在不同數據庫中切換,我們需要使用Db系列的類。由于是抽象類,當然無法自行實例化,你只能通過DbProviderFactory創建,無論是連接、適配器甚至參數,都需要通過它來創建。熟悉設計模式的朋友應該知道,這是提供者模式和工廠模式的混血兒,微軟那些年輕的小學究們還真是煞費苦心。
DbProviderFactory通過DbProviderFactories.GetDbFactory(“提供者名稱”)獲取。這樣,你需要在代碼中維持一個DbProviderFactory對象,然后,比較笨拙的做法是,在每個地方,創建Db系列的對象時,都用DbProviderFactory.CreateXXX之類的方式去做。我非常討厭這樣子,為什么討厭,你懂的:
這樣不是很好嗎?DbConnection connection= new DbConnection(數據庫類型,連接字符串);甚至,你的系統中如果不涉及到同時使用Oracle、SqlServer的情形,你可以將數據庫類型和連接字符串保存在配置文件中,這樣創建連接的時候連參數也無需考慮。
既不需要理解DbProviderFactory,也不需要理解DbProviderFactories,更不需要理解“提供者名稱”,我相信這些東西很難用自然的中文,來表達它是神馬意思。每次實例化的時候代碼也少了許多,這顯然符合我們概念少、做事少的目標。
目的明確,那么,如同阿Q先生所說,我們革命吧
我選擇的項目管理工具,比較懶惰。使用Team Foundation Server,使用微軟的Scrum 1.0模版,我們基于這個模版創建一個團隊項目,命名為Faster。按照慣例,應該先提出項目愿景、列出Product Backlog,這實際上就是Story、用例、用戶情景的集合,這些翻譯我不滿意,因此我簡單的翻譯成“項目目標”和“功能清單”。
項目目標:以最簡單的方式處理數據訪問,消滅絕大部分與數據訪問相關的代碼編寫任務。
1.簡化DbConnection的創建方式:工作量評估2 優先級1000
2.獲取數據庫中的元數據:工作量評估5 優先級2000
3.自動生成Crud命令:工作量評估3 優先級3000
4.實現數據訪問泛型類:工作量評估8 優先級4000
5.制作代碼生成器:工作量評估3 優先級5000
6.通過外鍵處理一對多關系:工作量評估2 優先級6000
項目目標定義清晰是非常重要的,兩句話,解決“我們這個項目要做什么”的問題。
功能清單,必須使用用戶語言,每一項功能都是滿足用戶的一項特定的需求。這里非常簡單的只列出標題,實際工作中,對于每一項功能,我僅僅是寫一段兩百字以內的介紹,絕不愿意書寫非常龐大的文本,那既沒有必要、程序員也不會真正用心去看。注意標題的書寫格式,沒有主語,動賓結構,形如“做--什么”,其中優先級應由用戶決定,工作量評估則是團隊程序員通過討論決定。這里工作量評估的單位是一個相對的單位,你可以將其看成“理想工作日”,只是衡量每項功能相互之間的大小。我首先在功能列表中找出最小的第一項,將其定義為2,第二項我認為它的工作量應是第一項的2倍到3倍之間,定義為5。
然后,我們將這些功能的“迭代”也就是我翻譯的階段,設置為第一個版本的第一個Sprint,嗯,有些拗口。我們簡單些:我認為這些任務在一個階段就能夠完成,無需劃分為多個階段。如果是比較大的項目,此時應根據優先級和工作量評估,將其劃分成多個階段,保持每個階段的工作量總和大體均衡。微軟Scrum模版中的版本概念不需要考慮。
現在,打開Product Backlog,什么都沒有了。
面對你的第一個Sprint,也就是第一個階段,剛剛那些功能都轉到這里了。
接下來,我們為每一項功能劃分“任務”,舉例來說,對于第一項功能,簡化DbConnection的創建工作,我將其劃分為如下的工作任務:
1.創建測試數據庫 優先級1005 需要1小時
2.繼承DbConnection,創建Db類 優先級1010 需要2小時
3.創建連接:優先級1015 需要1小時
4.使用配置文件中的默認連接字符串,創建連接:優先級1020,需要1小時
5.實現其他2種創建連接方式:優先級1025,需要1小時
總體需要6小時的時間,一天的工作量。可以看到每項工作任務的狀態,都是To do…分配給誰,還是空白。
那么,我先將第一項任務,分配給自己。當然如果是多人工作的話,為每個人分配一項任務。然后,我準備開始做第一項工作了,將其狀態設置為in progress。
必須注意,團隊中每一個程序員,在任何時刻只面對一項任務,這是關注點的問題。
那么,開始創建測試數據庫。
首先,在這項工作任務的描述欄目中,已經有任務內容的簡要描述。那么,先按照這樣的格式寫一句話:10月3日 8:50開始,預期1小時。之后記錄工作內容,結束后加上結束時間和中途中斷的時間。這只是個人的工作習慣。
首先建立一個Sql Server數據庫,我創建了三個表格,Post、Tag、PostForTag,看起來很眼熟,嗯,這是一個簡易的微博數據庫,只處理發布微薄,沒有用戶系統也沒有評論、轉發之類的功能。我們為Post增加一個Image字段,這當然是為了測試方便,但這也讓Post表格編程一個可以分類處理相片的東西。
然后,我們為三個表格定義外鍵關系,由于Post是數據量比較大的表格,那么同時也為其中幾個經常會用于查詢的字段定義索引。
為這個數據庫創建腳本,將這個腳本加入到測試項目,這樣同時也能夠對數據庫腳本進行版本管理。
另外我們加入一個簡單的Excel文件,這個也是有必要的,測試OleDb,我們至少需要驗證兩種不同的Db對象,工作是否正常。
既然有創建測試數據庫這項工作任務,這說明我們在寫單元測試的時候,沒有使用Mock對象。一向很排斥這個,因為Mock實際上增加了單元測試工作的工作量、邏輯也變得復雜,而帶來的好處相當有限,這與“簡單”的宗旨是不合拍的。
這項工作如期結束,將任務狀態改為“Done“,將第二項任務的狀態改為in progress…
最初我企圖用擴展方法解決問題,不過,由于擴展方法的語法不支持屬性和字段,我決定使用原始的方法。定義一個類Db,繼承于DbConnection。當然,要實現這個類需要覆蓋抽象類的許多方法和屬性,這是比較艱難的工作。不過,我用一種很奇怪的方式規避了這類工作。我在這個類里,加入一個私有的DbConnection類型的字段connection,所有需要覆蓋的方法、屬性和事件,都傳遞給這個connection去做,這就如同抄書,幾分鐘就搞定了一個自定義的DbConnection類,當然,這不是一個抽象類。在這個類里,同時加入了一個DbProviderFactory類型的私有字段,私有,所以使用該類的程序員是不能感覺到DbPRoviderFactory的存在的。
嗯,這就是所謂的封裝。所謂面向對象,其實百分之七十以上的場景,都只是運用“封裝”。屏蔽內部細節,提供簡易的服務接口。
不過,我實際上做的工作,是消除Ado.net中的提供者模式和工廠模式,換句話說,是在替微軟的Ado.net打補丁,將他們高深的設計模式知識粉碎掉。由于工作性質齷齪,工作內容低級趣味,所以在這里談及面向對象還是多少有些慚愧的。
第二項任務就這么忽悠過去,沒有必要寫任何單元測試,這只是抄寫的工作。
開始做第三項任務,先從全局歸納一下,我們需要為Db類提供三種構造方法,用于創建連接:
Db():使用配置文件中默認的提供者和連接字符串創建連接,我們簡單的將這個配置項命名為"ApplicationServices",因為Asp.net 和Asp.net Mvc項目中都包含這個配置項。
Db(連接字符串,提供者名稱)
Db(配置項名稱):根據配置文件中某個配置項,獲取連接字符串和提供者名稱,創建連接。
另外,桌面應用中,我們常常在程序運行的整個周期,使用唯一的一個連接,當然,我是指類似Sqlite一類的桌面數據庫。那么,我們為其提供一個靜態屬性Default,這個屬性返回默認的Db對象。
這樣,多數工作場景,你可以通過如下兩個步驟,使用這個連接,
第一、在配置文件的ConnectionString節,定義連接字符串和數據庫類型,保持此前同樣的語法:
<add name="ApplicationServices" connectionString="Data Source=.;Initial Catalog=MiniBlog;Integrated Security=True;"
providerName="System.Data.SqlClient" />
<add name="RunTime" connectionString="Data Source=.;Initial Catalog=MiniBlog;Integrated Security=True;"
providerName="System.Data.SqlClient" />
第二個RunTime,是訪問Excel文件的用到的連接字符串。大家應該看出,這是在單元測試項目的App.config中使用的。
這里要關注一下,在Vs2010結合Tfs的情形下,怎樣寫單元測試是最輕松的。
一般的流程是:先寫單元測試、讓代碼編譯通過、寫代碼實現讓測試通過、寫下一個單元測試。這會導致在寫單元測試的時候,代碼語法自動提示是無法工作的。
我這么做:先在類圖中添加方法,然后在這個方法中“創建單元測試”,再寫單元測試,再實現。然后下一個方法或屬性。這個過程中,永遠不要對私有成員進行單元測試,雖然Vs2010也能夠通過創建訪問器來測試私有成員,但那個毫無意義。對public成員的測試必然會覆蓋私有成員,如果私有成員邏輯很復雜,那就需要重構。
下一篇將描述第三項工作任務的具體過程,用Step By Step的方式講述Tdd的工作方式。 類圖如下:


浙公網安備 33010602011771號