微服務之道:8個原則,打造高效的微服務體系

2023-06-01 18:00:15

hi,我是熵減,見字如面。

現在,在大型的軟體工程系統中,微服務化的系統設計,成為了大部分時候的必然之選。

而如何將微服務做有效的設計,則是需要每一個團隊和工程師都需要考慮的一個問題。在保持系統的一致性、可理解性、可維護性和可延伸性上,需要有一些基本的指導原則。

下面分享微服務設計和實踐中的8個基礎原則,具體如下:

基本原則概覽

在微服務中,設計API時可以遵循以下原則:

  • 單一職責原則(Single Responsibility Principle):每個微服務應該只關注一個特定的業務領域或功能,因此其API應該具有明確的職責,只提供與該領域或功能相關的介面。

  • 顯式介面原則(Explicit Interface Principle):API應該明確和清晰地定義其介面,包括輸入引數、輸出結果和可能的錯誤碼。這樣可以提高介面的可理解性和可維護性,減少誤用和不必要的溝通。

  • 界限上下文(Bounded Context):根據微服務的邊界和業務需求,劃分出不同的界限上下文。API的設計應該與界限上下文相匹配,以確保介面的一致性和內聚性。

  • 服務契約(Service Contract):API應該建立明確的服務契約,包括介面的語意、協定和版本管理。這有助於不同團隊之間的共同作業,確保服務之間的相容性和互操作性。

  • 資訊隱藏(Information Hiding):API應該隱藏內部實現細節,只暴露必要的介面。這樣可以減少對內部實現的依賴性,提高服務的獨立性和可替代性。

  • 無狀態性(Statelessness):儘量設計無狀態的API,即不儲存使用者端的狀態資訊,每個請求都應該是獨立的。這樣可以提高服務的可伸縮性和容錯性。

  • 適應性與演進性(Adaptability and Evolution):API應該具有適應性和演進性,能夠容易地適應不同的需求和變化。通過版本控制和向後相容性,可以使API在不破壞現有使用者端的情況下進行演進和升級。

  • 安全性和身份驗證(Security and Authentication):API應該提供適當的安全機制和身份驗證,以保護服務和資料的安全性。這可能包括使用加密傳輸、身份驗證令牌和存取控制等措施。

接下來,我們將通過日常的一些具體demo,來逐個展開這8個原則。

原則具體範例

單一職責原則

以下是一個違反單一職責原則的反例,使用Java程式碼進行演示:

public class UserAPI {
    
    public void createUser(String username, String password) {
        // 建立使用者的邏輯
    }
    
    public void getUserInfo(String username) {
        // 獲取使用者資訊的邏輯
    }
    
    public void updateUser(String username, String newPassword) {
        // 更新使用者密碼的邏輯
    }
    
    public void deleteUser(String username) {
        // 刪除使用者的邏輯
    }
    
    public void sendEmail(String username, String subject, String content) {
        // 傳送郵件的邏輯
    }
    
}

在上述程式碼中,UserAPI 類違反了單一職責原則。它既包含了使用者管理的功能(建立、獲取、更新、刪除使用者),又包含了郵件傳送的功能。這使得這個類承擔了多個不同的職責,導致了耦合度增加、程式碼複雜度提高和可維護性下降的問題。

根據單一職責原則,應該將不同的職責拆分為獨立的類或模組。

對於上述例子,可以將使用者管理和郵件傳送拆分為兩個單獨的類,分別負責各自的功能。這樣可以提高程式碼的可讀性、可維護性和擴充套件性。

public class UserManagementAPI {
    
    public void createUser(String username, String password) {
        // 建立使用者的邏輯
    }
    
    public void getUserInfo(String username) {
        // 獲取使用者資訊的邏輯
    }
    
    public void updateUser(String username, String newPassword) {
        // 更新使用者密碼的邏輯
    }
    
    public void deleteUser(String username) {
        // 刪除使用者的邏輯
    }
    
}

public class EmailService {
    
    public void sendEmail(String username, String subject, String content) {
        // 傳送郵件的邏輯
    }
    
}

通過將功能進行拆分,每個類只關注一個特定的職責,程式碼變得更加清晰、可維護,並符合單一職責原則。

顯式介面原則

以下是一個違反顯式介面原則的反例,使用Java程式碼進行演示:

public class Calculator {
    
    public int calculate(int a, int b, String operation) {
        if (operation.equals("add")) {
            return a + b;
        } else if (operation.equals("subtract")) {
            return a - b;
        } else if (operation.equals("multiply")) {
            return a * b;
        } else if (operation.equals("divide")) {
            return a / b;
        }
        return 0;
    }
    
}

