「程式跑得出來只是基本,跑得快才是王道!」在實務開發中,效能問題更是影響用戶體驗的關鍵。
在 Day 15 我們完成了羅馬數字轉換器,功能雖然完整,但如果要處理大量轉換呢?今天讓我們用測試驅動的方式來優化效能 ⚡。
今天我們將經歷四個階段:建立基準 → 快取機制 → 演算法改進 → 驗證比較
在進行任何優化前,我們需要先知道「現在有多慢」。就像減肥前要先量體重一樣,沒有基準就無法知道改善了多少。
建立效能測試檔案:
// 建立 tests/day16/performance.test.ts
import { describe, test, expect } from 'vitest'
import { toRoman, fromRoman } from '../../src/roman/converter'
describe('Roman Numeral Performance Tests', () => {
test('performanceBenchmark', () => {
const start = performance.now()
for (let i = 1; i <= 3999; i++) {
toRoman(i)
}
const end = performance.now()
const duration = end - start
expect(duration).toBeLessThan(100)
console.log(`toRoman: ${duration.toFixed(2)}ms for 3999 conversions`)
})
})
紅燈 🔴:執行測試發現效能需要優化。
當我們發現同樣的數字會被重複轉換時,何不把結果記下來?這就是快取的概念 - 用空間換時間。
建立快取版本測試:
// 建立 tests/day16/cache-optimization.test.ts
import { describe, test, expect } from 'vitest'
import { toRomanCached, fromRomanCached, clearCache } from '../../src/roman/converter'
describe('Cached Roman Numeral Converter', () => {
test('cacheRepeatedCalls', () => {
clearCache()
// 第一次呼叫
const result1 = toRomanCached(1994)
// 第二次呼叫相同數字應該更快
const start = performance.now()
const result2 = toRomanCached(1994)
const duration = performance.now() - start
expect(result1).toBe(result2)
expect(result1).toBe('MCMXCIV')
expect(duration).toBeLessThan(1) // 快取應該非常快
})
})
紅燈 🔴:快取版本的函數不存在。
實作快取版本:
// 更新 src/roman/converter.ts
// 快取物件
const toRomanCache = new Map<number, string>()
export function toRomanCached(num: number): string {
if (toRomanCache.has(num)) {
return toRomanCache.get(num)!
}
const result = toRoman(num)
toRomanCache.set(num, result)
return result
}
export function clearCache(): void {
toRomanCache.clear()
}
綠燈 🟢:快取測試通過!
快取命中時,查詢時間從毫秒級降到微秒級。
除了快取,我們還能從演算法本身下手。原本的實作使用了多個條件判斷,而查表法可以簡化這個過程。
建立查表法測試:
// 建立 tests/day16/lookup-optimization.test.ts
import { describe, test, expect } from 'vitest'
import { toRomanOptimized } from '../../src/roman/converter'
describe('Lookup Table Optimization', () => {
test('optimizedConvertsCorrectly', () => {
expect(toRomanOptimized(1)).toBe('I')
expect(toRomanOptimized(1994)).toBe('MCMXCIV')
expect(toRomanOptimized(3999)).toBe('MMMCMXCIX')
})
test('optimizedHandlesEdgeCases', () => {
expect(() => toRomanOptimized(0)).toThrow()
expect(() => toRomanOptimized(4000)).toThrow()
})
})
紅燈 🔴:優化版本尚未實作。
實作優化版本:
// 更新 src/roman/converter.ts
const ROMAN_LOOKUP: Array<[number, string]> = [
[1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'],
[100, 'C'], [90, 'XC'], [50, 'L'], [40, 'XL'],
[10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I']
]
export function toRomanOptimized(num: number): string {
if (num <= 0 || num > 3999) {
throw new Error('Number must be between 1 and 3999')
}
let result = ''
let remaining = num
for (const [value, symbol] of ROMAN_LOOKUP) {
const count = Math.floor(remaining / value)
if (count > 0) {
result += symbol.repeat(count)
remaining -= value * count
}
}
return result
}
綠燈 🟢:優化測試通過!
查表法讓程式碼更簡潔,執行也更快。
建立效能比較測試:
// 更新 tests/day16/performance.test.ts
test('bulkConversionComparison', () => {
const testData = Array.from({ length: 500 }, (_, i) => i + 1)
const start1 = performance.now()
testData.forEach(num => toRoman(num))
const duration1 = performance.now() - start1
const start2 = performance.now()
testData.forEach(num => toRomanOptimized(num))
const duration2 = performance.now() - start2
console.log(`Original: ${duration1.toFixed(2)}ms`)
console.log(`Optimized: ${duration2.toFixed(2)}ms`)
expect(duration2).toBeLessThan(duration1)
})
確保優化不破壞功能:
// 建立 tests/day16/regression.test.ts
test('allImplementationsIdentical', () => {
const testCases = [1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000, 1994, 3999]
for (const num of testCases) {
const original = toRoman(num)
const optimized = toRomanOptimized(num)
const cached = toRomanCached(num)
expect(optimized).toBe(original)
expect(cached).toBe(original)
}
})
// 完整實作 src/roman/converter.ts
const toRomanCache = new Map<number, string>()
export function toRomanCached(num: number): string {
if (toRomanCache.has(num)) {
return toRomanCache.get(num)!
}
const result = toRomanOptimized(num)
toRomanCache.set(num, result)
return result
}
export function clearCache(): void {
toRomanCache.clear()
}
執行所有效能測試:
# 執行所有效能測試
npm test tests/day16/
# 執行特定的效能測試
npm test tests/day16/performance.test.ts
今天我們使用 TDD 完成了羅馬數字轉換器的效能優化:
試著實作 LRU(Least Recently Used)快取,限制快取大小到 100 個項目。當快取滿時,刪除最少使用的項目。
// 檢查快取是否正確初始化
console.log('Cache size:', toRomanCache.size)
// 定期清理快取
if (toRomanCache.size > 10000) {
toRomanCache.clear()
}
今天我們學會了:
明天我們將進行羅馬數字轉換器的最終整理與回顧,學習如何重構和改進程式碼架構,為這個階段做完美收尾 🎯。