完成了六天的羅馬數字轉換器開發,今天我們要進行程式碼的最終整理與回顧。將散落在不同測試日期中的程式碼重構成一個乾淨、可維護、生產就緒的 Laravel 服務。
在過去 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
透過六天的 Roman Numeral Kata,我們累積了哪些寶貴經驗?
紅-綠-重構 循環讓我們始終保持程式碼有測試覆蓋,每次只專注解決一個問題,透過重構持續改善設計,建立可靠的安全網。
從 toRoman(1) → "I"
開始,我們學會了先讓最簡單的測試通過,逐步增加複雜性,每次變更都有測試保護。
有了測試作為安全網,我們能夠放心改善程式碼結構,優化效能而不破壞功能,重新組織模組架構。
toRoman
)fromRoman
)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);
Day 11-16的TDD實踐進程:
toRoman(1) → "I"
fromRoman
雙向轉換功能讓我們回顧這 17 天的學習之旅:
完成 Roman Numeral Kata 是一個重要的里程碑!我們不僅學會了 TDD 的核心技術,更建立了以測試驅動開發的思維模式。
每一個測試都是對品質的承諾,每一次重構都是對卓越的追求。讓我們帶著這些收穫,繼續前進!