最近在整理一段舊系統的商業邏輯時,發現錯誤處理幾乎清一色都是 try/catch + throw
現在其實已經是 21 世紀了,很多時候有更好的方法可以處理 try catch 跟以前學習的方式有點不同
這篇單純記錄我實際套用 Result Pattern 後的想法與最小可行寫法筆記一下..
先講重點結論 - 如果一個失敗是你"早就知道會發生"的狀況,那就不該用 exception 來表達
Result Pattern 的價值不在於多厲害,而是把失敗直接寫進回傳型別,讓控制流程變得在主流程可以得知
不用在外面呼叫也是一堆 try/catch 處理
我常寫的 code:
public User GetUserById(int id)
{
var user = _repository.Find(id);
if (user is null)
throw new UserNotFoundException("User is not Existed.");
return user;
}
這一段程式碼其實沒有錯,只是實務成本太高呼叫端只能被迫去接你的 exception ,其實這資料(用戶)不存在
是一個可以被預期的可能,然後可以被正常表達的處理狀態,就可以調整使用 Result Pattern
Result Pattern 和新想法 ,就是不要用 throw 來中斷流程,而是回傳一個成功或失敗的結果。
於是可以試著把 method 改成這樣
public Result GetUserById(int id){
//...
}
這邊給一個比較簡單我也常用的Result<T> 設計
public class Error
{
public string Message { get; }
public Error(string message)
{
Message = message;
}
}
public class Result
{
public bool IsSuccess { get; }
public T Value { get; }
public Error Error { get; }
private Result(T value)
{
IsSuccess = true;
Value = value;
}
private Result(Error error)
{
IsSuccess = false;
Error = error;
}
public static Result Success(T value) => new(value);
public static Result Failure(string message) =>
new(new Error(message));
}
這樣我們就可以重新實做 GetUserById
public Result GetUserById(int id)
{
var user = _repository.Find(id);
if (user == null)
return Result.Failure("User is not Existed.");
return Result.Success(user);
}
//呼叫方式
var result = service.GetUserById(id);
if (!result.IsSuccess)
{
Console.WriteLine(result.Error.Message);
return;
}
Process(result.Value);
其實看到現在,或許你覺得無聊跟多此一舉,但是其實在實務上你在在設計 API ,我們也會常常使用
Result Pattern 畢竟對方呼叫是 200 正確,但是 service provider 端這邊可能會必須要告訴呼叫端 server 發生出的錯誤
之後這樣的 code 改成 API 也會比較方便,真正的把 try catch 留給我們不能預期的錯誤在去攔截
畢竟以前我常常很喜歡透過各種 Exception 去表示錯誤,我現在幾乎都是 Result + Exception 混用
讓商業邏輯回到 if/else,程式碼會更好讀,也更好測試
等到真的需要錯誤分類或更細緻的錯誤處理時,再慢慢把它補回來就好
在這之前就多打點字,不過現在都有 AI 幫忙寫了,在現在 AI 時代更應該把程式碼寫得更加的結構好維護
--
本文原文首發於個人部落格:[C#] 從 try/catch 到 Result Pattern:讓錯誤回到主流程的寫法
--
---
The bug existed in all possible states.
Until I ran the code.