iT邦幫忙

2025 iThome 鐵人賽

DAY 17
0

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

今天的目標 🎯

  1. 整理和重構完整的程式碼
  2. 建立清晰的模組結構
  3. 加入完整的文檔字串和類型提示
  4. 建立綜合測試套件
  5. 回顧 TDD 學習歷程
  6. ✅ 建立乾淨、可維護的最終版本

羅馬數字 Kata 回顧 📚

在過去 6 天的開發歷程中,我們從最簡單的測試 to_roman(1) → "I" 開始,逐步建立了完整的羅馬數字轉換器,包含基礎符號轉換、擴展範圍支援、雙向轉換功能,以及效能優化機制。

建立生產就緒的模組結構 🏗️

完整模組實作

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

# 建立 src/roman/converter.py
"""Roman Numeral Converter - Production-ready module"""

from typing import Dict, List, Tuple
from functools import lru_cache

# 基本符號對應表
ROMAN_MAPPINGS: List[Tuple[int, str]] = [
    (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')
]

ROMAN_VALUES: Dict[str, int] = {
    '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
}

VALID_ROMAN_CHARS = {'M', 'D', 'C', 'L', 'X', 'V', 'I'}


def to_roman(number: int) -> str:
    """將阿拉伯數字轉換為羅馬數字"""
    if not isinstance(number, int):
        raise TypeError(f"Input must be an integer, got {type(number).__name__}")
    
    if number <= 0 or number > 3999:
        raise ValueError(f"Number must be between 1 and 3999, got {number}")
    
    result = []
    remaining = number
    
    for value, symbol in ROMAN_MAPPINGS:
        count, remaining = divmod(remaining, value)
        if count > 0:
            result.append(symbol * count)
    
    return ''.join(result)


def from_roman(roman: str) -> int:
    """將羅馬數字轉換為阿拉伯數字"""
    if not isinstance(roman, str):
        raise TypeError(f"Input must be a string, got {type(roman).__name__}")
    
    if not roman.strip():
        raise ValueError("Roman numeral cannot be empty")
    
    normalized_roman = roman.strip().upper()
    
    for char in normalized_roman:
        if char not in VALID_ROMAN_CHARS:
            raise ValueError(f"Invalid Roman character: '{char}'")
    
    result = 0
    i = 0
    
    while i < len(normalized_roman):
        if i + 1 < len(normalized_roman):
            two_char = normalized_roman[i:i+2]
            if two_char in ROMAN_VALUES:
                result += ROMAN_VALUES[two_char]
                i += 2
                continue
        
        one_char = normalized_roman[i]
        if one_char in ROMAN_VALUES:
            result += ROMAN_VALUES[one_char]
            i += 1
        else:
            raise ValueError(f"Invalid Roman numeral sequence at position {i}")
    
    return result


def is_valid_roman(roman: str) -> bool:
    """驗證羅馬數字格式是否正確"""
    try:
        number = from_roman(roman)
        return to_roman(number) == roman.strip().upper()
    except (ValueError, TypeError):
        return False


@lru_cache(maxsize=4000)
def to_roman_cached(number: int) -> str:
    """快取版本的羅馬數字轉換器"""
    return to_roman(number)


@lru_cache(maxsize=4000)
def from_roman_cached(roman: str) -> int:
    """快取版本的羅馬數字解析器"""
    return from_roman(roman)

建立綜合測試套件 🧪

整合測試涵蓋所有核心功能:

# 建立 tests/day17/test_roman_converter.py
import pytest
from src.roman.converter import to_roman, from_roman, to_roman_cached

def test_complete_conversion_workflow():
    """測試完整的雙向轉換流程"""
    test_cases = [
        (1, 'I'), 
        (4, 'IV'), 
        (1994, 'MCMXCIV'), 
        (3999, 'MMMCMXCIX')
    ]
    
    for number, expected in test_cases:
        # 測試數字轉羅馬
        assert to_roman(number) == expected
        # 測試羅馬轉數字
        assert from_roman(expected) == number

def test_invalid_input_handling():
    """測試無效輸入的錯誤處理"""
    # 測試無效數字範圍
    with pytest.raises(ValueError):
        to_roman(0)
    
    # 測試空字串輸入
    with pytest.raises(ValueError):
        from_roman('')

def test_cached_performance():
    """測試快取版本的功能正確性"""
    # 確認快取版本結果與原版一致
    assert to_roman_cached(1994) == to_roman(1994)
    assert to_roman_cached(3999) == 'MMMCMXCIX'

TDD 學習回顧與反思 💡

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

TDD 循環的威力

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

漸進式開發的重要性

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

Python 特色應用

在 Python 實作中,我們特別運用了類型提示、文檔字串、functools.lru_cache 內建快取裝飾器,以及 pytest 簡潔的函數式測試風格。

完整實作總結 📋

核心功能清單

  • 阿拉伯數字轉羅馬數字 (to_roman)
  • 羅馬數字轉阿拉伯數字 (from_roman)
  • 羅馬數字格式驗證 (is_valid_roman)
  • 快取優化版本 (to_roman_cached, from_roman_cached)

品質特性

  • 類型提示支援: 完整的類型註解提升程式碼可讀性
  • 錯誤處理機制: 嚴謹的輸入驗證與錯誤訊息
  • 效能優化: LRU快取減少重複運算
  • 文檔字串: 清晰的函數說明文件

測試執行與驗證 ✅

# 執行所有Day 17測試
pytest tests/day17/ -v

# 產生測試覆蓋率報告
pytest tests/day17/ --cov=src.roman --cov-report=term

# 檢查程式碼品質
black src/roman/ && isort src/roman/ && mypy src/roman/

TDD 深度反思與學習心得 🤔

六天開發歷程回顧

Day 11-16的TDD實踐進程:

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

Python 特色的 TDD 實踐 🐍

Pythonic 的測試風格

def test_basic_roman_symbols():
    """測試基本羅馬數字符號"""
    assert to_roman(1) == "I"
    assert to_roman(5) == "V"
    assert to_roman(10) == "X"

@pytest.mark.parametrize("number,expected", [
    (1, "I"),
    (4, "IV"),
    (9, "IX"),
    (14, "XIV"),
    (44, "XLIV"),
    (90, "XC"),
    (400, "CD"),
    (900, "CM")
])
def test_parameterized_conversion(number, expected):
    """使用參數化測試驗證多個案例"""
    assert to_roman(number) == expected

測試策略演進 📈

透過六天的實作,我們的測試策略也在不斷進化:

測試組織方式的改進
## Day 11 - 簡單的單一測試
def test_converts_1_to_I():
    assert to_roman(1) == 'I'

## 組織化的測試套件
class TestRomanConverter:
    def test_basic_symbols(self):
        """測試基本符號轉換"""
        pass
    
    def test_subtraction_cases(self):
        """測試減法規則"""
        pass
    
    def test_error_handling(self):
        """測試錯誤處理"""
        pass
測試覆蓋率的提升
  • 從單一案例到全面覆蓋
  • 從正向測試到包含錯誤處理
  • 從單向轉換到雙向驗證

今天的核心收穫 🎁

通過 Roman Numeral Kata 的完整實作,我們深度體驗了 TDD 的精髓:

TDD 實踐技能

  • 熟練掌握紅-綠-重構循環
  • 學會寫出清晰的測試案例
  • 建立測試優先的開發習慣
  • 理解測試驅動的API設計

Python 程式設計技能

  • 模組化設計思維
  • 完整的錯誤處理策略
  • 類型提示系統運用
  • Pythonic 程式風格

Python 生態系統整合

  • pytest 測試框架熟練應用
  • 現代 Python 工程化實踐

軟體工程實踐

  • 漸進式開發方法
  • 測試覆蓋策略
  • 程式碼品質管理
  • 函數介面設計原則

旅程地圖回顧 🗺️

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

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

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

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

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

總結與鼓勵 💪

Roman Numeral Kata 的完成標誌著我們在 Python TDD 領域達成了重要里程碑!

已掌握的 Python TDD 核心技能

  • pytest 測試框架的熟練運用
  • TDD 紅-綠-重構循環的內化
  • Python 類型提示系統的實戰經驗
  • 函數式程式設計與 TDD 的結合
  • Python 內建工具的優化應用

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


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

尚未有邦友留言

立即登入留言