iT邦幫忙

2024 iThome 鐵人賽

DAY 4
1

大家好!今天我們要討論的是 API 設計中的一個關鍵步驟——界定 API 邊界。這個過程就像是在建造一個圖書館之前,先確定每個區域的用途:哪些地方是放書的、哪些地方是供人閱讀的,以及哪些地方需要與其他部門協作。通過合理界定 API 的邊界,我們可以確保 API 的功能既精確又高效,避免陷入過度設計或欠缺設計的陷阱。


1.什麼是API邊界?

API 的邊界決定了它的功能範圍,就像圖書館的功能區劃一樣。清晰地界定 API 的邊界,不僅能夠幫助你專注於 API 的核心功能,還能避免捲入不必要的複雜性和業務耦合,從而提高系統的靈活性和可維護性。

我們先來反向思考一下:如果 API 沒有明確的邊界,會變成什麼樣子?

沒有邊界的 API:多合一反模式

假設今天你要為公司開發一個客戶管理系統,這個系統需要對公司客戶進行 CRUD 操作(Create, Read, Update, Delete),具體需求包括:

  1. 查詢單筆客戶

  2. 查詢多筆客戶

  3. 新增客戶

  4. 修改單筆客戶

  5. 批次修改客戶

  6. 刪除單筆客戶

如果我們不清楚地界定 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 設計看起來很方便,所有的操作都集中在一個方法中。然而,這種設計會帶來以下問題:

  1. 可讀性差: 隨著功能的增多,CustomerAction 方法的代碼將變得越來越長,難以維護。每次新增或修改功能,都需要改動這個方法,容易引入錯誤。

  2. 高耦合度: 不同的業務邏輯被耦合在一起,任何一個操作的改變都可能影響到其他操作,導致代碼的穩定性下降。

  3. 難以測試: 由於所有操作都集中在一個方法中,對這個方法進行單元測試時需要考慮多種情況,測試複雜度大大增加。

  4. 無法擴展: 當你需要為客戶管理系統新增更多功能時,這個多合一 API 的複雜性會成倍增長,最終導致代碼難以擴展。

有邊界的 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 帶來的好處

  1. 清晰的結構: 每個 API 方法只負責一項具體任務,代碼結構清晰,易於閱讀和理解。

  2. 低耦合度: 不同的業務邏輯被分開,修改某一個功能時,不會影響到其他功能,代碼更穩定。

  3. 易於測試: 由於每個 API 方法只負責一個功能,測試變得簡單,能夠專注於測試單一功能的正確性。

  4. 良好的可擴展性: 當需要新增功能時,可以輕鬆地添加新的 API 方法,而不會影響到現有的功能。

上面的程式範例,還不是主流的架構設計下的程式碼範例,帶篇幅慢慢帶到,會一步一步將程式碼修正到較好的架構與設計。


2. 如何界定 API 邊界?

界定 API 邊界涉及多個步驟,從理解業務需求到識別核心資源,再到劃分功能責任。這些步驟幫助我們構建一個精確且靈活的 API,能夠滿足業務需求而不會過度膨脹。

  1. 理解業務需求: 首先,你需要深入理解 API 所服務的業務領域。這意味著你必須清楚地了解業務流程,以及哪些功能是 API 必須支持的。例如,在圖書館管理系統中,圖書的借閱和歸還是核心功能,因此這部分必須由 API 來處理。

  2. 識別核心資源: 核心資源是 API 必須管理的主要對象,這些資源通常是業務中最重要的實體。在圖書館管理系統中,核心資源可能包括「圖書」、「用戶」和「借閱記錄」。這些資源的管理需要被 API 清晰地界定和實現。

  3. 劃分功能責任: 明確哪些功能應該由 API 來處理,哪些應該留給其他系統或模組來完成。例如,圖書館的書籍管理和借閱系統可以由 API 處理,但身份驗證和支付功能可能應該交給專門的第三方服務來負責。這種劃分可以讓 API 聚焦於核心業務,同時保持整個系統的靈活性和可擴展性。

  4. 正確的HTTP 規格:

    正確使用 HTTP 方法和狀態碼是界定 API 邊界的重要部分。遵循 RESTful 設計原則,我們可以更清晰地定義 API 的功能和行為:

    • GET: 用於獲取資源,不應該對資源進行修改。例如,獲取圖書信息或借閱記錄。
    • POST: 用於創建新資源。例如,添加新的圖書或創建新的借閱記錄。
    • PUT: 用於更新現有資源。例如,更新圖書信息或修改借閱記錄。
    • DELETE: 用於刪除資源。例如,刪除過期的借閱記錄。
    • PATCH: 用於部分更新資源。例如,更新圖書的部分信息而不是全部。

    同時,使用適當的 HTTP 狀態碼可以更好地表達 API 的響應:

    • 200 OK: 請求成功。
    • 201 Created: 資源創建成功。
    • 204 No Content: 請求成功,但無返回內容(如刪除操作)。
    • 400 Bad Request: 請求格式錯誤。
    • 404 Not Found: 請求的資源不存在。
    • 500 Internal Server Error: 服務器內部錯誤。

    通過正確使用 HTTP 方法和狀態碼,我們可以更好地定義 API 的邊界,使其行為更加清晰和可預測。


3. 案例分析:圖書館管理系統的 API 邊界

假設你正在設計一個圖書館管理系統的 API,我們可以通過以下方式來界定它的邊界:

  • 應該處理的功能:
    • 圖書管理: API 應該負責圖書的新增、查詢、更新和刪除。這些操作直接關係到圖書館的核心業務——書籍管理。
    • 借閱管理: API 還應該負責處理書籍的借閱和歸還,包括借閱記錄的維護,這是圖書館運作的關鍵部分。
  • 不應該處理的功能:
    • 身份驗證: 這部分應該由專門的身份驗證系統處理,API 只需接收來自身份驗證系統的驗證結果並據此進行操作。
    • 支付處理: 如果涉及到罰款或其它支付項目,應該由第三方支付服務來處理,API 只需與該支付系統進行集成,處理支付結果即可。

這樣的劃分可以幫助 API 專注於圖書館的核心業務,而不會因為捲入過多的非核心功能而變得複雜難維護。


今日小結:

界定 API 邊界是設計高效且可維護 API 的第一步。通過明確 API 的職責範圍,我們可以確保它專注於核心業務,並且能夠靈活應對未來的需求變更。這種精確的劃分可以避免過度設計和欠缺設計,從而提高整個系統的穩定性和可擴展性。明天,我們將在此基礎上進一步討論如何建立 API 模型,這將為 API 的實際開發奠定堅實的基礎。


上一篇
Day 3 什麼是Web API?
下一篇
Day 5 建立API模型(上)程式設計下的模型(Model)
系列文
使用 C# 從零開始玩轉 Web API,從基礎到微服務與雲端部署的全面探索22
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言