本系列:
寫技術文件的難度太大了!數次刪改,都沒能滿意,所以我還決定,先寫出來,以後再逐步整理完善——否則可能這個系列都沒辦法寫下去了。這也算是借鑑了敏捷的思路,先寫再改,不斷迭代重構吧!
前面的幾篇部落格反響還不錯,但還有一個硬傷,“說了這麼多理論,能不能實踐?”講類似概念的文章不算多,但也不少了,但我一直沒能從中收穫太多的東西,反而更是雲裡霧裡的糊塗了。估計這主要是兩方面的原因造成的:我智商低,卻愛較真!
你說得得天花亂墜,我只信一點,眼見為實,“是騾子是馬,牽出來溜溜?”
按照你說的架構,把系統搭起來,跑起來,需求改上個幾百上千遍,高併發大流量衝一衝……咦,這樣一番折騰下來,沒被砸跨,系統千錘百煉之後,還百鍊成鋼繞指柔。那我才豎起大拇指,真是不錯!
我相信,按照DDD、TTD、敏捷開發之類的理念,一定有成功的案例,不然他們不會被站在巔峰的技術大牛們交相稱讚。但很遺憾,我這個野生程式設計師,沒機會融入那個圈子。
所以我就用了一個最蠻最笨的方法:我自己做一個系統,嚴格按照我自己對於這些概念的理解進行開發,看最後這條路能不能走出來?歷經五年甚至更多時間的摸索和實踐,我覺得我基本上是走出來了。
所以,如果你願意,就靜下心來,聽我細細道來吧。
尷尬
在確定了忘記資料庫的大原則之後,我們理應從業務層入手開始系統的搭建。
/*
為什麼不是從UI層開始?不要笑,我還真記得,有看到過對這種做法的總結和推薦,還有一個什麼專有名詞,大概就是“頁面驅動”之類的。
而且你靜下心想一想,我們很多的開發實際上就是這樣做的!確定方案之後,美工出效果圖,前臺切圖出靜態頁面,程式設計師改成動態的,一頁一頁的做。
任務考核就大概是這樣的,“我們今天把某個頁面做完”。這種做法的好壞利弊我們就不展開了。但如果你一定要一個不從UI層開始的理由,我覺得最有力的就是:我們系統要做三個版本,電腦桌面頁面、手機頁面和手機APP。
*/
業務層裡,通常我們就把需求裡的一些名詞拎出來,做成一個一個的類,以創業家園為例,就應該有一個部落格類(Blog),部落格裡還有方法,比如GetBlog(int Id),或者GetBlogs(int pageIndex, int pageSize),如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Blog { string Title { get; set; } string Body { get; set; } Blog Get(int Id) { return new Blog(); } IList GetBlogs(int pageIndex, int pageSize) { return new List() { }; } } |
這是我最開始接觸三層架構時業務層類的樣子,寫在書上的。
但我就感覺這種做法特別彆扭!一個部落格物件取出10篇部落格,一輛汽車具有提供十輛汽車的能力。這都是些什麼亂七八糟的東西?不通啊……
我曾經想過將所有的Get()方法設定成靜態的,這樣從邏輯上說稍微通暢一點:通過部落格類可以獲取一些部落格例項。但還是不爽,類的靜態方法就喪失了物件的繼承多型等特性。比如,取10篇文章,和取10篇部落格就無法重用。
後來我才慢慢明白了,這種做法其實還是來自於“資料庫驅動”的思想。Blog類其實代表的是資料庫中Blog表,一個Blog例項就代表著一行資料,然後通過該表取到一些行,這些行又被封裝成Blog類(細究起來還是很亂,是吧?)。估計當初微軟DataSet的流行加劇了這一現象,當然DataSet本身沒有問題,它的邏輯是自洽的;然而有很多開發人員不認可DataSet,說它效能低,要用DataReader,自己“封裝”,結果不知怎麼的,就搞成了上面那種樣式的“四不像”。
Entity
上述傳統的業務層架構,除了邏輯上的混亂以外,還有一個很大的問題:難以測試!和資料庫攪在一起,怎麼測試?我是頭都大了。我得去做一個小型資料庫啊?而且這個資料庫還得insert/update之類 的,測試的基準資料就會變,所以每一次單元測試都得tear down(回到基準測試環境),這個又怎麼搞?
//當然,後來我還是找到了混合資料庫的測試方法,但我很高興當時我對資料庫的測試完全絕望的狀態。因為這促成了我的“忘記資料庫”的構想和實踐
所以我就在想,能不能把資料庫的操作隔離出來?這個時候,我應該是已經開始接觸ORM了,他們的操作方式給了我啟迪:關聯式資料庫的“增刪改查”中“改”沒了。改(update)被“異化”成:取出(Load) -> 修改 -> 再儲存(Savae)的過程(可參考《忘記資料庫》中的例子)。所以,我們是不是就可以首先把“改”獨立出來?通過不斷的演化,我最後形成了一個Entity的project,負責且僅負責物件狀態的改變,而完全不涉及物件的載入儲存等功能。
這樣做最大的好處,就是解決了Entity的單元測試的問題。由於(至少是暫時)不再需要考慮這些物件和儲存問題,那麼在測試的時候,我需要一個物件,只需要直接new一個就行了,而不是從資料庫裡取,這多方便啊!
Query(Repository)
那麼,物件的增刪查怎麼辦?從技術層面來講,我們只能依靠ORM工具了,我用的是NHibernate。簡單的說,通過NHibernate,我們可以在物件和資料庫結構中建立關係(對映)。然後,可以通過NHibernate的session,呼叫session.Save(), session.Delete(), session.Load()和session.Query()等方法將物件儲存、刪除或者載入/檢索到記憶體(C#專案)中使用。
/// 為什麼是NHibernate?
/// 1、我的專案開始得比較早,好幾年前了,應該是。當時Entity Framework還很不成熟,所以沒有辦法,只能選擇NHibernate
/// 2、我想看一看微軟框架以外的世界。其實後來我就知道了,在Java世界,我的這些做法已經差不多是主流了,所謂的SSH之類的。當然,對Java世界我也研究不深,可能也有差異。我的這個框架是自己摸索出來的,覺得夠用就好。
但從系統架構層面講,有另外一種提法:Repository模式。
Repository,從字面意義上理解,就是倉庫。這個概念我覺得很貼切,就像汽車存放在庫房裡,我們通過倉庫管理員,取出一輛或多輛汽車。這就有“程式碼對映真實世界”,一種邏輯自洽的感覺;而不是之前,一輛汽車取出十輛汽車的樣子。
具體到程式碼層面,就大概是這個樣子:
1 2 3 4 5 6 7 8 9 10 11 12 |
class BlogRepository { IList GetBlogs(int pageIndex, int pageSize) { return new List() { }; } Blog Get(int Id) { return new Blog(); } } |
但Repository的理解和使用都有爭議,主流的大概有兩種:
- 認為Repository是類似於集合,或者一種封裝集合的物件。所以還是把它放到了Entity中使用。
- 認為Repository是“聚合根”的一種,和取出/儲存物件並列,應該置於Entity之外。
我連Repository都沒有顯式的使用,所以就不進行這種關於概念的抽象討論了。後面有機會我們穿插著講一講吧。
我們“增”和“刪”直接利用了NHibernate的session機制,只是把“查(select)”給單獨抽象了出來,也單獨的抽象成一個名為Query的project。
Service
好了,現在我們可以回頭歸納一下。對系統資料的操作,我們腦海中應該是這樣一個概念:
- 前提:所有的物件平時都是直接的儲存在磁碟裡,然後:
- 我們需要某個/些物件時,就把他們從磁碟裡取出來,載入到記憶體中
- 進行一些操作修改
- 最後再儲存到磁碟中
那麼問題來了,上面這些步驟,由“誰”來做呢?注意我們現在所說的這些東西,都是在業務層的範疇。所以,按照三層架構的思路,應該是UI層呼叫BLL層,而我們的UI層,採用的是MVC,所以,這樣工作,是不是應該在Controller裡面做?
但是,閱讀我們的原始碼,你就會發現,我們在UI層和BLL層之間加了一個Service層。實際上是由Service層來做的這些載入、修改和儲存的工作。我非常同意這麼一個觀點:絕不能為了分層而分層。那麼,Service層存在的意義是什麼?
主要是為了前後端分離。早期的開發過程中,我設想過招聘一個專門的前端開發人員,他/她不管後臺的具體業務邏輯、和資料庫的互動,只管頁面的呈現和互動。那麼這裡就有一個問題,我不想她只是一個單純的美工,畫出效果圖切片弄成一個html的靜態頁面就完了,我希望她一樣的用VS進行開發,用Razor做成view,還負責頁面的互動和跳轉,所以她還得在Controller裡建Action,在Action裡寫程式碼。所以她在Action裡寫程式碼,是要得到資料用以呈現的,是需要根據頁面回發的資料呼叫不同的業務邏輯的。那麼,這些資料這些呼叫怎麼得來?等著後臺開發人員完成了之後再做?這無疑是很不經濟的。
所以我們抽象了一個ServiceInterface,前臺和後臺開發人員可以先確立一系列的介面,然後各自去完成自己的實現。於是就有了:
- UIDevService:前臺開發人員的“模擬”實現,看原始碼就可以發現,裡面是一些非常簡單粗暴的邏輯。比如需要一個ViewModel物件,就直接給new一個就可以了。
- ProdService:真正的業務邏輯實現,是一直連到資料庫的。
這其實就有一點“面向介面”的意思,前臺後臺都依賴於ServiceInterface的介面,而不管其具體的實現。
// 從這裡我們就可以看出來,複雜的架構是一種無奈的選擇。
// 如果我們的所有開發人員都是全棧級別的,可以從效果圖一直插到資料庫,我們可能就根本不需要這麼麻煩。
// 而現實的情況是,而大部分的開發人員,都有他們的專攻方向;全棧程式設計師畢竟太少了。
當然,這樣隔離出UIDevService之後,還附帶了其他一些好處,比如更便利的單元測試。這些我們都以後再說。
上張圖吧。先看看,看不懂也就算了,實在是我畫得不咋的。以後還會詳細講的:
ViewModel
我們專案中還有一個ViewModel,我們的開發人員曾不止一次的提出來:為什麼不能直接使用Entity呢?
我非常理解他的疑惑,一次次的把一個Entity裡面的Article的屬性取出來,再一條條的放到一個ArticleViewModel裡面去,這多鬧心啊?吃飽了撐的?
其實,我也是開發人員,這框架是我一個字母一個字母敲出來的,能偷懶的我肯定都會偷懶!就像前面我沒采用Repository一樣,我甚至都還弄過兩層架構,但最後都沒有好下場,才一步步走到今天。簡單的說,ViewModel存在的原因主要有兩個:
第一、前後端分離的要求。如果直接使用Entity,前臺開發人員是不是又得等著後臺開發人員把Entity先建好?是不是Entity一有變動就會立馬影響前臺開發?有興趣的同學可以觀察我們的ui.task.zyfei.net.sln解決方案,BLL層裡的所有project是根本就沒有包括在裡面的,我們徹底的做到了物理隔絕!
第二、ViewModel和Entity其實是不能100%對應的。嘗試過的同學都應該明白。比如我們創業家園專案裡有“最新發布部落格”的列表小方塊,它是一個部落格的集合,你怎麼弄?你說我可以使用IList;但這個小方塊裡還有一個邏輯:如果當前使用者是部落格博主,顯示修改連結。所以需要“當前使用者”的資料,你又怎麼把這個資料弄進來?當然,這是一個很大的命題。你肯定可以通過各種手段做到,最簡單的就是使用ViewBag。混合ViewBag和Enitty,幾乎可以解決所有問題,但有時候太醜陋了!
最後,我們其實應該跳出來,從架構的角度來思考這個問題。ViewModel究竟是什麼?它說承載的職責應該是什麼?應該由誰來構建它?……
我認為:ViewModel本質上就是一個用於頁面呈現的資料容器(DTO),所以他不應該具有任何內在邏輯,而且應該由前端開發人員來構建它。前端開發人員應該徹底的擺脫業務層中的Entity的束縛,根據頁面的呈現規律,大膽的進行各種抽象組合,使得ViewModel真正的綻放它的光彩!
MVC
說完了上面這些,MVC其實也就沒什麼好說的了。就是Controller呼叫Service,得到ViewModel供View使用這樣一個流程。當然,裡面有很多值得細講的內容,比如mvc route的測試、使用Autofac切換Service的實現、Session Per Request進行效能優化等。我們在之後的分則裡細講。
這裡還是上一張我製作的PPT吧,醜了點,先將就看吧!
Tool
看過原始碼的同學肯定也注意到了專案裡有一個Tool的專案資料夾。裡面最重要的,就是BuildDatabase專案。這個專案,肩負了構建開發和整合測試資料庫的雙重責任,還有幫助生成環境資料庫更新的作用,是測試驅動的有力保證。可參考(文件可測試化)
要填的坑
框架就這麼拉出來了,但其實裡面的坑還有很多,趁著有思路,先挖出來,以後慢慢填:
1. UI
- CurrentUser的處理:也是一個相當頭痛的東西,因為會大量使用,那麼就想著要重用,要想重用就傷腦筋
- Get-Post-Redirct模式:裡面也是一堆的坑。因為Http是無狀態的,所以Redirect的時候就面臨著一個傳遞資料的問題
- MVC Route:曾經傷心欲絕,當頁面複雜之後,url就跳不到指定的action;或者稍一改動,以前的route規則就就崩潰了
- Partial View、EditTemplate和Child Action:在裡面不知道暈了多久
- 單元測試
- 其他效能優化
2. Service
- 提高效能:SessionPerRequest。這個必須放在最前面說,因為它深刻的影響了我們下面提到的頁面架構的很多東西
- UIDev和Prod的切換:利用Autofac
- SessionPerRequest的具體實現,和UI和NHibernate都攪在一起,真不知道該放在哪裡說
- 為什麼不使用Repository模式而採用Query
- ViewMode的Map:使用Automapper
- 單元測試:Query又要攪到資料庫,唉……
3. BLL
- Entity大集合的效能問題。由於物件間的1:n的關係對映,造成一不小心,就扯出一堆集合資料出來,比如一個Author的所有Article,一個Article的所有Comment、Agree和Disagree。要這樣弄的話,再多的記憶體也吃不消。
- Entity的多型應用。超級大坑,簡直是要出人命的感覺,我覺得我能爬出來都是個奇蹟
- Entity的單元測試。由於Entity之間複雜的物件關係,其單元測試簡直就是一場災難
- Entity的NHMap單元測試。Entity裡都沒問題了,但你怎麼保證Entity的資料庫對映時正確的?只能做單元測試,還是繞不開資料庫!
4. Tool
- BuildDatabase:超級繁瑣超級難
- 其他清理統計工具等
呵呵,原來有這麼多坑!
這又讓我不由得想起我煩躁咆哮,扯頭髮摔滑鼠的那些日日夜夜,我也不止一次的懷疑過,我是不是走錯道了?這些亂七八糟的MVC、測試驅動、物件導向……根本就沒有讓我更高效順暢的開發,好像只是不斷的在扯我的後腿。我就用傳統的辦法,拖控制元件增刪改查資料庫又怎麼啦?不是一樣能用?而且說不定早就開發完了!……
但一次又一次解決問題的喜悅,一不小心窺視到另一個世界的驚奇,讓我欲罷不能。這可能就是技術路,人生路,大抵也如此吧?