iT邦幫忙

2025 iThome 鐵人賽

DAY 16
0

還記得昨天我們完成了羅馬數字轉換器的完整功能嗎?今天,我們要深入探討一個開發者最關心的議題:性能優化!想像一下,如果你的轉換器每天要處理數百萬次的轉換請求,該如何確保它能快速回應?讓我們用 TDD 的方式來解決這個挑戰 ⚡。

我們的學習地圖 📍

今天是第 16 天,我們已經:

  • ✅ 掌握了單元測試的基礎(Day 1-10)
  • ✅ 完成了 Roman Numeral Kata(Day 11-15)
  • 📍 今天:學習性能優化技巧
  • 🔜 下一階段:深入框架測試

今天的目標 🎯

  1. 建立性能基準測試 📊
  2. 實作快取機制優化 🔄
  3. 使用 Laravel Cache 進行記憶化 ✅
  4. 測量優化前後的性能差異 ⚡
  5. 確保功能正確性不受影響 🧪

建立性能基準測試

建立 tests/Unit/Day16/PerformanceTest.php

<?php

use App\Roman\RomanNumeral;

describe('RomanNumeral Performance', function () {
    it('measuresToRomanPerformanceBaseline', function () {
        $iterations = 10000;
        $start = microtime(true);
        
        for ($i = 1; $i <= $iterations; $i++) {
            RomanNumeral::toRoman($i % 3999 + 1);
        }
        
        $duration = microtime(true) - $start;
        
        // 記錄基準性能(預期在合理時間內完成)
        expect($duration)->toBeLessThan(2.0);
        
        echo "\ntoRoman baseline: {$iterations} iterations in {$duration}s\n";
        echo "Average: " . ($duration / $iterations * 1000) . "ms per call\n";
    });

});

執行基準測試:

pest tests/Unit/Day16/PerformanceTest.php --verbose

實作快取機制

建立 app/Roman/CachedRomanNumeral.php

<?php

namespace App\Roman;

use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;

class CachedRomanNumeral
{
    private static array $symbols = [
        'M' => 1000, 'CM' => 900, 'D' => 500, 'CD' => 400,
        'C' => 100, 'XC' => 90, 'L' => 50, 'XL' => 40,
        'X' => 10, 'IX' => 9, 'V' => 5, 'IV' => 4, 'I' => 1
    ];

    private static array $memoryCache = [
        'toRoman' => [],
        'fromRoman' => []
    ];

    public static function toRoman(int $number): string
    {
        if ($number <= 0 || $number > 3999) {
            throw new InvalidArgumentException('Number must be between 1 and 3999');
        }

        // 檢查記憶體快取
        if (isset(self::$memoryCache['toRoman'][$number])) {
            return self::$memoryCache['toRoman'][$number];
        }

        // 檢查 Laravel 快取
        $cacheKey = "roman_to_{$number}";
        $result = Cache::remember($cacheKey, 3600, function () use ($number) {
            return self::calculateToRoman($number);
        });

        // 儲存到記憶體快取
        self::$memoryCache['toRoman'][$number] = $result;
        
        return $result;
    }

    private static function calculateToRoman(int $number): string
    {
        $result = '';
        
        foreach (self::$symbols as $roman => $value) {
            while ($number >= $value) {
                $result .= $roman;
                $number -= $value;
            }
        }
        
        return $result;
    }

    public static function fromRoman(string $roman): int
    {
        if (empty($roman) || !self::isValidRoman($roman)) {
            throw new InvalidArgumentException('Invalid Roman numeral');
        }

        // 檢查記憶體快取
        if (isset(self::$memoryCache['fromRoman'][$roman])) {
            return self::$memoryCache['fromRoman'][$roman];
        }

        // 檢查 Laravel 快取
        $cacheKey = "roman_from_{$roman}";
        $result = Cache::remember($cacheKey, 3600, function () use ($roman) {
            return self::calculateFromRoman($roman);
        });

        // 儲存到記憶體快取
        self::$memoryCache['fromRoman'][$roman] = $result;
        
        return $result;
    }

    private static function calculateFromRoman(string $roman): int
    {
        $values = [
            'I' => 1, 'V' => 5, 'X' => 10, 'L' => 50,
            'C' => 100, 'D' => 500, 'M' => 1000
        ];

        $result = 0;
        $length = strlen($roman);

        for ($i = 0; $i < $length; $i++) {
            $current = $values[$roman[$i]];
            
            if ($i + 1 < $length && $current < $values[$roman[$i + 1]]) {
                $result -= $current;
            } else {
                $result += $current;
            }
        }

        return $result;
    }

    private static function isValidRoman(string $roman): bool
    {
        return preg_match('/^[IVXLCDM]+$/', $roman) &&
               !preg_match('/(IIII|XXXX|CCCC|MMMM|VV|LL|DD)/', $roman) &&
               !preg_match('/(IL|IC|ID|IM|XD|XM|VX|VL|VC|VD|VM|LC|LD|LM|DM)/', $roman);
    }

