前言
由於上次的內容已經完成取出商品列表頁與商品明細頁,我們已經知道MVC中資料如何由後端傳遞到前端的技巧,以及一種從前端帶資料至後端的技巧(網址帶入商品id的商品明細頁),所以本次就針對原本寫好的Code進行改動,實作分層的架構。
分層架構

這是我們這次的目標,我們要將之前的內容分解成以上,並且實作1~6的步驟。
Step1. 開始之前先搬移商品資料的位置
於專案中Models資料夾新增DBEntity資料夾,並新增DBEntity.cs檔案以及Product.cs檔案,

首先是DBEntity.cs,
再來就是Product.cs,這個就是原先寫在ProductController中的Product類別,要注意的地方是Product類別繼承了DBEntity類別(第6行),目的是告訴Product具有DBEntity的特性,也就是這個Class有Id屬性,這個設計後面會使用到😀。
於專案中新增 Mock資料夾,並新增MockData.cs檔案,

MockData.cs的內容為原先於ProductController中的productList資料,再經過一層Dictionary包裝(為了以後不只productList的資料時也能夠存取)。
這些都完成後,ProductController內的productList、Product類別可以先刪除了。
Step2. 建立Repository層級
建立Repository資料夾,並且建立MockRepository.cs檔案,

MockRepository.cs內容為針對MockData的資料進行存取,而資料存取的方法大致這邊分為,
- 新增(Create)
- 修改(Update)
- 刪除(Delete)
- 查詢(GetAll)
- 保存變更(Save) – 目前這個用不到,是規劃給未來串接資料庫時使用
所以現在針對MockData的DataList存取實作這些方法,如下,
- 這邊用到泛型的技巧設計出共用的存取的方法,以及用到了方才設計的DBEntity類別,設置泛型的目標為具有DBEntity特性的物件。
- 這邊也利用了Dictionary的特性去存取資料,這也是為什麼設計MockData時要使用字典,一切都是為了不管存取哪種資料都可以共用同一個方法😎。
Step3. 建立Service層級
建立Service資料夾,並建立ProductService.cs檔案,

ProductService的內容,主要有3件事情,
1. 存取資料前處理業務邏輯
什麼是存取前處理業務邏輯呢? 這邊用本次的案例來說明,
我們規劃的商品id都是1、2、3的大於0的整數值,所以我訂下的規則就是由網址傳入的數值要大於0,今天如果有用戶故意或無意的傳入-123的負數值時,我的業務邏輯就會處理該怎麼告訴用戶他的數值有問題,這就是在處理業務邏輯。
我個人比較常見的業務邏輯是,
(1) 輸入數據的檢核,例如,Email欄位,檢查用戶輸入有效的Email格式
(2) 業務規則檢核,例如,輸入規定輸入的數值範圍只能100~200之間
2. 呼叫Repository存取資料
接續1,如果業務邏輯處理皆通過時,則正常去存取所需的資料,
反之,當檢核邏輯已經未通過時,則存取的動作可以略過,這麼做也可以有效的降低Server的負擔。
3. 存取資料後處理業務邏輯並組成DTO模型,最終回傳該模型
存取資料到需要的資料後,再處理一次業務邏輯,一樣我用舉例的方式,假設今天是訂購商品,我要買10個,但存取資料後發現庫存剩下3個,那就要處理庫存不足的情況。
後續組成DTO模型,Data Transfer Object Model,這就是用於單純的傳遞資料的模型,內容不含任何的操作行為,
可能會有很多人會問為何不直接回傳EntityModel就好,例如直接回傳這次案例的Product類別,我的理由是,
(1) 資料模型的內容不一定每個欄位都被需要或是資料需要被處理過,例如,除了登入以外的需求,大部分會員密碼的資料是不被需要的;時間資料可能因為時區不同在組成DTO時需要額外處理。
(2) DTO的組成可能不只來源於一種類別的資料,例如,在存取商品資料時,除了商品本身的資料外還需要”商品的圖片”資料時DTO的組成來源就可能來自多個EntityModel(假設商品跟商品圖片的關係是一對多,所以商品圖片會是另外的EntityModel,本次案例非上述設計)
(3) 在面對需求變更時,如果變更的是EntityModel時,DTO可能不被修改;如果變更的是DTO時,EntityModel也可能不被影響,目的是不讓需求改變時牽一髮而動全身。
所以我認為DTO模型還是有存在的必要性,另外,DTO模型的使用時機還有非常多種案例情境,有機會再分享給大家😀。
好,以上長篇大論🤮,還是來實作本次的內容,於Models資料夾建立DTO資料夾,再於DTO資料夾建立Product資料夾,再於Product資料夾建立ProductDTO.cs檔案,

