領域驅動和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
示例還沒有寫完,不過大概框架有了。還需要邊學習邊完善。