大家好!今天我們要討論的是 API 設計中的一個關鍵步驟——界定 API 邊界。這個過程就像是在建造一個圖書館之前,先確定每個區域的用途:哪些地方是放書的、哪些地方是供人閱讀的,以及哪些地方需要與其他部門協作。通過合理界定 API 的邊界,我們可以確保 API 的功能既精確又高效,避免陷入過度設計或欠缺設計的陷阱。
API 的邊界決定了它的功能範圍,就像圖書館的功能區劃一樣。清晰地界定 API 的邊界,不僅能夠幫助你專注於 API 的核心功能,還能避免捲入不必要的複雜性和業務耦合,從而提高系統的靈活性和可維護性。
我們先來反向思考一下:如果 API 沒有明確的邊界,會變成什麼樣子?
沒有邊界的 API:多合一反模式
假設今天你要為公司開發一個客戶管理系統,這個系統需要對公司客戶進行 CRUD 操作(Create, Read, Update, Delete),具體需求包括:
查詢單筆客戶
查詢多筆客戶
新增客戶
修改單筆客戶
批次修改客戶
刪除單筆客戶
如果我們不清楚地界定 API 的邊界,而是選擇一個「多合一」的 API 來處理這些需求,那麼 API 可能會設計成這樣:
public class Customer
{
public string Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
public class ApiResult
{
public bool Success { get; set; }
public string Message { get; set; }
public List<Customer> Customers { get; set; }
public Customer SingleCustomer { get; set; }
}
public ApiResult CustomerAction(List<Customer> customers, string actionType)
{
switch (actionType)
{
case "GetSingle":
// 查詢單筆客戶邏輯
break;
case "GetMultiple":
// 查詢多筆客戶邏輯
break;
case "Create":
// 新增客戶邏輯
break;
case "UpdateSingle":
// 修改單筆客戶邏輯
break;
case "UpdateBatch":
// 批次修改客戶邏輯
break;
case "Delete":
// 刪除客戶邏輯
break;
default:
return new ApiResult { Success = false, Message = "無效的操作類型" };
}
return new ApiResult { Success = true, Message = "操作成功" };
}
或許部分初學者會覺得這不就是switch case 的用法,根據Type來分類並執行他的動作,但若是把詳細的各動作邏輯都寫出來,程式碼的閱讀性會變的非常糟糕。不妨看看下面這個將邏輯部分完整呈現的範例:
public class Customer
{
public string Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
public class ApiResult
{
public bool Success { get; set; }
public string Message { get; set; }
public List<Customer> Customers { get; set; }
public Customer SingleCustomer { get; set; }
}
private List<Customer> _customers = new List<Customer>();
public ApiResult CustomerAction(List<Customer> customers, string actionType, string customerId = null)
{
switch (actionType)
{
case "GetSingle":
var customer = _customers.FirstOrDefault(c => c.Id == customerId);
if (customer == null)
{
return new ApiResult { Success = false, Message = "客戶不存在" };
}
return new ApiResult { Success = true, Message = "查詢成功", SingleCustomer = customer };
case "GetMultiple":
return new ApiResult { Success = true, Message = "查詢成功", Customers = _customers };
case "Create":
foreach (var newCustomer in customers)
{
_customers.Add(newCustomer);
}
return new ApiResult { Success = true, Message = "新增成功" };
case "UpdateSingle":
var existingCustomer = _customers.FirstOrDefault(c => c.Id == customerId);
if (existingCustomer == null)
{
return new ApiResult { Success = false, Message = "客戶不存在" };
}
existingCustomer.Name = customers[0].Name;
existingCustomer.Email = customers[0].Email;
return new ApiResult { Success = true, Message = "更新成功" };
case "UpdateBatch":
foreach (var updatedCustomer in customers)
{
var customerToUpdate = _customers.FirstOrDefault(c => c.Id == updatedCustomer.Id);
if (customerToUpdate != null)
{
customerToUpdate.Name = updatedCustomer.Name;
customerToUpdate.Email = updatedCustomer.Email;
}
}
return new ApiResult { Success = true, Message = "批次更新成功" };
case "Delete":
var customerToDelete = _customers.FirstOrDefault(c => c.Id == customerId);
if (customerToDelete == null)
{
return new ApiResult { Success = false, Message = "客戶不存在" };
}
_customers.Remove(customerToDelete);
return new ApiResult { Success = true, Message = "刪除成功" };
default:
return new ApiResult { Success = false, Message = "無效的操作類型" };
}
}
看到這一大陀的程式碼,如果要針對特定的功能進行測試與修改,你要先能一眼找出要修改的點變的非常困難,更何況上述還不是很完整的處理商業邏輯,還是非常省略過後的程式。因此我們能看到
沒有邊界的 API 帶來的問題
這種多合一的 API 設計看起來很方便,所有的操作都集中在一個方法中。然而,這種設計會帶來以下問題:
可讀性差: 隨著功能的增多,CustomerAction 方法的代碼將變得越來越長,難以維護。每次新增或修改功能,都需要改動這個方法,容易引入錯誤。
高耦合度: 不同的業務邏輯被耦合在一起,任何一個操作的改變都可能影響到其他操作,導致代碼的穩定性下降。
難以測試: 由於所有操作都集中在一個方法中,對這個方法進行單元測試時需要考慮多種情況,測試複雜度大大增加。
無法擴展: 當你需要為客戶管理系統新增更多功能時,這個多合一 API 的複雜性會成倍增長,最終導致代碼難以擴展。
為了解決上述問題,我們需要明確 API 的邊界,將不同的操作分散到不同的 API 方法中。這樣,每個 API 只負責一項具體的功能,既清晰又易於維護。
public ApiResult GetCustomerById(string id)
{
// 查詢單筆客戶邏輯
}
public ApiResult GetCustomers()
{
// 查詢多筆客戶邏輯
}
public ApiResult CreateCustomer(Customer customer)
{
// 新增客戶邏輯
}
public ApiResult UpdateCustomer(Customer customer)
{
// 修改單筆客戶邏輯
}
public ApiResult UpdateCustomers(List<Customer> customers)
{
// 批次修改客戶邏輯
}
public ApiResult DeleteCustomer(string id)
{
// 刪除單筆客戶邏輯
}
有邊界的 API 帶來的好處
清晰的結構: 每個 API 方法只負責一項具體任務,代碼結構清晰,易於閱讀和理解。
低耦合度: 不同的業務邏輯被分開,修改某一個功能時,不會影響到其他功能,代碼更穩定。
易於測試: 由於每個 API 方法只負責一個功能,測試變得簡單,能夠專注於測試單一功能的正確性。
良好的可擴展性: 當需要新增功能時,可以輕鬆地添加新的 API 方法,而不會影響到現有的功能。
上面的程式範例,還不是主流的架構設計下的程式碼範例,帶篇幅慢慢帶到,會一步一步將程式碼修正到較好的架構與設計。
界定 API 邊界涉及多個步驟,從理解業務需求到識別核心資源,再到劃分功能責任。這些步驟幫助我們構建一個精確且靈活的 API,能夠滿足業務需求而不會過度膨脹。
理解業務需求: 首先,你需要深入理解 API 所服務的業務領域。這意味著你必須清楚地了解業務流程,以及哪些功能是 API 必須支持的。例如,在圖書館管理系統中,圖書的借閱和歸還是核心功能,因此這部分必須由 API 來處理。
識別核心資源: 核心資源是 API 必須管理的主要對象,這些資源通常是業務中最重要的實體。在圖書館管理系統中,核心資源可能包括「圖書」、「用戶」和「借閱記錄」。這些資源的管理需要被 API 清晰地界定和實現。
劃分功能責任: 明確哪些功能應該由 API 來處理,哪些應該留給其他系統或模組來完成。例如,圖書館的書籍管理和借閱系統可以由 API 處理,但身份驗證和支付功能可能應該交給專門的第三方服務來負責。這種劃分可以讓 API 聚焦於核心業務,同時保持整個系統的靈活性和可擴展性。
正確的HTTP 規格:
正確使用 HTTP 方法和狀態碼是界定 API 邊界的重要部分。遵循 RESTful 設計原則,我們可以更清晰地定義 API 的功能和行為:
同時,使用適當的 HTTP 狀態碼可以更好地表達 API 的響應:
通過正確使用 HTTP 方法和狀態碼,我們可以更好地定義 API 的邊界,使其行為更加清晰和可預測。
假設你正在設計一個圖書館管理系統的 API,我們可以通過以下方式來界定它的邊界:
這樣的劃分可以幫助 API 專注於圖書館的核心業務,而不會因為捲入過多的非核心功能而變得複雜難維護。
今日小結:
界定 API 邊界是設計高效且可維護 API 的第一步。通過明確 API 的職責範圍,我們可以確保它專注於核心業務,並且能夠靈活應對未來的需求變更。這種精確的劃分可以避免過度設計和欠缺設計,從而提高整個系統的穩定性和可擴展性。明天,我們將在此基礎上進一步討論如何建立 API 模型,這將為 API 的實際開發奠定堅實的基礎。