iT邦幫忙

2025 iThome 鐵人賽

DAY 17
0

完成了六天的羅馬數字轉換器開發,今天我們要進行程式碼的最終整理與回顧。將散落在不同測試日期中的程式碼重構成一個乾淨、可維護、生產就緒的 Laravel 服務。

今天的目標 🎯

  1. 整理和重構完整的程式碼
  2. 建立清晰的服務架構
  3. 加入完整的 PHPDoc 文件
  4. 建立綜合測試套件
  5. 回顧 TDD 學習歷程
  6. ✅ 建立乾淨、可維護的最終版本

羅馬數字 Kata 回顧 📚

在過去 6 天的開發歷程中,我們從最簡單的測試 toRoman(1) → "I" 開始,逐步建立了完整的羅馬數字轉換器。

建立統一的服務架構 🏗️

完整服務實作

將六天開發的程式碼重構為一個統一、乾淨的服務:

建立 app/Services/RomanNumeralService.php

<?php

namespace App\Services;

use InvalidArgumentException;

/**
 * Roman Numeral Conversion Service
 * 
 * Provides bidirectional conversion between Arabic numbers (1-3999)
 * and Roman numerals with caching optimization.
 */
class RomanNumeralService
{
    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 $values = [
        'I' => 1, 'V' => 5, 'X' => 10, 'L' => 50,
        'C' => 100, 'D' => 500, 'M' => 1000
    ];

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

    public function toRoman(int $number): string
    {
        $this->validateArabicNumber($number);

        if (isset(self::$cache['toRoman'][$number])) {
            return self::$cache['toRoman'][$number];
        }

        $result = $this->convertToRoman($number);
        self::$cache['toRoman'][$number] = $result;
        
        return $result;
    }

    public function fromRoman(string $roman): int
    {
        $this->validateRomanNumeral($roman);

        if (isset(self::$cache['fromRoman'][$roman])) {
            return self::$cache['fromRoman'][$roman];
        }

        $result = $this->convertFromRoman($roman);
        self::$cache['fromRoman'][$roman] = $result;
        
        return $result;
    }

    private function validateArabicNumber(int $number): void
    {
        if ($number < 1 || $number > 3999) {
            throw new InvalidArgumentException("Number must be between 1 and 3999, got: {$number}");
        }
    }

    private function validateRomanNumeral(string $roman): void
    {
        if (empty($roman)) {
            throw new InvalidArgumentException('Roman numeral cannot be empty');
        }

        if (!$this->isValidRomanFormat($roman)) {
            throw new InvalidArgumentException("Invalid Roman numeral format: {$roman}");
        }
    }

    private function convertToRoman(int $number): string
    {
        $result = '';
        
        foreach (self::$symbols as $roman => $value) {
            $count = intval($number / $value);
            if ($count > 0) {
                $result .= str_repeat($roman, $count);
                $number %= $value;
            }
        }
        
        return $result;
    }

    private function convertFromRoman(string $roman): int
    {
        $result = 0;
        $length = strlen($roman);

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

        return $result;
    }

    private function isValidRomanFormat(string $roman): bool
    {
        return preg_match('/^[IVXLCDM]+$/', $roman) && 
               !preg_match('/(IIII|XXXX|CCCC|MMMM|VV|LL|DD)/', $roman);
    }

    public function clearCache(): void
    {
        self::$cache = ['toRoman' => [], 'fromRoman' => []];
    }

    public function getCacheStats(): array
    {
        return ['total_cached' => count(self::$cache['toRoman']) + count(self::$cache['fromRoman'])];
    }
}

整合測試套件 🧪

將六天來的測試經驗整合成一個完整的測試套件,驗證所有功能的正確性:

建立 tests/Unit/Day17/RomanNumeralServiceTest.php

<?php

use App\Services\RomanNumeralService;

describe('RomanNumeralService Integration', function () {
    beforeEach(function () {
        $this->service = new RomanNumeralService();
        $this->service->clearCache();
    });

    it('handles complete conversion workflow', function () {
        $testCases = [1 => 'I', 4 => 'IV', 1994 => 'MCMXCIV', 3999 => 'MMMCMXCIX'];

        foreach ($testCases as $number => $expected) {
            expect($this->service->toRoman($number))->toBe($expected);
            expect($this->service->fromRoman($expected))->toBe($number);
        }
    });

    it('validates input boundaries', function () {
        expect(fn () => $this->service->toRoman(0))
            ->toThrow(InvalidArgumentException::class);
        expect(fn () => $this->service->fromRoman(''))
            ->toThrow(InvalidArgumentException::class);
    });

    it('demonstrates caching effectiveness', function () {
        $result1 = $this->service->toRoman(1994);
        $stats = $this->service->getCacheStats();
        expect($stats['total_cached'])->toBe(1);
    });
});

