實戰範例 004:化繁為簡:移除過度設計的微服務 RPC 抽象
簡介與核心精神
在現代軟體工程中,微服務架構 (Microservices Architecture) 與遠端程序呼叫 (Remote Procedure Call, RPC) 被廣泛視為解決系統擴展性問題的銀彈。許多開發團隊在專案初期,即使業務邏輯相對單純、團隊規模不大,便急於導入微服務架構,設立了複雜的服務發現 (Service Discovery)、負載均衡 (Load Balancing)、以及各種 RPC 抽象層 (如 gRPC, Thrift, 或基於 RabbitMQ 的事件總線)。
然而,根據 Ponytail 的極簡主義開發哲學,「不要為了未來可能發生的問題,在今天寫下過於複雜的程式碼」。過早的微服務化會帶來巨大的認知負擔與維運成本。原本一個簡單的函式呼叫 (Function Call),在微服務架構下,變成了需要考慮網路延遲 (Network Latency)、序列化/反序列化 (Serialization/Deserialization)、超時重試 (Timeout & Retry)、斷路器 (Circuit Breaker) 以及分散式追蹤 (Distributed Tracing) 的複雜非同步操作。
本篇實戰範例將帶領讀者探討一個典型的「過度設計」場景:一個實際上部署在一起、共用同一個資料庫的「單體應用 (Monolith)」,卻因為追求時髦,內部模組之間採用了繁重的 RPC 抽象層進行通訊。我們將展示如何運用 Ponytail 哲學,將這些不必要的抽象移除,回歸簡單、可靠且高效的直接模組呼叫。
實際場景描述
我們正在維護一個電子商務系統的後端程式碼。這個系統目前由三位開發人員維護,所有的服務都打包在同一個 Docker 容器中運行,並且連線到同一個 PostgreSQL 資料庫。
但是,在前輩開發者的「宏偉藍圖」下,系統被強行拆分成了 OrderService、UserService 與 InventoryService。為了讓這些服務能夠在「未來」順利拆分成真實的微服務,前輩實作了一個名為 EnterpriseRpcDispatcher 的巨大抽象層。
當 OrderService 需要獲取使用者的基本資料來計算 VIP 折扣時,它不能直接引入 UserService 的方法,而是必須建構一個 Request 物件,將其發送到虛擬的 RPC 通道,然後等待 Response。這不僅讓除錯變得異常困難,也讓原本只需 1 毫秒的記憶體內呼叫,變成了需要經歷多層中介軟體 (Middleware) 處理的冗長過程。
原始的過度設計代碼
讓我們來看看這段令人窒息的原始程式碼。這段程式碼充滿了不必要的介面、抽象類別與序列化邏輯。
// 檔案:src/infrastructure/rpc/EnterpriseRpcDispatcher.ts
export interface RpcRequest {
serviceName: string;
methodName: string;
payload: string; // JSON 字串化後的資料
correlationId: string;
timestamp: number;
}
export interface RpcResponse {
statusCode: number;
data: string;
errorMessage?: string;
}
export class EnterpriseRpcDispatcher {
private registry: Map<string, any> = new Map();
public registerService(serviceName: string, instance: any) {
this.registry.set(serviceName, instance);
console.log(`[RPC] Service ${serviceName} registered.`);
}
public async dispatch(request: RpcRequest): Promise<RpcResponse> {
console.log(`[RPC] Dispatching request ${request.correlationId} to ${request.serviceName}.${request.methodName}`);
// 模擬網路延遲與中介軟體處理
await new Promise(resolve => setTimeout(resolve, 50));
const service = this.registry.get(request.serviceName);
if (!service) {
return { statusCode: 404, data: "", errorMessage: "Service not found" };
}
if (typeof service[request.methodName] !== 'function') {
return { statusCode: 400, data: "", errorMessage: "Method not found" };
}
try {
// 痛苦的解析過程
const parsedPayload = JSON.parse(request.payload);
const result = await service[request.methodName](parsedPayload);
return {
statusCode: 200,
data: JSON.stringify(result)
};
} catch (error: any) {
return { statusCode: 500, data: "", errorMessage: error.message };
}
}
}
// 檔案:src/services/UserService.ts
export class UserService {
public async getUserDiscountRate(payload: { userId: string }): Promise<{ rate: number }> {
// 模擬資料庫查詢
const user = { id: payload.userId, isVip: true };
if (user.isVip) {
return { rate: 0.8 };
}
return { rate: 1.0 };
}
}
// 檔案:src/services/OrderService.ts
import { EnterpriseRpcDispatcher, RpcRequest } from '../infrastructure/rpc/EnterpriseRpcDispatcher';
import { v4 as uuidv4 } from 'uuid';
export class OrderService {
constructor(private rpcDispatcher: EnterpriseRpcDispatcher) {}
public async calculateOrderTotal(userId: string, subTotal: number): Promise<number> {
const request: RpcRequest = {
serviceName: 'UserService',
methodName: 'getUserDiscountRate',
payload: JSON.stringify({ userId }),
correlationId: uuidv4(),
timestamp: Date.now()
};
const response = await this.rpcDispatcher.dispatch(request);
if (response.statusCode !== 200) {
throw new Error(`Failed to get user discount: ${response.errorMessage}`);
}
const responseData = JSON.parse(response.data);
const discountRate = responseData.rate;
return subTotal * discountRate;
}
}
在這段程式碼中,OrderService 為了取得一個簡單的折扣率,必須手動打包一個包含 correlationId 和字串化 payload 的 RpcRequest。更糟糕的是,型別安全 (Type Safety) 在通過 JSON.stringify 和 JSON.parse 時完全喪失了。開發者甚至不知道 responseData 裡面到底有沒有 rate 這個屬性。
開發者與 AI 的對話記錄
為了展示 Ponytail 哲學如何介入並改變這種局面,以下是開發者與 AI 代理之間的對話記錄。
開發者:
「AI,我們的
EnterpriseRpcDispatcher似乎遇到了一些效能瓶頸。當訂單量稍微增加時,這層虛擬的 RPC 分發器吃掉了太多 CPU 資源。我打算引入 gRPC 並使用 Protocol Buffers 來替換現在的 JSON 序列化機制。你覺得我們應該怎麼設計這個新的GrpcProtobufDispatcher?幫我產生 protobuf 的定義檔吧!」
AI (Ponytail 模式):
「等等,先停下來。在我們引入 gRPC 這種更重量級的基礎設施之前,我需要確認幾個關鍵問題。目前
OrderService和UserService是部署在不同的伺服器上嗎?」
開發者:
「沒有,我們團隊目前只有三個人,所有的服務都在同一個 Node.js 執行緒裡面跑,打包成單一的 Docker Image 部署在 AWS ECS 上。」
AI (Ponytail 模式):
「如果它們都在同一個記憶體空間內運行,為什麼你需要一個
EnterpriseRpcDispatcher來進行通訊?你現在面臨的效能瓶頸,正是因為你在不需要網路通訊的地方,強行模擬了網路通訊的序列化與非同步等待開銷。更重要的是,你失去了 TypeScript 寶貴的靜態型別檢查。」
開發者:
「但是前輩說,這樣設計是為了未來的『可擴展性 (Scalability)』!如果有一天我們需要將
UserService拆分成獨立的微服務,我們就不需要修改OrderService的商業邏輯,只需要把EnterpriseRpcDispatcher替換成真正的網路呼叫就好了。」
AI (Ponytail 模式):
「這就是典型的『為尚未發生的未來過度設計』(YAGNI - You Aren’t Gonna Need It)。你為了一個可能永遠不會發生的未來,付出了每天除錯困難、喪失型別安全、以及當下效能低落的代價。
真正的模組化 (Modularity) 不在於你是否使用 RPC 框架,而在於模組之間的『耦合度 (Coupling)』。只要
OrderService依賴的是UserService的介面 (Interface) 或公開方法,未來就算要拆分,也只需要實作一個UserServiceClient代理類別即可。現在,讓我們把這層厚重的 RPC 垃圾丟掉,回歸最純粹的函式呼叫。」
開發者:
「聽起來很有道理…那我該怎麼重構?把 JSON 序列化都砍掉嗎?」
AI (Ponytail 模式):
「沒錯。我們直接讓
OrderService透過依賴注入 (或直接實例化) 的方式呼叫UserService的方法。保留 TypeScript 的強型別,移除所有的字串轉換、中介軟體延遲與繁瑣的錯誤代碼判斷。」
重構與優化:回歸直接呼叫
在 Ponytail 理念的指導下,我們將冗長的 EnterpriseRpcDispatcher 完全刪除。取而代之的,是清晰、直接且具備 100% 型別安全的依賴關係。
// 檔案:src/services/UserService.ts
// 定義清晰的介面與型別
export interface UserDiscountResult {
rate: number;
}
export class UserService {
// 直接回傳型別化資料,不再有 JSON 字串
public async getUserDiscountRate(userId: string): Promise<UserDiscountResult> {
// 邊界檢查與安全防護
if (!userId || userId.trim() === '') {
throw new Error("Invalid user ID provided for discount calculation.");
}
// 模擬資料庫查詢
const user = { id: userId, isVip: true };
// 商業邏輯
if (user.isVip) {
return { rate: 0.8 };
}
return { rate: 1.0 };
}
}
// 檔案:src/services/OrderService.ts
import { UserService } from './UserService';
export class OrderService {
// 直接依賴 UserService 實例 (可以透過 DI 容器或手動注入)
constructor(private userService: UserService) {}
public async calculateOrderTotal(userId: string, subTotal: number): Promise<number> {
// 邊界檢查:確保 subTotal 合法
if (subTotal < 0) {
throw new Error("Subtotal cannot be negative.");
}
try {
// 簡單直接的方法呼叫,擁有完整的 IDE 提示與型別安全!
const discountResult = await this.userService.getUserDiscountRate(userId);
const finalTotal = subTotal * discountResult.rate;
return finalTotal;
} catch (error) {
// 保留必要的異常處理,不再解析神祕的 statusCode
console.error(`Error calculating order total for user ${userId}:`, error);
throw new Error("Order calculation failed due to internal service error.");
}
}
}
// 檔案:src/index.ts (系統進入點)
import { UserService } from './services/UserService';
import { OrderService } from './services/OrderService';
// 簡單直接的裝配 (Wiring)
const userService = new UserService();
const orderService = new OrderService(userService);
// 執行業務邏輯,沒有任何多餘的 RPC 分發器開銷
orderService.calculateOrderTotal('user-123', 1000)
.then(total => console.log(`Final Total: ${total}`))
.catch(err => console.error(err));
重構亮點分析
- 型別安全的回歸:不再有
JSON.parse(response.data)。discountResult.rate現在受到 TypeScript 編譯器的嚴格保護,如果UserService更改了回傳結構,編譯期就會報錯。 - 消滅無意義的延遲:直接函式呼叫的耗時幾乎為零,移除了模擬網路傳輸的虛擬等待,系統處理吞吐量大幅提升。
- 錯誤處理的簡化:移除了複雜的
statusCode與errorMessage封裝,直接使用標準的try...catch進行異常捕獲。 - 程式碼行數的銳減:移除了數十行的
RpcRequest、RpcResponse定義以及EnterpriseRpcDispatcher的冗長邏輯,讓新加入的開發者能在一分鐘內看懂業務流程。
效益分析表格與解讀
下表量化了這次「反向工程」重構所帶來的具體效益:
| 評估維度 | 原始過度設計 (RPC 抽象) | 重構後 (Ponytail 直接呼叫) | 改善程度 / 解讀 |
|---|---|---|---|
| 程式碼行數 (LOC) | 120+ 行 (含 Dispatcher) | 40+ 行 | 減少 66%。移除了所有處理序列化、路由註冊的基礎設施代碼。 |
| 通訊延遲 | 50ms+ (序列化/反序列化與虛擬等待) | < 0.1ms (記憶體內調用) | 效能極大化。徹底根除了因為字串轉換與非同步造成的效能瓶頸。 |
| 型別安全性 | 極低 (依賴 Any 與 JSON 解析) | 極高 (嚴格的 TypeScript 介面) | 編譯期保證。開發者在 IDE 中即可獲得完整的屬性提示,避免 Runtime 錯誤。 |
| 除錯難度 | 高 (需追蹤 CorrelationID 與 Event Log) | 低 (標準的 Stack Trace) | 發生錯誤時,呼叫堆疊 (Stack Trace) 清晰可見,不再被 Dispatcher 截斷。 |
| 架構複雜度 | 微服務級別 (單體應用硬穿微服務外套) | 模組化單體 (Modular Monolith) | 符合團隊當前規模,專注於業務邏輯,將維運成本降至最低。 |
結論總結
「過早的最佳化是萬惡的根源,過早的微服務化則是架構災難的開端。」
透過移除虛構的 RPC 抽象層,我們證明了 Ponytail 的極簡哲學:在沒有真實的實體隔離需求之前,最簡單的函式呼叫永遠是最好的選擇。這不僅減少了程式碼的體積,更實質地提升了系統穩定性與開發效率。如果有一天,業務真的成長到需要將 UserService 獨立部署,我們只需實作一個實作相同介面的 RemoteUserService 替換進去即可,現在不需要為此付出任何代價。