在上述程式碼中,Calculator 類違反了顯式介面原則。它使用了一個字串引數 operation 來決定進行何種計算操作。這種設計方式不明確和不清晰,沒有明確定義介面和輸入輸出的結構,使用方在呼叫時無法準確理解和使用該介面。

根據顯式介面原則,應該明確和清晰地定義介面,包括輸入引數、輸出結果和可能的錯誤碼。以下是一個改進的範例:

public interface Calculator {
    
    int add(int a, int b);
    
    int subtract(int a, int b);
    
    int multiply(int a, int b);
    
    int divide(int a, int b);
    
}

通過定義明確的介面,使用方可以清晰地知道介面的輸入和輸出,而不需要傳遞一個字串引數來確定操作型別。這樣可以提高介面的可理解性和可維護性,遵循了顯式介面原則。

public class SimpleCalculator implements Calculator {
    
    @Override
    public int add(int a, int b) {
        return a + b;
    }
    
    @Override
    public int subtract(int a, int b) {
        return a - b;
    }
    
    @Override
    public int multiply(int a, int b) {
        return a * b;
    }
    
    @Override
    public int divide(int a, int b) {
        return a / b;
    }
    
}

通過實現明確的介面,使用方可以根據介面定義來呼叫相應的方法,而無需傳遞一個字串引數來指定操作。這樣可以提高程式碼的可讀性、可維護性和擴充套件性,並符合顯式介面原則。

界限上下文

界限上下文原則(Bounded Context)的主要目標是將大型系統拆分為不同的上下文,每個上下文專注於一個特定的業務領域,並且在該上下文內保持一致性。以下是一個違反界限上下文原則的反例:

假設有一個電子商務系統,其中包含訂單管理和庫存管理兩個領域。下面是一個違反界限上下文原則的實現範例:

public class OrderService {
    
    public void createOrder(Order order) {
        // 建立訂單的邏輯
    }
    
    public void updateOrderStatus(Order order, String status) {
        // 更新訂單狀態的邏輯
    }
    
    public void reserveStock(Order order) {
        // 預留庫存的邏輯
    }
    
    public void releaseStock(Order order) {
        // 釋放庫存的邏輯
    }
    
    public void updateStock(Order order) {
        // 更新庫存的邏輯
    }
    
}

在上述程式碼中,OrderService 類同時包含了訂單管理和庫存管理的邏輯,違反了界限上下文原則。應該將這兩個業務領域拆分為獨立的上下文,分別負責各自的功能。

public class OrderService {
    
    public void createOrder(Order order) {
        // 建立訂單的邏輯
    }
    
    public void updateOrderStatus(Order order, String status) {
        // 更新訂單狀態的邏輯
    }
    
}

public class InventoryService {
    
    public void reserveStock(Order order) {
        // 預留庫存的邏輯
    }
    
    public void releaseStock(Order order) {
        // 釋放庫存的邏輯
    }
    
    public void updateStock(Order order) {
        // 更新庫存的邏輯
    }
    
}

通過將訂單管理和庫存管理拆分為獨立的服務,每個服務只關注自己領域的邏輯,程式碼變得更加清晰、可維護,並符合界限上下文原則。這樣可以減少不同領域之間的耦合度,提高程式碼的模組化和可延伸性。

服務契約

服務契約(Service Contracts)旨在定義服務之間的明確契約,包括輸入引數、輸出結果和可能的錯誤碼。

假設我們有一個使用者管理服務,其中有一個方法用於更新使用者資訊。下面是一個違反服務契約原則的範例:

public class UserService {
    
    public void updateUser(String userId, String newEmail) {
        // 更新使用者資訊的邏輯
    }
    
}

在上述程式碼中,updateUser 方法只接受使用者ID和新的電子郵件地址作為引數,但是它沒有返回任何結果或處理任何錯誤情況。這違反了服務契約原則,因為它沒有明確定義輸入引數、輸出結果和錯誤處理。

改進的做法是明確定義服務的契約,包括輸入引數、輸出結果和錯誤處理。以下是一個改進的範例:

public class UserService {
    
    public UpdateUserResponse updateUser(UpdateUserRequest request) {
        // 更新使用者資訊的邏輯
        // ...
        UpdateUserResponse response = new UpdateUserResponse();
        // 設定更新結果
        return response;
    }
    
}

public class UpdateUserRequest {
    private String userId;
    private String newEmail;
    // 其他相關屬性和方法
    
    // Getters and setters
}

