文章出處

寫技術文檔的難度太大了!數次刪改,都沒能滿意,所以我還決定,先寫出來,以后再逐步整理完善——否則可能這個系列都沒辦法寫下去了。這也算是借鑒了敏捷的思路,先寫再改,不斷迭代重構吧!


前面的幾篇博客反響還不錯,但還有一個硬傷,“說了這么多理論,能不能實踐?”講類似概念的文章不算多,但也不少了,但我一直沒能從中收獲太多的東西,反而更是云里霧里的糊涂了。估計這主要是兩方面的原因造成的:我智商低,卻愛較真!

你說得得天花亂墜,我只信一點,眼見為實,“是騾子是馬,牽出來溜溜?”
按照你說的架構,把系統搭起來,跑起來,需求改上個幾百上千遍,高并發大流量沖一沖……咦,這樣一番折騰下來,沒被砸跨,系統千錘百煉之后,還百煉成鋼繞指柔。那我才豎起大拇指,真是不錯!

我相信,按照DDD、TTD、敏捷開發之類的理念,一定有成功的案例,不然他們不會被站在巔峰的技術大牛們交相稱贊。但很遺憾,我這個野生程序員,沒機會融入那個圈子。
所以我就用了一個最蠻最笨的方法:我自己做一個系統,嚴格按照我自己對于這些概念的理解進行開發,看最后這條路能不能走出來?歷經五年甚至更多時間的摸索和實踐,我覺得我基本上是走出來了。

所以,如果你愿意,就靜下心來,聽我細細道來吧。
 
尷尬


在確定了忘記數據庫的大原則之后,我們理應從業務層入手開始系統的搭建。

/*
為什么不是從UI層開始?不要笑,我還真記得,有看到過對這種做法的總結和推薦,還有一個什么專有名詞,大概就是“頁面驅動”之類的。
而且你靜下心想一想,我們很多的開發實際上就是這樣做的!確定方案之后,美工出效果圖,前臺切圖出靜態頁面,程序員改成動態的,一頁一頁的做。
任務考核就大概是這樣的,“我們今天把某個頁面做完”。

這種做法的好壞利弊我們就不展開了。但如果你一定要一個不從UI層開始的理由,我覺得最有力的就是:我們系統要做三個版本,電腦桌面頁面、手機頁面和手機APP。
*/


業務層里,通常我們就把需求里的一些名詞拎出來,做成一個一個的類,以創業家園為例,就應該有一個博客類(Blog),博客里還有方法,比如GetBlog(int Id),或者GetBlogs(int pageIndex, int pageSize),如下所示:

 

    class Blog
    {
        string Title { get; set; }
        string Body { get; set; }

        Blog Get(int Id)
        {
            return new Blog();
        }

        IList<Blog> GetBlogs(int pageIndex, int pageSize)
        {
            return new List<Blog>() { };
        }
    }

 

這是我最開始接觸三層架構時業務層類的樣子,寫在書上的。

但我就感覺這種做法特別別扭!一個博客對象取出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,從字面意義上理解,就是倉庫。這個概念我覺得很貼切,就像汽車存放在庫房里,我們通過倉庫管理員,取出一輛或多輛汽車。這就有“代碼映射真實世界”,一種邏輯自洽的感覺;而不是之前,一輛汽車取出十輛汽車的樣子。

 

具體到代碼層面,就大概是這個樣子:

    class BlogRepository
    { 
        IList<Blog> GetBlogs(int pageIndex, int pageSize)
        {
            return new List<Blog>() { };
        }

        Blog Get(int Id)
        {
            return new Blog();
        }
    }

 

但Repository的理解和使用都有爭議,主流的大概有兩種:

  1. 認為Repository是類似于集合,或者一種封裝集合的對象。所以還是把它放到了Entity中使用。
  2. 認為Repository是“聚合根”的一種,和取出/存儲對象并列,應該置于Entity之外。

我連Repository都沒有顯式的使用,所以就不進行這種關于概念的抽象討論了。后面有機會我們穿插著講一講吧。

 

我們“增”和“刪”直接利用了NHibernate的session機制,只是把“查(select)”給單獨抽象了出來,也單獨的抽象成一個名為Query的project。

 

Service

 

好了,現在我們可以回頭歸納一下。對系統數據的操作,我們腦海中應該是這樣一個概念:

  • 前提:所有的對象平時都是直接的存儲在磁盤里,然后:
  1. 我們需要某個/些對象時,就把他們從磁盤里取出來,加載到內存中
  2. 進行一些操作修改
  3. 最后再存儲到磁盤中

那么問題來了,上面這些步驟,由“誰”來做呢?注意我們現在所說的這些東西,都是在業務層的范疇。所以,按照三層架構的思路,應該是UI層調用BLL層,而我們的UI層,采用的是MVC,所以,這樣工作,是不是應該在Controller里面做?

 

但是,閱讀我們的源代碼,你就會發現,我們在UI層和BLL層之間加了一個Service層。實際上是由Service層來做的這些加載、修改和存儲的工作。我非常同意這么一個觀點:絕不能為了分層而分層那么,Service層存在的意義是什么?

 

主要是為了前后端分離。早期的開發過程中,我設想過招聘一個專門的前端開發人員,他/她不管后臺的具體業務邏輯、和數據庫的交互,只管頁面的呈現和交互。那么這里就有一個問題,我不想她只是一個單純的美工,畫出效果圖切片弄成一個html的靜態頁面就完了,我希望她一樣的用VS進行開發,用Razor做成view,還負責頁面的交互和跳轉,所以她還得在Controller里建Action,在Action里寫代碼。所以她在Action里寫代碼,是要得到數據用以呈現的,是需要根據頁面回發的數據調用不同的業務邏輯的。那么,這些數據這些調用怎么得來?等著后臺開發人員完成了之后再做?這無疑是很不經濟的。

所以我們抽象了一個ServiceInterface,前臺和后臺開發人員可以先確立一系列的接口,然后各自去完成自己的實現。于是就有了:

  1. UIDevService:前臺開發人員的“模擬”實現,看源代碼就可以發現,里面是一些非常簡單粗暴的邏輯。比如需要一個ViewModel對象,就直接給new一個就可以了。
  2. 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<Blog>;但這個小方塊里還有一個邏輯:如果當前用戶是博客博主,顯示修改鏈接。所以需要“當前用戶”的數據,你又怎么把這個數據弄進來?當然,這是一個很大的命題。你肯定可以通過各種手段做到,最簡單的就是使用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、測試驅動、面向對象……根本就沒有讓我更高效順暢的開發,好像只是不斷的在扯我的后腿。我就用傳統的辦法,拖控件增刪改查數據庫又怎么啦?不是一樣能用?而且說不定早就開發完了!……

但一次又一次解決問題的喜悅,一不小心窺視到另一個世界的驚奇,讓我欲罷不能。這可能就是技術路,人生路,大抵也如此吧?

最近沉溺于知乎,耽擱了正事。實在是慚愧啊!無論如何,從本篇博客開始,我又回來了!回來繼續苦逼的填坑……


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()