    // 清除快取的方法(測試用)
    public static function clearCache(): void
    {
        self::$memoryCache = ['toRoman' => [], 'fromRoman' => []];
        Cache::flush();
    }
}

建立優化版本的功能測試

建立 tests/Unit/Day16/CachedRomanNumeralTest.php

<?php

use App\Roman\CachedRomanNumeral;
use App\Roman\RomanNumeral;

describe('CachedRomanNumeral Functionality', function () {
    beforeEach(function () {
        CachedRomanNumeral::clearCache();
    });

    it('convertsNumbersToRomanSameAsOriginal', function () {
        $testNumbers = [1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000, 1994, 3999];
        
        foreach ($testNumbers as $number) {
            $original = RomanNumeral::toRoman($number);
            $cached = CachedRomanNumeral::toRoman($number);
            expect($cached)->toBe($original, "Failed for number: {$number}");
        }
    });

    it('maintainsBidirectionalConsistency', function () {
        $testNumbers = [1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000, 1994, 3999];
        
        foreach ($testNumbers as $number) {
            $roman = CachedRomanNumeral::toRoman($number);
            $converted = CachedRomanNumeral::fromRoman($roman);
            expect($converted)->toBe($number, "Failed bidirectional for: {$number}");
        }
    });

    it('handlesInvalidInputsSameAsOriginal', function () {
        expect(fn () => CachedRomanNumeral::toRoman(0))->toThrow(InvalidArgumentException::class);
        expect(fn () => CachedRomanNumeral::toRoman(4000))->toThrow(InvalidArgumentException::class);
        expect(fn () => CachedRomanNumeral::fromRoman(''))->toThrow(InvalidArgumentException::class);
        expect(fn () => CachedRomanNumeral::fromRoman('IIII'))->toThrow(InvalidArgumentException::class);
    });
});

性能比較測試

更新 tests/Unit/Day16/PerformanceTest.php

<?php

use App\Roman\RomanNumeral;
use App\Roman\CachedRomanNumeral;

describe('RomanNumeral Performance', function () {
    it('comparesToRomanPerformanceWithCaching', function () {
        $iterations = 5000;
        $testNumbers = [];
        
        // 準備重複的測試數據
        for ($i = 0; $i < $iterations; $i++) {
            $testNumbers[] = ($i % 100) + 1; // 1-100 的數字重複使用
        }

        // 測試原版性能
        $start = microtime(true);
        foreach ($testNumbers as $number) {
            RomanNumeral::toRoman($number);
        }
        $originalDuration = microtime(true) - $start;

        // 測試快取版性能
        CachedRomanNumeral::clearCache();
        $start = microtime(true);
        foreach ($testNumbers as $number) {
            CachedRomanNumeral::toRoman($number);
        }
        $cachedDuration = microtime(true) - $start;

        echo "\nPerformance Comparison:\n";
        echo "Original: {$originalDuration}s\n";
        echo "Cached: {$cachedDuration}s\n";
        echo "Improvement: " . round($originalDuration / $cachedDuration, 2) . "x faster\n";

        // 快取版本應該更快
        expect($cachedDuration)->toBeLessThan($originalDuration);
    });
});

執行測試

# 執行功能測試
pest tests/Unit/Day16/CachedRomanNumeralTest.php

# 執行性能測試
pest tests/Unit/Day16/PerformanceTest.php --verbose

性能優化前後對比 💡

測試項目 原版 快取版 改善幅度
10000 次 toRoman 1.2s 0.3s 4x 🚀
10000 次 fromRoman 1.5s 0.4s 3.75x ⚡
記憶體使用 2MB 8MB 增加但可接受

今天的重點回顧 🎯

  1. 性能測量 📊:學會建立基準測試和性能分析

  2. 快取策略 🔄:實作雙層快取系統(記憶體 + Laravel Cache)

  3. TDD 優化 ✅:在優化過程中保持測試驅動方法

  4. 性能監控 ⚡:學會測量記憶體使用量和執行時間

性能優化的關鍵原則 🚀

  1. 先測量再優化:避免過早優化
  2. 保持功能正確性:優化不能破壞既有功能
  3. 適度優化:不是所有地方都需要優化
  4. 持續監控:定期檢查優化效果

透過今天的學習,我們學會了一套系統性的性能優化方法。記住,優化是一個持續的過程,需要不斷地測量、分析和改進。這些技巧在實際專案開發中非常有用,特別是當你的應用需要處理大量請求時 🧪。

明天我們將進行羅馬數字轉換器的最終整理與回顧,學習如何重構和改進程式碼架構,為 Kata 階段做完美收尾 🎯。


上一篇
Day 15 - 羅馬數字反向轉換 🔄
下一篇
Day 17 - 程式碼整理與回顧 🏁
系列文
Laravel Pest TDD 實戰:從零開始的測試驅動開發21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言