public class UpdateUserResponse {
    private boolean success;
    private String errorMessage;
    // 其他相關屬性和方法
    
    // Getters and setters
}

通過引入UpdateUserRequest和UpdateUserResponse物件來明確定義輸入引數和輸出結果的結構,我們可以更好地遵循服務契約原則。使用方可以清楚地瞭解服務的輸入引數型別、輸出結果型別以及如何處理錯誤情況。這樣可以提高程式碼的可讀性、可維護性和擴充套件性,並確保服務之間的契約明確。

資訊隱藏

資訊隱藏原則(Information Hiding Principle)是物件導向設計中的一個原則,它強調將類的內部細節隱藏起來,只暴露必要的介面給外部使用。

以下是一個違反資訊隱藏原則的反例:

public class BankAccount {
    
    public String accountNumber;
    public String accountHolder;
    public double balance;
    
    public void deposit(double amount) {
        balance += amount;
    }
    
    public void withdraw(double amount) {
        balance -= amount;
    }
    
    public void displayAccountInfo() {
        System.out.println("Account Number: " + accountNumber);
        System.out.println("Account Holder: " + accountHolder);
        System.out.println("Balance: " + balance);
    }
}

在上述程式碼中,BankAccount 類違反了資訊隱藏原則。它將賬戶號碼、賬戶持有人和餘額都宣告為公共的屬性,可以被外部直接存取和修改。同時,displayAccountInfo 方法也直接列印賬戶資訊到控制檯。

改進的做法是將類的內部狀態和實現細節封裝起來,只提供必要的介面供外部存取和操作。以下是一個改進的範例:

public class BankAccount {
    
    private String accountNumber;
    private String accountHolder;
    private double balance;
    
    public BankAccount(String accountNumber, String accountHolder) {
        this.accountNumber = accountNumber;
        this.accountHolder = accountHolder;
        this.balance = 0;
    }
    
    public void deposit(double amount) {
        balance += amount;
    }
    
    public void withdraw(double amount) {
        balance -= amount;
    }
    
    public void displayAccountInfo() {
        System.out.println("Account Number: " + accountNumber);
        System.out.println("Account Holder: " + accountHolder);
        System.out.println("Balance: " + balance);
    }
}

在改進後的程式碼中,將賬戶的屬性宣告為私有,並提供了公共的方法來進行存款、取款和顯示賬戶資訊的操作。外部程式碼無法直接存取和修改賬戶的內部狀態,只能通過提供的介面來與賬戶物件進行互動,保護了類的內部實現細節並提高了封裝性。

無狀態性

無狀態性原則(Statelessness Principle)強調在設計中避免儲存使用者端的狀態資訊,每個請求應該是獨立且自包含的。

以下是一個違反無狀態性原則的反例:

public class ShoppingCart {
    
    private List<Item> items;
    private double totalPrice;
    
    public void addItem(Item item) {
        items.add(item);
        totalPrice += item.getPrice();
    }
    
    public void removeItem(Item item) {
        items.remove(item);
        totalPrice -= item.getPrice();
    }
    
    public double getTotalPrice() {
        return totalPrice;
    }
    
    // 其他方法
}

在上述程式碼中,ShoppingCart 類儲存了使用者端的狀態資訊,包括購物車中的商品列表和總價格。每次新增或移除商品時,都會更新購物車中的商品列表和總價格。這違反了無狀態性原則,因為購物車的狀態資訊依賴於之前的操作,並且隨著時間的推移會發生變化。

改進的做法是使購物車變得無狀態,每個請求都是獨立的。以下是一個改進的範例:

public class ShoppingCart {
    
    public double calculateTotalPrice(List<Item> items) {
        double totalPrice = 0;
        for (Item item : items) {
            totalPrice += item.getPrice();
        }
        return totalPrice;
    }
    
    // 其他方法
}

在改進後的程式碼中,我們將購物車改為一個無狀態的類,不儲存任何狀態資訊。相反,我們提供了一個 calculateTotalPrice 方法,接收商品列表作為引數,並根據傳入的列表計算總價格。每次呼叫該方法時,都是獨立的,不依賴於之前的操作,保持了無狀態性。

這樣設計可以更好地遵循無狀態性原則,使每個請求都是獨立的,無需儲存使用者端的狀態資訊,提高了可伸縮性和可靠性。

適應性與演進性

適應性與演進性原則(Adaptability and Evolvability Principle)強調系統的設計應具備適應變化和演進的能力,能夠靈活應對新需求和技術的引入。

以下是一個違反適應性與演進性原則的反例:

public class PaymentService {
    