其中ProductDTO.cs內容與EntityModel一模一樣,是因為目前還沒有其他的情境需求,如上所述,就算一樣DTO模型還是有存在的必要,如下,
再來看ProductService.cs,本篇內容Focus在分層架構,故並沒有寫太多干擾閱讀的業務邏輯,簡單的處理業務邏輯可以看一下用id找商品的方法(36行開始),
Step4. 修改Controller/Action內容
簡單來說,
1. 要修改成呼叫Service的方法來存取內容
2. 最後組成ViewModel用於最後呈現給用戶看的畫面
在這邊我定義的ViewModel就是給View所使用的模型,其中只包含你想給用戶看到的資料,並且我會讓一個View只對應到一個ViewModel,所以一個ViewModel的組成可能也來自多個DTO。
實作這次的案例,於Models資料夾建立ViewModel資料夾,再於ViewModel資料夾建立Product資料夾,再於Product資料夾建立ProductListViewModel.cs、ProductListViewModel.cs檔案,

其內容分別為下,
然後修改,ProductController.cs內容
閱讀完後可以發現 List Action方法裡把原先寫在cshtml內容中的處理搬移到這裡變成組成ViewModel時處理(26 ~ 43行),所以cshtml的內容就可以變得相對單純,只是將要渲染的資料渲染上去,內容則不含任何的邏輯操作,
List.cshtml、Detail.cshtml的內容更新為以下,
以上,完成後可編譯專案看看自己有沒有寫錯東西,如果正常運行的結果則會與上篇內容一樣,但可以再看到分層架構的圖片並檢視目前的Code,流程是不是與圖片中的6個步驟一樣,這樣分層的架構就完成了🎉。

好,可能有人會說,做了分層架構要多寫好多資料夾、檔案,這樣看起來其實很亂很麻煩,感覺一開始寫在一份檔案裡面比較輕鬆。
我的回答則是,假設你的Controller其中有個方法有2000行的Code,今天面對需求是業務邏輯要修改,你要修改的地方在第幾行? 請問你要找多久? 修改的過程會不會牽一髮動全身?
所以分層架構最棒的地方在於我將事情的性質做了分類然後切割成更小的顆粒實作,實現了所謂的”職責分離”,這樣面對需求變更時,我要修改業務邏輯我就去Service改;我要修改資料存取我就去Repository改;我要修改畫面的資料我就去改ViewModel的組成。
另外,當我的顆粒變小以後,也方便未來去撰寫單元測試👍。

本篇內容為重構的第一步,先把分層的概念帶入至這個專案,下一篇會先把存取資料由MockData改成存取資料庫(MSSQL)的資料,將會介紹EntityFramework Core這個ORM套件,敬請期待😉。
完成的完整專案(Part6),
https://github.com/ChouJustice/CoreMVC_Learning
結語
- 本篇內容在於將原始內容切割成分層的架構所以過程步驟較為繁瑣,但請務必去理解整個過程的流程,我認為會對大家有幫助。
- 分層除了Code的內容之外,也可以觀察這次我在專案中的資料夾結構都是為了日後在找東西時方便尋找。
- 下篇將使用到MSSQL資料庫,請務必先準備好相應的環境與工具。
- 還是要說明一下,根據您的團隊與Coding Style的不同,每個團隊所設計出的架構都不盡相同,這次我分享學習過程中用了這樣的架構去Coding體悟到的感想還請不吝指教🙏。