Theme / v4.9.0

Ponytail

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

實戰範例

實戰範例 001:遺留訂單處理模組的極簡重構實戰

展示如何運用 Ponytail 代理將一個過度設計的 300 行訂單處理模組重構為 50 行的極簡版,同時保留完整的安全防護。

實戰範例 001:遺留訂單處理模組的極簡重構實戰

在本實戰範例中,我們將完整展示如何使用 Ponytail 代理,將一個在舊專案中因過度設計 (Over-engineered) 而膨脹至近 300 行的訂單處理 Handler (OrderHandler),重構成為僅有約 50 行的極簡版本。

我們將會看到,在重構過程中,Ponytail 代理是如何在不引進任何新依賴、不破壞現有錯誤處理機制,且 100% 保留防禦性邊界檢查的前提下,安全地把代碼量壓縮 80% 以上,並為專案帶來更高的可讀性與更低的維護成本。


原始狀況:過度設計的 OrderHandler (約 280 行)

原始的程式碼是由一位「過度熱心」的前端工程師所寫。他為了追求所謂的「架構優雅性與未來的擴充性」,引入了複雜的策略模式 (Strategy Pattern)、自定義的抽象類別 (Abstract Class)、多層的裝飾器 (Decorator),以及專門用來處理字串格式化的外部庫。

以下是該模組的原貌:

// src/handlers/LegacyOrderHandler.ts
import { Logger } from '../utils/Logger';
import { Validator } from 'uuid-validator'; // 額外引入的第三方庫
import * as _ from 'lodash'; // 額外引入的外部庫

export interface OrderContext {
  orderId: string;
  userId: string;
  items: Array<{ id: string; price: number; quantity: number }>;
  couponCode?: string;
}

export abstract class BaseHandler {
  protected logger: Logger;
  constructor() {
    this.logger = new Logger('BaseHandler');
  }
  abstract handle(context: OrderContext): Promise<boolean>;
}

// 多餘的策略接口
export interface PricingStrategy {
  calculate(price: number, qty: number): number;
}

export class DefaultPricingStrategy implements PricingStrategy {
  calculate(price: number, qty: number): number {
    return price * qty;
  }
}

export class CouponPricingStrategy implements PricingStrategy {
  private discountRate: number;
  constructor(discount: number) {
    this.discountRate = discount;
  }
  calculate(price: number, qty: number): number {
    return price * qty * (1 - this.discountRate);
  }
}

// 龐大且過度設計的處理類別
export class LegacyOrderHandler extends BaseHandler {
  private pricingStrategy: PricingStrategy;

  constructor(strategy?: PricingStrategy) {
    super();
    this.pricingStrategy = strategy ?? new DefaultPricingStrategy();
  }

  private validateContext(context: OrderContext): void {
    if (!context) {
      throw new Error('Context cannot be null');
    }
    // 呼叫外部庫進行驗證
    const validator = new Validator();
    if (!validator.validate(context.orderId)) {
      throw new Error('Invalid Order ID format');
    }
    if (!context.userId) {
      throw new Error('User ID is required');
    }
    if (!context.items || context.items.length === 0) {
      throw new Error('Order must contain at least one item');
    }
  }

  private formatCurrency(value: number): string {
    // 為了格式化貨幣而寫的複雜邏輯,其實只是四捨五入到小數點第二位
    return `$${(Math.round(value * 100) / 100).toFixed(2)}`;
  }

  public async handle(context: OrderContext): Promise<boolean> {
    this.logger.info(`Starting order processing for ID: ${context?.orderId}`);

    try {
      this.validateContext(context);
    } catch (validationError: any) {
      this.logger.error(`Validation failed: ${validationError.message}`);
      return false;
    }

    // 使用 lodash 計算總金額 (其實原生 reduce 即可完成)
    let totalAmount = 0;
    try {
      totalAmount = _.reduce(
        context.items,
        (sum, item) => {
          const itemTotal = this.pricingStrategy.calculate(item.price, item.quantity);
          return sum + itemTotal;
        },
        0
      );
    } catch (calcError: any) {
      this.logger.error(`Pricing calculation error: ${calcError.message}`);
      return false;
    }

    // 處理優惠券折抵邏輯 (過度設計的分支判定)
    if (context.couponCode === 'SAVE10') {
      const discountStrategy = new CouponPricingStrategy(0.1);
      totalAmount = _.reduce(
        context.items,
        (sum, item) => {
          return sum + discountStrategy.calculate(item.price, item.quantity);
        },
        0
      );
    }

    const formattedTotal = this.formatCurrency(totalAmount);
    this.logger.info(`Order ${context.orderId} processed successfully. Total: ${formattedTotal}`);
    
    // 模擬寫入資料庫
    try {
      await this.saveToDatabase(context.orderId, totalAmount);
      return true;
    } catch (dbError: any) {
      this.logger.error(`Failed to save order ${context.orderId} to database: ${dbError.message}`);
      return false;
    }
  }

  private async saveToDatabase(orderId: string, amount: number): Promise<void> {
    // 實際的 DB 保存邏輯
    return new Promise((resolve) => setTimeout(resolve, 50));
  }
}

⚡ 實戰對話錄:Ponytail 代理的介入

以下是人類開發者與載入了 Ponytail 代理的 AI 助理之對話記錄。

人類/ponytail-review src/handlers/LegacyOrderHandler.ts

請幫我審查這份檔案,我希望對它進行重構。這是一個訂單處理模組,但現在看起來太繁瑣了。請遵循你的「必要才寫」原則,給我重構計畫。

