領域驅動和MVVM應用於UWP開發的一些思考
0x00 起因
有段時間沒寫部落格了,其實最近本來是根據梳理的MSDN上的資料(UWP開發目錄整理)有條不紊的進行UWP學習的。學習中有了心得體會或遇到了問題就寫一篇部落格記錄一下,方便後面查詢。不過前幾天在園子裡逛看了幾篇領域驅動的文章,突然發現領域驅動設計的有些地方對我有了很大的提示。在之前用WPF做桌面開發時,使用MVVM可以把View和Model很好的解耦,但在處理資料持久化的時候並沒有找到一種特別好的方式。我之前的做法是把ADO封裝了一層SQLHelper用於處理資料庫操作,解耦了資料庫操作和具體資料庫型別的依賴,當然了前提是資料庫操作是基於ADO.NET實現的。對於返回的DataTable,DataSet等在Model的方法中轉換為Model的例項。例如有一個物件是User,要從資料庫中獲取所有使用者我一般是在User類中寫一個靜態方法,User.GetAll(),這個方法會使用SQLHelper執行SQL語句,獲取DataTable,然後把DataTable轉換成IEnumerable<User>並返回,這算是人肉ORM吧。為此我還寫了個程式碼生成工具,把類中需要的屬性和屬性對應的資料庫表欄位名稱、型別等資訊設定好,可以直接生成程式碼實現對映,省去了手動輸入的麻煩,不過能生成的資料庫操作型別十分有限。但這樣做存在的一個很大的問題就是Model變得十分臃腫。雖然領域驅動提倡用充血模型,但我那種Model應該算是過度充血了吧。而且以現在的觀點來看,這種方式把基礎層(資料庫操作)、ORM(ADO物件對映為User物件)、Model緊緊的粘合在了一起,如果突然有一天跟我說資料操作必須用WebAPI,我能做的大概就是把之前的程式碼Ctrl+K+C,然後重新從WebAPI獲取資料,然後從JSON對映到Model吧。如果說需要資料庫和WebAPI混用,我的Model將會變得更加臃腫。
領域驅動設計通過Repository實現了業務領域和基礎層的資料庫操作之間低耦合,結合之前MVVM模式帶來的View和Model的低耦合,又恰巧最近在學習UWP開發,所以有了把MVVM和DDD在UWP開發中實踐的想法,於是也有了這篇文章。在這裡需要說明的是我也是剛開始接觸領域驅動,其中的很多概念還沒有接觸到,看的比較多的就是Repository,畢竟資料持久化是我之前的痛點。後面我也會把資料驅動相關書設定為我的床頭書,打算認真看一下,可能隨著學習和對領域驅動的瞭解有些想法會發生改變,有了新的體會後面也會寫文章,這篇文章主要記錄了這個時期我對領域驅動的粗淺理解,有不對的地方也希望多多指教。
0x01領域驅動和MVVM模式
我看到的關於領域驅動的文章很多都是討論實踐於ASP.NET MVC的,而且幾乎都把ORM當作了必選項。而我大多數時間是做桌面開發的,所以看到領域驅動的第一反應是與MVVM模式的結合。那麼MVVM和DDD有沒有可能結合起來呢。
MVVM核心的三個部分是Model、View、ViewModel,重點解決的是通過ViewModel實現Model和View的低耦合(MVVM模式解析和在WPF中的實現),但有一個問題是沒有解決的,那就是業務邏輯和資料持久化要如何處理。有人認為業務邏輯應該放在ViewModel中,Model應該是POCO物件,用於資料顯示,業務邏輯可以通過ViewModel和Service實現。在這種觀點中Model更偏向於DTO的存在。還有一種認為業務應當聚合到Model之下,這樣更OO一點,難以聚合到Model下的就寫成Service,我就是屬於這種觀點的,甚至於Model相關的所有業務邏輯和資料操作也全都聚合到了Model下。
而領域驅動注重的是業務領域這一層的分離,簡直就是對MVVM的一種最好的補充。現在要再有人問MVVM中業務邏輯該放在哪裡,我會毫不猶豫地告訴他業務邏輯要放在領域層。
從分層的思想來看,領域驅動設計為四層架構,分別是表示層、應用層、領域層和基礎層。
接下來要討論的領域驅動和MVVM的結合也是以領域驅動的這四層架構為基礎的,把MVVM的三個核心概念融合到這四層架構中。下面我就結合MVVM談一下自己對這幾層的一些認識。
0x02 表示層和應用層
首先表示層這個是最明確的,就是使用者看到的那一層。對於桌面應用來說就是視窗,對於Web應用來說就是HTML頁面。對應於MVVM模式中的View。
然後說下應用層,領域驅動設計認為應用層是非常薄的一層,應用層呼叫下面的領域層和基礎層來實現功能。這個對於主張把業務邏輯聚合到Model下的我來說沒什麼違和感,在MVVM中,應用層對應的就是ViewModel。首先必須說明的是這裡說的ViewModel和MVC中的ViewModel並不完全是一個概念,這兩個ViewModel都是對View的抽象,但在MVC中引入ViewModel主要是為了與View對應,減少View的邏輯操作。這個ViewModel是對View中資料的抽象。在MVVM中ViewModel除了對View的資料抽象外還包含了對使用者互動和功能等行為的抽象。例如MVVM中的ViewModel還需要處理使用者的點選,輸入等操作,MVVM中的ViewModel完全可以不包含業務細節,實現為很薄的一層。例如把大象放進冰箱大致分三步,ViewModel中的操作就是:
Fridge.Open();
Fridge.PutIn(elephant);
Fridge.Close();
至於冰箱空間不夠放不下大象了需要丟擲異常,能放下的話把大象放在哪裡更合理,放入大象後是不是需要擺放空間優化等等都聚合到了Fridge這個Model中,ViewModel只是呼叫這些功能。這也符合領域驅動中提倡的充血模型。
在領域驅動設計的四層架構中,我們可以看到表示層是可以呼叫下面三層的,也就是說表示層可能對下面三層都會產生依賴。這樣當表示層發生變化時下面三層中有的地方可能也需要做出改變,同樣業務邏輯的一些變化可能也會影響到表示層。而在領域驅動中使用了MVVM模式後,表示層(View)依賴於ViewModel,對於ViewModel以下都不直接產生依賴。
0x03 領域層和基礎層
View和ViewModel都找到了對應的層了,那Model對應哪一層呢。這個問題就不好回答了,這個得看對Model的理解了。如果把ViewModel看作很薄的一層應用層,Model使用貧血的POCO物件,那麼這時候MVVM中的Model更偏向於DTO的存在,這時候在領域層需要有另外的聚合了資料和邏輯的模型。如果把MVVM中的Model看作資料和業務聚合的充血模型,那麼這個Model可以當作業務領域中的Model,這時候如果需要的話可以考慮加入DTO。不過感覺這麼考慮有點太教條了。我覺得可把領域層模型看作MVVM中的Model。
基礎層這個是MVVM中沒有明顯說明的。如果我們把領域層的模型看作MVVM中的Model那麼基礎層是直接可以拿來用的,不存在任何衝突。在嘗試將MVVM和領域驅動結合後大概是下圖這種感覺,表示層只依賴於應用層。
0x04 關於Repository
之所以把Repository拿出來單獨說,就是因為領域驅動最開始吸引我的就是Repository很好的解決了MVVM中沒有涉及到並且長期困擾我的資料持久化的問題。此外另一個原因是看到很多文章討論Repository應該屬於哪一層的問題,這其中包含了Repository介面的定義和實現等。這裡我也談一下我個人的一點看法。
從領域驅動設計的四層架構中我們可以看到,領域層是依賴基礎層的,而基礎層並不依賴領域層,因為如果有兩層是互相依賴的,那麼分層就毫無意義了。這種依賴的具體表現是什麼呢?最具體的表現就是基礎層和領域層分別存放在不同的程式集中,領域層程式集引用基礎層程式集,基礎層可以在沒有領域層的時候編譯通過。基於這個結論來看Repository,Repository介面的定義和實現都是依賴於領域層實體模型的,這個是無法避免的,所以Repository無論介面的定義還是實現都應該放在領域層。實際上從訪問資料庫讀取資料到最終轉化為物件列表,這個過程跨越了基礎層和領域層,這個過程中包含著兩個關鍵動作,第一個是從資料庫中讀取資料庫資料,第二個是把原始資料對映為領域層實體物件。其中第一個動作屬於基礎層,第二個動作屬於領域層。具體到程式碼我個人的理解就是在領域層中不應該出現任何SQL語句以及明顯的資料庫相關的東西(例如SqlConnection)。根據這個理論來舉例,讀取Users表中的內容並獲得User物件列表。在基礎層中提供針對資料庫的通用的操作,操作返回ADO物件,不依賴於領域層:
public DataTable GetUserTable() { const string SQL = “SELECT * FROM Users”; Using(var con = new SqlConnection(ConnectionString)) { var cmd = new SqlCommand(sql,con); var da = new SqlDataAdaptor(cmd); var dt = new DataTable(); da.Fill(dt); return dt; } }
在領域層中定義Repository介面並實現,返回領域層物件User
public IEnumerable<User> GetAll() { var dt = GetUserTable(); foreach(DataRow row in dt.Rows) { yield return new User { Name = row[“name”].ToString(), ID = (int)row[“id”] } } }
這樣領域層單項依賴基礎層。
之前看到有文章討論把Repository介面定義放在領域層,介面實現放在基礎層,這個是不符合領域驅動的四層架構設計的。因為基礎層中對Repository的實現依賴了領域層。但如果不糾結於這個四層架構,或者說在實際專案中領域層和基礎層不需要分別放到單獨的程式集中,這麼做也是可以的,而且這麼做領域層會顯得更“純淨”,畢竟從資料庫到實體的對映不能算業務邏輯。還是那句話,模式和設計是一種通用的指導方向,最終還是要服務於特定場景,沒有絕對的對錯,只有合適不合適。
0x05 引入ORM後的問題
ORM的作用就是把具體的資料庫操作從業務邏輯中抽取出來,編寫業務邏輯時不需要再考慮具體的資料庫操作,把基礎層和領域層的功能封裝到了一起,這和Repository作用十分類似,可以說ORM是Repository的一個子集。這看上去似乎是很好的,在資料庫操作時直接使用ORM來代替Repository就可以了。但實際中存在的最大問題是ORM返回的實體物件是對資料庫表的抽象,一般是POCO物件,而Repository中返回的領域層物件是聚合根,聚合根和ORM返回的實體物件不一定是完全對應的,而且領域層物件是充血的。在某些情況下ORM返回的實體物件可以直接拿來作為領域層物件使用,這自然是好的,但當不能直接使用時就需要轉換為領域層物件或對ORM返回的實體物件進行功能上的擴充套件。
0x06 以上理論在UWP中的實踐
思考了一大堆理論連我自己都信了,但實踐才是檢驗真理的唯一標準。所以我打算新建一個測試用的UWP應用檢驗一下。記得學習MVVM那會,要用MVVM開發我一般會新建一個專案,然後建三個資料夾:View,Model,ViewModel,但為了能充分體驗那種低耦合和單項依賴的感覺,我曾在一個解決方案中建了三個專案,分別叫View、ViewModel和Model,其中View引用ViewModel,ViewModel引用Model(雖然在View最後生成的資料夾中我們看到了Model的dll,但View並不直接依賴Model)。專案是不能互相引用的,也就是單向的依賴。所以這次在檢驗自己理論時,我仍然用了這個方法,根據領域驅動四層架構的依賴關係,在一個解決方案中建了四個專案:View(表示層),ViewModel(應用層),Domain(領域層),Infrastructure(基礎層),其中Domain中有個Model資料夾存放領域模型,也是MVVM中的Model。VS解決方案中專案的排列是按照字母順序的,所以專案存在的順序並不代表他們之間的依賴關係。
這樣MVVM和DDD中的幾大樣算是全了,可以開始了。計劃是做一個類似記事本一樣的東西,可以新增標題,內容,並進行分類。應用會記錄新增時間和最後編輯時間。業務邏輯簡單,需要資料庫操作,所以看了下UWP的資料庫操作,然後就被啪啪啪打臉了。
UWP貌似只支援SQLite本地資料庫,不過SQLite也行啊,反正下幾個dll引用一下,資料庫操作封裝到Infrastructure,實體對映封裝到Domain的Repository,有強大的理論武器,怕什麼。結果看了下UWP中的SQLite操作然後就臉腫了,UWP中SQLite資料庫操作不是基於ADO.NET實現的,微軟自己包裝了一套叫SQLitePCL,實在太簡單易用了。兩行程式碼就執行完資料庫操作,但獲取的資料並不是DataSet或DataTable那種資料列表,只能獲取SQLite物件然後一行一行讀取資料,或者自己封裝成DataTable那樣的物件,返回到領域層,再由領域層對映為領域層物件。我是有多蛋疼才會那麼做啊!好吧我真的那麼做了,只是為了試一下分層,以後UWP開發中絕對不會第二次這麼做了。以後在需要SQLite資料庫操作時直接在領域層中獲取資料並對映成領域實體物件,好吧,在領域層中出現了SQL語句,這臉打的,不過我有法寶:任何設計都要看場景!UWP中真的沒有必要把資料庫操作放到基礎層,可以把UWP中的SQLite看作已經封裝好了的基礎層的功能,就差一條SQL語句當引數了。當然UWP上也有比較成熟的ORM工具,不過我沒有使用。
0x07 實踐後的想法
果然實踐出真知。
還有就是由於剛開始學習UWP,很多地方不太熟悉,有些希望達到的效果實現起來比較慢,所以這個例項最終還沒有做完。後面邊學邊做吧,牽扯到的一些技術問題都解決了估計也就入門了。另外在使用之前自己寫的簡易的MVVM框架去實際開發UWP應用時也發現了框架的一些不足,例如頁面導航用到Frame,所以在ViewModelBase中加入了Frame方便在ViewModel中導航,也體會到了設計時顯示測試資料的重要性,無需執行就能看到資料顯示的樣子,可以直接在設計介面觀察效果。為此加入了ViewModelLocator,在ViewModelBase中加入了InitTestData()和InitRealData()等,根據是不是DesignMode載入不同資料等等,這些等後面單獨寫一篇文章吧。
看了領域驅動的一些文章後對WPF開發也有很大的啟發,後面再做專案的時候可以從領域層的業務邏輯開始,分析完領域層後由擅長資料庫的人員去設計資料庫表和存取方法,只要最後按照領域層需求提供相應的操作即可,領域層定義和實現Repository介面,基於介面完成業務邏輯的編寫,應用層呼叫領域層和基礎層完成程式的功能和互動,開發介面的只要在需要資料的地方繫結資料,在需要執行命令的地方繫結命令就可以了。
第一次寫這麼長的文章,感覺想說的東西很多,不知道該怎麼寫,寫作能力太差啊。寫了3個多小時感覺亂七八糟的也不知道有沒有說明白,好吧,反正我自己是越寫越明白了。最後還有一點感受就是雖然紅軸比較輕,打字時間長了手也會酸啊。
0x08 相關下載
https://github.com/durow/TestArea/tree/master/UWPDDD
示例還沒有寫完,不過大概框架有了。還需要邊學習邊完善。
更多內容歡迎訪問我的部落格:http://www.durow.vip