前面幾篇一路從:
Route
↓
Controller
↓
View
↓
Model / ViewModel
慢慢把 MVC 接起來。
看到這裡,我原本會覺得:
既然 Controller 已經收到請求了,
那我就在 Controller 裡:
查資料
判斷條件
轉換欄位
存資料庫
最後 return View()
不就好了?
技術上當然不是完全不能這樣寫。
但企業專案如果每一個 Controller 都什麼事情自己做,很快就會變成:
Controller 超長
邏輯全部混在一起
同一套規則到處重複
很難測試
很難維護
改一個規則不知道會影響哪裡
所以實際專案裡很常會再多一層:
Service
這篇就來整理:
Controller 到底該做什麼?
Service 又負責什麼?
為什麼不要把所有邏輯都塞在 Controller?
假設現在要做:
新增使用者
如果所有事情都寫在 Controller,可能會變成:
[HttpPost]
public IActionResult Create(CreateUserViewModel model)
{
if (string.IsNullOrWhiteSpace(model.Name))
{
ModelState.AddModelError("Name", "姓名不能空白");
return View(model);
}
if (model.RoleId == 1)
{
model.RoleName = "管理員";
}
else
{
model.RoleName = "一般使用者";
}
User entity = new User
{
Name = model.Name,
Email = model.Email,
RoleId = model.RoleId,
CreateTime = DateTime.Now
};
_dbContext.Users.Add(entity);
_dbContext.SaveChanges();
return RedirectToAction("Index");
}
乍看之下好像也沒什麼問題。
但這個 Controller 同時在做:
接收 HTTP Request
驗證資料
判斷角色
轉換 ViewModel
建立 Entity
存資料庫
決定最後要去哪裡
也就是:
什麼都做
如果功能再複雜一點,可能還會加入:
檢查目前登入者權限
查詢其他資料表
判斷資料狀態
組合名稱
寫入操作紀錄
寄通知
最後一個 Action 可能就變成上百行。
這種 Controller 常會被叫做:
Fat Controller
胖 Controller
我後來會把 Controller 先理解成:
接住 Request,決定要呼叫誰處理,最後決定怎麼 Response。
例如:
接收網址參數
接收表單資料
接收 Request Model
基本 ModelState 判斷
呼叫 Service
return View()
RedirectToAction()
return Ok()
return BadRequest()
它比較像整個流程的:
入口
+
協調者
而不是所有事情的實作者。
可以先記:
Controller
比較關心 HTTP 流程
例如:
[HttpPost]
public IActionResult Create(CreateUserViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
_userService.Create(model);
return RedirectToAction("Index");
}
這樣 Controller 一眼就能看懂:
資料不合法
↓
回原頁面
資料合法
↓
交給 Service
成功
↓
回列表頁
真正「怎麼建立使用者」則交給 Service。
Service 可以先理解成:
負責系統的業務邏輯和流程規則。
例如:
角色怎麼判斷
狀態怎麼轉換
資料能不能修改
資料要怎麼組合
哪些欄位要轉換
要查哪些資料
完成後還要做哪些事情
這些都比較像 Service 的工作。
例如:
public class UserService
{
public void Create(CreateUserViewModel model)
{
string roleName;
if (model.RoleId == 1)
{
roleName = "管理員";
}
else
{
roleName = "一般使用者";
}
User entity = new User
{
Name = model.Name,
Email = model.Email,
RoleId = model.RoleId,
CreateTime = DateTime.Now
};
_userRepository.Create(entity);
}
}
Controller 就只剩:
[HttpPost]
public IActionResult Create(CreateUserViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
_userService.Create(model);
return RedirectToAction("Index");
}
這時分工就清楚很多。
可以先用這張表記。
| Controller | Service |
|---|---|
| 接收 Request | 處理業務邏輯 |
| 接收網址參數 | 判斷規則 |
| 接收表單資料 | 欄位轉換 |
| ModelState 判斷 | 狀態處理 |
| 呼叫 Service | 組合資料 |
| 決定 View / Redirect / JSON | 呼叫資料存取 |
| 處理 HTTP Response | 執行功能流程 |
我自己會記成:
Controller
「這個 Request 要怎麼處理?」
Service
「這個功能實際要怎麼做?」
這個詞我一開始也覺得很抽象。
其實可以直接看需求。
例如需求說:
只有管理員可以修改資料
這就是規則。
狀態為完成後不能再次編輯
也是規則。
員工編號要先轉成內部主鍵才能查資料
也是規則。
新增資料時要自動填入建立者和建立時間
也是規則。
這些不是單純:
收到 HTTP Request
也不是:
SELECT * FROM ...
而是:
這個系統「應該怎麼運作」
這些就很適合放在 Service。
不一定。
這就要看專案架構。
有些比較簡單的專案可能是:
Controller
↓
Service
↓
DbContext
↓
Database
Service 直接用:
_dbContext.Users
查資料。
但比較分層的專案可能是:
Controller
↓
Service
↓
Repository
↓
DbContext
↓
Database
或:
Controller
↓
Service
↓
DBService
↓
SQL
↓
Database
也就是:
Service
負責「要查什麼、要做什麼」
資料存取層
負責「怎麼跟資料庫拿」
例如 Service:
public UserDetailViewModel GetDetail(int id)
{
User user = _userRepository.GetById(id);
UserDetailViewModel model = new UserDetailViewModel
{
Name = user.Name,
Email = user.Email
};
return model;
}
Repository:
public User GetById(int id)
{
return _dbContext.Users
.FirstOrDefault(x => x.Id == id);
}
這樣:
Service
不用知道太多資料庫操作細節
Repository
不用知道畫面怎麼顯示
一開始真的會覺得:
本來 Controller 一支檔案就寫完了
現在變成:
Controller
Service
Repository
不是更麻煩嗎?
小功能確實可能看起來比較多。
但專案一大,這種分工會開始有價值。
假設 Controller 是:
public IActionResult Detail(int id)
{
UserDetailViewModel model = _userService.GetDetail(id);
return View(model);
}
我一眼就知道:
收到 id
↓
取得詳細資料
↓
傳給 View
不用先讀 80 行 SQL、權限判斷、欄位轉換。
假設:
使用者姓名顯示規則
不只一個 Controller 需要。
如果全部寫 Controller:
UserController 寫一次
ReportController 再寫一次
AdminController 再寫一次
以後規則改了,三個地方都要改。
如果集中在:
UserService
其他地方就能共用同一套邏輯。
Controller A
↓
UserService
Controller B
↓
UserService
Controller C
↓
UserService
例如畫面上的角色名稱錯了。
如果所有東西都塞在 Controller,我只能從一大坨程式裡慢慢找。
但有分層後,可以先問:
Controller 傳錯資料?
Service 轉換錯?
Repository 查錯資料?
Database 原始資料就錯?
問題比較容易被拆開。
前一篇有講:
Entity
比較靠近資料庫
ViewModel
比較靠近畫面
Service 就常站在中間做這件事:
Entity
↓
整理、轉換、判斷
↓
ViewModel
例如資料庫:
RoleId = 1
畫面不要顯示:
1
而是:
管理員
Service 可以負責轉換:
UserDetailViewModel model = new UserDetailViewModel
{
Name = entity.Name,
RoleName = GetRoleName(entity.RoleId)
};
View 就只需要:
@Model.RoleName
不需要自己判斷:
@if (Model.RoleId == 1)
{
...
}
也不是。
不是看到:
if
就一定叫業務邏輯。
例如 Controller:
if (!ModelState.IsValid)
{
return View(model);
}
這比較是在處理:
HTTP 表單驗證流程
放 Controller 很合理。
但例如:
if (user.Status == "Completed")
{
throw new Exception("完成後不可修改");
}
這就比較像:
系統規則
通常更適合 Service。
所以不能只看語法。
要問的是:
這個判斷是在處理 HTTP?
還是在處理系統規則?
簡單的小轉換技術上當然可以。
但如果開始出現:
員工編號轉內部 ID
狀態代碼轉名稱
權限判斷
建立者欄位填值
更新者欄位填值
多張資料組合
全部塞 Controller,就很容易變胖。
比較理想的方向通常是:
Controller
↓
把輸入交出去
Service
↓
處理規則和資料轉換
尤其同一套轉換如果很多功能都會用,更不適合到處複製。
假設現在是:
查看使用者詳細資料
Controller:
public IActionResult Detail(int id)
{
UserDetailViewModel model = _userService.GetDetail(id);
return View(model);
}
Service:
public UserDetailViewModel GetDetail(int id)
{
User user = _userRepository.GetById(id);
UserDetailViewModel model = new UserDetailViewModel
{
Name = user.Name,
Email = user.Email
};
return model;
}
Repository:
public User GetById(int id)
{
return _dbContext.Users
.FirstOrDefault(x => x.Id == id);
}
最後 View:
@model UserDetailViewModel
<h1>@Model.Name</h1>
<p>@Model.Email</p>
完整流程就是:
瀏覽器
↓
Controller
↓
Service
↓
Repository
↓
Database
↓
Repository 回傳 Entity
↓
Service 轉成 ViewModel
↓
Controller
↓
View
實際專案可能看到:
IUserService.cs
UserService.cs
Controller 宣告:
private readonly IUserService _userService;
而真正實作是在:
public class UserService : IUserService
{
}
這裡可以先理解成:
IUserService
定義「這個 Service 能做哪些事情」
UserService
真正實作「這些事情怎麼做」
例如 Interface:
public interface IUserService
{
UserDetailViewModel GetDetail(int id);
}
Service:
public class UserService : IUserService
{
public UserDetailViewModel GetDetail(int id)
{
// 真正處理邏輯
}
}
現在先不用深入 Interface 和 Dependency Injection。
只要知道:
F12 按下去
有時候會先跳到 Interface
真正程式可能還在另一支 Service 裡
這件事下一篇追程式時會很重要。
如果我打開一個 Controller,看到:
public IActionResult Detail(int id)
{
var model = _userService.GetDetail(id);
return View(model);
}
以前可能會覺得:
就這樣?
真正的程式去哪了?
現在我會先拆:
id
↓
從 Request 進來的資料
_userService.GetDetail(id)
↓
真正功能往 Service 走
model
↓
Service 回傳的資料
return View(model)
↓
把資料交給 View
所以我不會期待 Controller 一定有很多程式。
有時候:
Controller 越乾淨
反而代表真正邏輯被放到其他負責的層
假設看到:
public IActionResult Update(UserViewModel model)
{
// 30 行資料查詢
// 20 行權限判斷
// 10 行欄位轉換
// 20 行資料庫更新
// 10 行通知
}
我不會直接說:
這一定是錯的
因為還是要看專案規模和團隊架構。
但我會開始注意:
這些邏輯是不是很多地方都會用?
是不是屬於系統規則?
是不是讓 Controller 很難閱讀?
是不是和 HTTP 沒有直接關係?
如果答案都是:
對
那通常就很適合往 Service 拆。
例如 Controller:
var model = _userService.GetDetail(id);
我想知道資料到底怎麼來。
現在可以:
把游標放在 GetDetail
↓
按 F12
有可能先跳到:
IUserService
看到:
UserDetailViewModel GetDetail(int id);
這裡只有宣告,沒有真正內容。
接著要再找到:
UserService
裡真正的:
public UserDetailViewModel GetDetail(int id)
{
...
}
然後裡面可能又看到:
_userRepository.GetById(id);
再繼續往下追。
也就是:
Controller
↓
Service Interface
↓
Service
↓
Repository / DBService
↓
Database
這就是下一篇的主題。
Controller 不是不能寫邏輯。
真正的重點是:
不要讓 Controller 同時負責太多不同責任
可以先這樣分:
Controller
↓
接收 Request
取得參數
基本 HTTP 流程判斷
呼叫 Service
決定 View / Redirect / JSON
Service
↓
處理業務規則
權限與狀態判斷
資料組合
欄位轉換
協調資料存取
Repository / DBService / DbContext
↓
實際和資料庫溝通
所以一個比較清楚的流程可能是:
Request
↓
Controller
↓
Service
↓
資料存取層
↓
Database
查詢結果再回來:
Database
↓
Entity / DTO
↓
Service
↓
ViewModel
↓
Controller
↓
View
這樣前一篇的:
Entity
ViewModel
DTO
也開始真的有位置了。
以前看到 Controller 裡只有:
_userService.GetDetail(id);
可能會覺得:
真正的程式是不是不見了?
現在會知道:
不是不見了
只是被分到 Service 裡去了
下一篇就不只講概念,直接開始實際追:
Controller 裡只看到
_userService.GetDetail(id),我要怎麼用 F12 一路追到 Service、資料存取,最後找到資料到底是從哪裡查出來的?