Theme / v4.9.0

Ponytail

讓 AI 編碼代理像最懶的資深工程師

實戰範例

實戰範例 004:化繁為簡:移除過度設計的微服務 RPC 抽象

探討如何將不必要的微服務 RPC 抽象層移除,回歸簡單的單體架構或直接模組呼叫,減少網路延遲與除錯成本。

實戰範例 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 資料庫。

但是,在前輩開發者的「宏偉藍圖」下,系統被強行拆分成了 OrderServiceUserServiceInventoryService。為了讓這些服務能夠在「未來」順利拆分成真實的微服務,前輩實作了一個名為 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 和字串化 payloadRpcRequest。更糟糕的是,型別安全 (Type Safety) 在通過 JSON.stringifyJSON.parse 時完全喪失了。開發者甚至不知道 responseData 裡面到底有沒有 rate 這個屬性。

開發者與 AI 的對話記錄

為了展示 Ponytail 哲學如何介入並改變這種局面,以下是開發者與 AI 代理之間的對話記錄。

開發者

「AI,我們的 EnterpriseRpcDispatcher 似乎遇到了一些效能瓶頸。當訂單量稍微增加時,這層虛擬的 RPC 分發器吃掉了太多 CPU 資源。我打算引入 gRPC 並使用 Protocol Buffers 來替換現在的 JSON 序列化機制。你覺得我們應該怎麼設計這個新的 GrpcProtobufDispatcher?幫我產生 protobuf 的定義檔吧!」

AI (Ponytail 模式)

「等等,先停下來。在我們引入 gRPC 這種更重量級的基礎設施之前,我需要確認幾個關鍵問題。目前 OrderServiceUserService 是部署在不同的伺服器上嗎?」

開發者

「沒有,我們團隊目前只有三個人,所有的服務都在同一個 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));

重構亮點分析

  1. 型別安全的回歸:不再有 JSON.parse(response.data)discountResult.rate 現在受到 TypeScript 編譯器的嚴格保護,如果 UserService 更改了回傳結構,編譯期就會報錯。
  2. 消滅無意義的延遲:直接函式呼叫的耗時幾乎為零,移除了模擬網路傳輸的虛擬等待,系統處理吞吐量大幅提升。
  3. 錯誤處理的簡化:移除了複雜的 statusCodeerrorMessage 封裝,直接使用標準的 try...catch 進行異常捕獲。
  4. 程式碼行數的銳減:移除了數十行的 RpcRequestRpcResponse 定義以及 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 替換進去即可,現在不需要為此付出任何代價。