Refactor yaml2pptx.py from a 1,245-line monolithic script into a modular architecture with clear separation of concerns. The entry point is now 127 lines, with business logic distributed across focused modules. Architecture: - core/: Domain models (elements, template, presentation) - loaders/: YAML loading and validation - renderers/: PPTX and HTML rendering - preview/: Flask preview server - utils.py: Shared utilities Key improvements: - Element abstraction layer using dataclass with validation - Renderer logic built into generator classes - Single-direction dependencies (no circular imports) - Each module 150-300 lines for better readability - Backward compatible CLI interface Documentation: - README.md: User-facing usage guide - README_DEV.md: Developer documentation OpenSpec: - Archive refactor-yaml2pptx-modular change (63/70 tasks complete) - Sync 5 delta specs to main specs (2 new + 3 updated)
253 lines
8.8 KiB
Markdown
253 lines
8.8 KiB
Markdown
## Context
|
||
|
||
yaml2pptx.py 当前是一个 1,245 行的单文件脚本,包含了从 YAML 解析到 PPTX 生成的完整流程。虽然功能完整,但代码组织存在以下问题:
|
||
|
||
- **可读性差**:单文件过大,开发者和 LLM 工具难以快速理解代码结构
|
||
- **维护困难**:修改一个功能可能需要在文件中跳转多次,容易引入错误
|
||
- **扩展性受限**:添加新元素类型需要在多处添加代码(验证、PPTX 渲染、HTML 渲染)
|
||
- **测试不便**:无法对单个模块进行独立测试
|
||
|
||
当前约束:
|
||
- 必须保持 `uv run yaml2pptx.py` 的单入口使用方式
|
||
- 所有依赖通过 `/// script` 头部声明
|
||
- 不能污染主机环境的 Python 配置
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:**
|
||
- 将代码拆分为职责清晰的模块,每个文件控制在 150-300 行
|
||
- 引入元素抽象层,使添加新元素类型只需实现数据类和渲染方法
|
||
- 建立清晰的依赖关系,避免循环依赖
|
||
- 保持向后兼容,用户使用方式不变
|
||
- 为未来扩展(如添加 PDF 渲染器、Video 元素)奠定基础
|
||
|
||
**Non-Goals:**
|
||
- 不改变现有功能的行为(纯重构,不添加新功能)
|
||
- 不改变 YAML 文件格式或 CLI 参数
|
||
- 不迁移到标准 Python 包(保持单文件入口模式)
|
||
- 不添加单元测试(可以在后续单独添加)
|
||
|
||
## Decisions
|
||
|
||
### 1. 四层架构设计
|
||
|
||
**决策**:采用 core/loaders/renderers/preview 四层架构
|
||
|
||
```
|
||
html2pptx/
|
||
├── yaml2pptx.py # 入口层:CLI + main
|
||
├── core/ # 核心层:领域模型
|
||
│ ├── elements.py
|
||
│ ├── template.py
|
||
│ └── presentation.py
|
||
├── loaders/ # 加载层:数据输入
|
||
│ └── yaml_loader.py
|
||
├── renderers/ # 渲染层:数据输出
|
||
│ ├── pptx_renderer.py
|
||
│ └── html_renderer.py
|
||
├── preview/ # 预览层:可选功能
|
||
│ └── server.py
|
||
└── utils.py # 工具层:通用函数
|
||
```
|
||
|
||
**理由**:
|
||
- **职责分离**:每层有明确的职责,修改影响范围可控
|
||
- **依赖方向清晰**:入口 → 渲染 → 核心 ← 加载,避免循环依赖
|
||
- **易于扩展**:添加新渲染器(如 PDF)只需在 renderers/ 下新增文件
|
||
|
||
**替代方案**:
|
||
- 扁平结构(所有文件在同一目录):简单但缺乏层次,未来扩展会混乱
|
||
- 更细粒度的分层(如 domain/application/infrastructure):过度设计,不适合当前规模
|
||
|
||
### 2. 元素抽象层使用 dataclass
|
||
|
||
**决策**:使用 Python dataclass 定义元素数据类
|
||
|
||
```python
|
||
from dataclasses import dataclass, field
|
||
|
||
@dataclass
|
||
class TextElement:
|
||
type: str = 'text'
|
||
content: str = ''
|
||
box: list[float] = field(default_factory=lambda: [1, 1, 8, 1])
|
||
font: dict = field(default_factory=dict)
|
||
|
||
def __post_init__(self):
|
||
# 创建时验证
|
||
if len(self.box) != 4:
|
||
raise ValueError("box 必须包含 4 个数字")
|
||
```
|
||
|
||
**理由**:
|
||
- **简洁性**:dataclass 自动生成 `__init__`、`__repr__` 等方法,减少样板代码
|
||
- **类型提示**:支持类型注解,IDE 和 LLM 工具可以提供更好的补全
|
||
- **验证时机**:`__post_init__` 在对象创建时自动调用,尽早发现错误
|
||
- **可扩展性**:未来可以添加方法(如 `to_dict()`、`validate_deep()`)
|
||
|
||
**替代方案**:
|
||
- TypedDict:运行时是字典,缺少验证能力
|
||
- 普通类:需要手写 `__init__`,代码冗长
|
||
|
||
### 3. 渲染器内置在生成器中
|
||
|
||
**决策**:将渲染逻辑内置在 PptxGenerator 和 HtmlRenderer 类中
|
||
|
||
```python
|
||
class PptxGenerator:
|
||
def add_slide(self, slide_data, base_path=None):
|
||
# 渲染元素
|
||
for elem in slide_data['elements']:
|
||
self._render_element(slide, elem, base_path)
|
||
|
||
def _render_element(self, slide, elem, base_path):
|
||
if isinstance(elem, TextElement):
|
||
self._render_text(slide, elem)
|
||
elif isinstance(elem, ImageElement):
|
||
self._render_image(slide, elem, base_path)
|
||
# ...
|
||
```
|
||
|
||
**理由**:
|
||
- **封装性**:渲染逻辑与生成器紧密相关,内置更自然
|
||
- **简单性**:不需要额外的渲染器接口或依赖注入
|
||
- **性能**:避免额外的方法调用开销
|
||
|
||
**替代方案**:
|
||
- 渲染器作为参数传入:增加复杂度,当前不需要运行时切换渲染器
|
||
- 统一渲染接口:过度抽象,PPTX 和 HTML 渲染差异较大
|
||
|
||
### 4. 创建时验证
|
||
|
||
**决策**:在元素对象创建时进行验证(`__post_init__` 方法)
|
||
|
||
**理由**:
|
||
- **尽早失败**:在数据进入系统时就发现错误,而不是等到渲染时
|
||
- **清晰的错误位置**:验证失败时,堆栈指向元素创建处,易于定位
|
||
- **避免无效状态**:确保元素对象始终处于有效状态
|
||
|
||
**替代方案**:
|
||
- 加载时验证:验证逻辑分散在 yaml_loader 中,难以维护
|
||
- 渲染时验证:错误发现太晚,可能已经生成了部分幻灯片
|
||
|
||
### 5. 元素工厂函数
|
||
|
||
**决策**:提供 `create_element(elem_dict)` 工厂函数,从字典创建元素对象
|
||
|
||
```python
|
||
def create_element(elem_dict: dict):
|
||
elem_type = elem_dict.get('type')
|
||
if elem_type == 'text':
|
||
return TextElement(**elem_dict)
|
||
elif elem_type == 'image':
|
||
return ImageElement(**elem_dict)
|
||
# ...
|
||
```
|
||
|
||
**理由**:
|
||
- **统一入口**:所有元素创建都通过工厂函数,便于添加通用逻辑(如日志、缓存)
|
||
- **类型安全**:工厂函数可以进行类型检查,避免创建错误的元素类型
|
||
- **易于扩展**:添加新元素类型只需在工厂函数中添加一个分支
|
||
|
||
### 6. 依赖管理策略
|
||
|
||
**决策**:所有依赖保留在 yaml2pptx.py 的 `/// script` 头部,预览依赖不再可选
|
||
|
||
```python
|
||
# /// script
|
||
# requires-python = ">=3.8"
|
||
# dependencies = [
|
||
# "python-pptx",
|
||
# "pyyaml",
|
||
# "flask",
|
||
# "watchdog",
|
||
# ]
|
||
# ///
|
||
```
|
||
|
||
**理由**:
|
||
- **简化处理**:不需要条件导入或可选依赖检查
|
||
- **一致性**:所有用户都有相同的依赖环境
|
||
- **依赖轻量**:flask 和 watchdog 都是轻量级库,不会显著增加安装时间
|
||
|
||
## Risks / Trade-offs
|
||
|
||
### 风险 1:导入路径变化可能引入错误
|
||
|
||
**风险**:重构后内部导入路径会发生变化,可能遗漏某些导入或引入循环依赖
|
||
|
||
**缓解措施**:
|
||
- 按模块逐步迁移,每迁移一个模块就测试一次
|
||
- 使用 Python 的 `import` 语句检查(运行脚本时会立即发现导入错误)
|
||
- 保持清晰的依赖方向:入口 → 渲染 → 核心 ← 加载
|
||
|
||
### 风险 2:元素验证可能过于严格
|
||
|
||
**风险**:在 `__post_init__` 中进行验证可能拒绝某些边缘情况的有效输入
|
||
|
||
**缓解措施**:
|
||
- 只验证关键约束(如 box 必须有 4 个元素),不验证业务逻辑
|
||
- 提供清晰的错误消息,帮助用户快速定位问题
|
||
- 如果发现验证过严,可以在后续放宽
|
||
|
||
### 风险 3:文件数量增加可能影响开发体验
|
||
|
||
**权衡**:从 1 个文件变成 8+ 个文件,开发者需要在多个文件间跳转
|
||
|
||
**缓解措施**:
|
||
- 每个文件职责清晰,命名直观(如 `elements.py`、`pptx_renderer.py`)
|
||
- 文件大小适中(150-300 行),易于在单个屏幕内浏览
|
||
- 提供清晰的模块文档,说明每个文件的职责
|
||
|
||
### 风险 4:重构可能引入功能回归
|
||
|
||
**风险**:代码重组过程中可能无意中改变了某些行为
|
||
|
||
**缓解措施**:
|
||
- 保持函数逻辑不变,只改变组织方式
|
||
- 重构后运行完整的功能测试(手动测试所有 YAML 示例)
|
||
- 对比重构前后生成的 PPTX 文件,确保输出一致
|
||
|
||
## Migration Plan
|
||
|
||
### 阶段 1:基础设施(utils + loaders)
|
||
|
||
1. 创建目录结构
|
||
2. 提取 `utils.py`(日志函数、颜色转换)
|
||
3. 提取 `loaders/yaml_loader.py`(YAML 加载和验证)
|
||
4. 更新 yaml2pptx.py 的导入,测试 YAML 加载功能
|
||
|
||
### 阶段 2:核心抽象(core)
|
||
|
||
1. 实现 `core/elements.py`(元素数据类 + 工厂函数)
|
||
2. 提取 `core/template.py`(Template 类)
|
||
3. 提取 `core/presentation.py`(Presentation 类)
|
||
4. 测试模板渲染功能
|
||
|
||
### 阶段 3:渲染器(renderers)
|
||
|
||
1. 实现 `renderers/pptx_renderer.py`(PptxGenerator + PPTX 渲染)
|
||
2. 实现 `renderers/html_renderer.py`(HtmlRenderer)
|
||
3. 测试 PPTX 生成和 HTML 预览功能
|
||
|
||
### 阶段 4:预览功能(preview)
|
||
|
||
1. 提取 `preview/server.py`(Flask 服务器 + 文件监听)
|
||
2. 测试预览服务器功能
|
||
|
||
### 阶段 5:入口整合
|
||
|
||
1. 重构 yaml2pptx.py(只保留 CLI + main)
|
||
2. 运行完整的端到端测试
|
||
3. 对比重构前后的输出,确保一致性
|
||
|
||
### 回滚策略
|
||
|
||
- 保留原始 yaml2pptx.py 的备份(如 yaml2pptx.py.backup)
|
||
- 如果发现严重问题,可以快速回滚到原始版本
|
||
- 使用 git 分支进行重构,确保可以随时回退
|
||
|
||
## Open Questions
|
||
|
||
无待解决问题。设计方案已在 explore mode 中与用户充分讨论并确认。
|