執行最終測試與驗證 ✅

測試執行與品質檢查

執行完整測試

# 執行所有 Day 17 測試
pest tests/Unit/Day17/ --verbose

# 檢查測試覆蓋率
pest --coverage --coverage-html=coverage-report

# Laravel Pint 程式碼格式檢查
./vendor/bin/pint app/Services/RomanNumeralService.php

# PHPStan 靜態分析
./vendor/bin/phpstan analyse app/Services/RomanNumeralService.php

TDD 學習回顧與反思 💡

透過六天的 Roman Numeral Kata,我們累積了哪些寶貴經驗?

TDD 循環的威力

紅-綠-重構 循環讓我們始終保持程式碼有測試覆蓋,每次只專注解決一個問題,透過重構持續改善設計,建立可靠的安全網。

漸進式開發的重要性

toRoman(1) → "I" 開始,我們學會了先讓最簡單的測試通過,逐步增加複雜性,每次變更都有測試保護。

重構的信心

有了測試作為安全網,我們能夠放心改善程式碼結構,優化效能而不破壞功能,重新組織模組架構。

完整實作總結 📋

核心功能

  • 阿拉伯數字轉羅馬數字 (toRoman)
  • 羅馬數字轉阿拉伯數字 (fromRoman)
  • 輸入驗證與錯誤處理
  • 記憶體快取優化

品質特性

  • PHPDoc 文件支援、錯誤處理機制、高效能實作

Laravel 特色的 TDD 實踐 🚀

Pest 測試框架的獨特優勢

Pest 為 Laravel 生態系統帶來了現代化的測試體驗:

// 富有表達力的測試語法
describe('Roman Numeral Conversion', function () {
    it('converts numbers to roman numerals', function () {
        expect(toRoman(1994))->toBe('MCMXCIV');
    });
    
    it('maintains bidirectional consistency', function () {
        expect(fromRoman(toRoman(42)))->toBe(42);
    });
});

// 簡潔的資料驗證
expect(fn () => toRoman(0))->toThrow(InvalidArgumentException::class);

TDD 深度反思與學習心得 🤔

六天開發歷程回顧

Day 11-16的TDD實踐進程:

  • Day 11: 第一個測試 toRoman(1) → "I"
  • Day 12: 處理重複數字與循環結構
  • Day 13: 減法表示(IV, IX)與介面設計
  • Day 14: 完整範圍支援與錯誤處理
  • Day 15: fromRoman雙向轉換功能
  • Day 16: 效能優化與快取機制

今天的核心收穫 🎁

TDD 實踐技能

  • 熟練掌握紅-綠-重構循環的精髓
  • 學會測試驅動的函數設計思維
  • 建立測試優先的開發習慣
  • 理解測試驅動的API設計

Laravel 生態系統整合

  • Pest 測試框架的現代化語法
  • 服務容器與依賴注入模式
  • Facade 模式的優雅封裝

軟體工程最佳實踐

  • 程式碼組織與責任分離
  • 完整的錯誤處理策略
  • 效能優化與快取機制

旅程地圖回顧 🗺️

讓我們回顧這 17 天的學習之旅:

第一階段回顧(Day 1-10)

  • ✅ 環境設定與 Pest 框架初探
  • ✅ 斷言方法與測試結構
  • ✅ 紅綠重構循環實踐
  • ✅ 測試生命週期掌握
  • ✅ 參數化測試技巧
  • ✅ 測試替身運用
  • ✅ 例外處理測試
  • ✅ 測試覆蓋率分析
  • ✅ 重構技巧深化

第二階段完成(Day 11-17)

  • ✅ Kata 方法學導入
  • ✅ 基礎符號轉換
  • ✅ 百位數處理擴展
  • ✅ 完整範圍實作
  • ✅ 雙向轉換功能
  • ✅ 效能優化策略
  • ✅ 程式碼整理回顧

總結與鼓勵 💪

完成 Roman Numeral Kata 是一個重要的里程碑!我們不僅學會了 TDD 的核心技術,更建立了以測試驅動開發的思維模式。

每一個測試都是對品質的承諾,每一次重構都是對卓越的追求。讓我們帶著這些收穫,繼續前進!


上一篇
Day 16 - 效能優化 🚀
下一篇
Day 18 - HTTP 測試基礎 🌐
系列文
Laravel Pest TDD 實戰:從零開始的測試驅動開發21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言