    public void processPayment(String paymentMethod, double amount) {
        if (paymentMethod.equals("creditCard")) {
            // 處理信用卡支付邏輯
            System.out.println("Processing credit card payment...");
        } else if (paymentMethod.equals("paypal")) {
            // 處理PayPal支付邏輯
            System.out.println("Processing PayPal payment...");
        } else if (paymentMethod.equals("applePay")) {
            // 處理Apple Pay支付邏輯
            System.out.println("Processing Apple Pay payment...");
        } else {
            throw new IllegalArgumentException("Unsupported payment method");
        }
    }
    
    // 其他方法
}

在上述程式碼中,PaymentService 類中的 processPayment 方法根據傳入的支付方式進行相應的支付處理。這種實現方式違反了適應性與演進性原則,因為當引入新的支付方式時,需要修改 processPayment 方法的程式碼來新增新的邏輯分支。

改進的做法是通過介面和策略模式來實現支付方式的適應性和演進性。以下是一個改進的範例:

public interface PaymentMethod {
    void processPayment(double amount);
}

public class CreditCardPayment implements PaymentMethod {
    @Override
    public void processPayment(double amount) {
        // 處理信用卡支付邏輯
        System.out.println("Processing credit card payment...");
    }
}

public class PayPalPayment implements PaymentMethod {
    @Override
    public void processPayment(double amount) {
        // 處理PayPal支付邏輯
        System.out.println("Processing PayPal payment...");
    }
}

public class ApplePayPayment implements PaymentMethod {
    @Override
    public void processPayment(double amount) {
        // 處理Apple Pay支付邏輯
        System.out.println("Processing Apple Pay payment...");
    }
}

public class PaymentService {
    
    public void processPayment(PaymentMethod paymentMethod, double amount) {
        paymentMethod.processPayment(amount);
    }
    
    // 其他方法
}

在改進後的程式碼中,我們定義了一個 PaymentMethod 介面,併為每種支付方式實現了具體的實現類。

PaymentService 類的 processPayment 方法接收一個 PaymentMethod 物件作為引數,並呼叫相應的支付方式的 processPayment 方法進行支付處理。

通過介面和策略模式的使用,我們使得支付方式的新增和修改變得更加靈活和可延伸,符合適應性與演進性原則。

安全性和身份驗證

安全性和身份驗證原則(Security and Authentication Principle)強調在系統設計中應該考慮安全性需求,並採取適當的身份驗證措施來保護系統和使用者的安全。

以下是一個違反安全性和身份驗證原則的反例:

public class UserController {
    
    public UserDTO getUser(String userId) {
        // 根據使用者ID查詢使用者資訊
        return userRepository.findUserById(userId);
    }
    
    public void updateUser(String userId, UserDTO updatedUser) {
        // 更新使用者資訊
        userRepository.updateUser(userId, updatedUser);
    }
    
    // 其他方法
}

在上述程式碼中,UserController 類提供了獲取使用者和更新使用者資訊的方法。然而,該實現沒有進行任何身份驗證或安全性檢查,任何人只要知道使用者ID就可以獲取和更新使用者資訊。

改進的做法是引入身份驗證和安全性措施來保護使用者資訊。以下是一個改進的範例:

public class UserController {
    
    private AuthenticationService authenticationService;
    private UserRepository userRepository;
    
    public UserDTO getUser(String userId, String authToken) {
        if (!authenticationService.isAuthenticated(authToken)) {
            throw new SecurityException("Unauthorized access");
        }
        // 根據使用者ID查詢使用者資訊
        return userRepository.findUserById(userId);
    }
    
    public void updateUser(String userId, UserDTO updatedUser, String authToken) {
        if (!authenticationService.isAuthenticated(authToken)) {
            throw new SecurityException("Unauthorized access");
        }
        // 更新使用者資訊
        userRepository.updateUser(userId, updatedUser);
    }
    
    // 其他方法
}

在改進後的程式碼中,我們引入了 AuthenticationService 來進行身份驗證,並在獲取使用者和更新使用者資訊的方法中進行驗證。

如果身份驗證失敗,將丟擲一個安全異常,阻止未經授權的存取。

這樣,使用者資訊的存取和修改將受到身份驗證和安全性保護,符合安全性和身份驗證原則。

最後

以上的8個基本原則,可以作為我們日常設計微服務API的指導,來幫助團隊確保服務的一致性、可理解性、可維護性和可延伸性。

然而,具體的微服務的API設計,還應根據具體的專案的特定需求、團隊的技術棧和業務場景做出適當的調整和決策。