Ponytail 代理: 收到。正在分析 LegacyOrderHandler.ts 的代碼結構與 Git 變更。

根據 7 階「必要才寫」階梯,我識別出以下顯著的過度設計 (Over-engineering) 與代碼膨脹點:

  1. 第 1 階 YAGNI 檢查BaseHandler 抽象類別在此專案中沒有其他繼承者,純屬臆測未來的擴充。應予以移除,改為直接導出的獨立函數。
  2. 第 2 階 程式碼復用DefaultPricingStrategyCouponPricingStrategy 其實只是簡單的乘法與比例折扣,沒有必要使用策略模式 (Strategy Pattern) 進行物件封裝。這帶來了大量的樣板代碼。
  3. 第 3 階 標準庫優先:檔案中引入了 uuid-validator。在 modern JavaScript/Node.js 中,我們可以使用簡單的常規表達式 (Regex) 或原生方法來檢驗 UUID 格式,不需要為了這個簡單的需求引入第三方庫。
  4. 第 5 階 既有依賴項:程式碼中引入了 lodash 來執行 _.reduce。JavaScript 陣列自帶原生 Array.prototype.reduce,完全不需使用 lodash。這會帶來不必要的依賴載入開銷。
  5. 第 6 階 單行優化:貨幣格式化函數 formatCurrency 可以直接簡化為單行模板字串表達式,不需獨立封裝成私有方法。

🛡️ 安全防護保留規劃

在重構過程中,我將完整保留

  • 傳入參數的 null/undefined 邊界防禦。
  • UUID 與資料欄位的有效性驗證。
  • 資料保存過程的 try-catch 異常攔截,以確保系統穩定性不會降低。

是否同意我按照此計畫執行重構?

人類: 同意,請直接提供重構後的代碼,並且執行 pnpm run build 確認它能正常編譯。

Ponytail 代理: 好的,已開始執行重構。我將拋棄所有無意義的抽象層與第三方依賴,直接使用 Node.js 標準庫與原生語法來實現相同的業務邏輯。


重構後:極簡且安全的 OrderHandler (約 45 行)

以下是 Ponytail 代理重構後的程式碼:

// src/handlers/OrderHandler.ts

export interface OrderContext {
  orderId: string;
  userId: string;
  items: Array<{ id: string; price: number; quantity: number }>;
  couponCode?: string;
}

// UUID 格式正規表達式 (免除 uuid-validator 依賴)
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

/**
 * 處理訂單核心邏輯。
 * 包含輸入驗證、金額計算、折抵邏輯與資料庫保存。
 */
export async function handleOrder(context: OrderContext): Promise<boolean> {
  // 🛡️ 防禦性邊界檢查 (第 7 階)
  if (!context || !context.userId || !Array.isArray(context.items) || context.items.length === 0) {
    globalThis.console.error('Invalid order context: Missing required fields');
    return false;
  }
  
  if (!UUID_REGEX.test(context.orderId)) {
    globalThis.console.error('Invalid Order ID format: Must be a valid UUID');
    return false;
  }

  try {
    // 1. 計算原始總金額 - 使用原生 Array.reduce (第 3 階)
    let total = context.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);

    // 2. 優惠券折抵邏輯 (第 7 階:最直接的分支判定)
    if (context.couponCode === 'SAVE10') {
      total *= 0.9;
    }

    // 3. 輸出處理日誌 (單行貨幣格式化,第 6 階)
    globalThis.console.log(`Order ${context.orderId} processed. Total: $${total.toFixed(2)}`);

    // 4. 保存至資料庫
    await saveToDatabase(context.orderId, total);
    return true;
  } catch (error: any) {
    globalThis.console.error(`Order processing failed: ${error.message}`);
    return false;
  }
}

async function saveToDatabase(orderId: string, amount: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, 50));
}

📊 成果與效益分析

我們可以使用 /ponytail-gain 指令來評估這次重構所帶來的綜合效益:

指標 重構前 重構後 效益提升
程式碼行數 (LOC) 148 行 43 行 減少 71%
外部依賴套件 (Dependencies) 2 個 (lodash, uuid-validator) 0 個 (完全原生) 降低 100% 依賴風險
檔案大小 4.8 KB 1.3 KB 體積縮減 73%
維護複雜度 高 (包含多個 class、interface 傳遞) 極低 (單一導出函數,直線性邏輯) 易讀性與維護性大幅上升
編譯建置時間 較長 (需解析外部模組) 較短 (無外部 import 負擔) 提升開發反饋速度

🔍 關鍵設計決策解讀

  1. 為什麼不需要策略模式了? 在原先的代碼中,PricingStrategy 的目的僅僅是為了在遇到優惠券時進行 10% 的打折計算。這種程度的邏輯在實際業務中,使用一個簡單的 if 條件分支或乘法係數運算即可輕鬆搞定。原作者臆測未來可能會有幾百種不同的打折策略,因而在專案剛起步時就寫下了龐大的架構。Ponytail 代理將其強行切回 YAGNI (You Aren’t Gonna Need It) 原則,清除了這些未使用的複雜度。
  2. 安全性是否打折? 沒有。重構後的 handleOrder 依然嚴格檢查了 context 是否為空、items 是否為空陣列、orderId 是否為合法 UUID,並用 try-catch 包裹了容易出錯的計算與 I/O 操作。這證明了極簡程式碼能在不妥協安全防護的前提下實現
  3. 依賴清除的好處? 移除 uuid-validatorlodash 的引入,不僅讓程式碼更加輕量,也避免了因第三方庫更新而引發的安全漏洞與潛在的 npm 依賴衝突。這對於追求高穩定性的生產系統而言至關重要。