1
0
Files
PPTX/openspec/changes/archive/2026-03-02-refactor-yaml2pptx-modular/design.md
lanyuanxiaoyao ed940f0690 refactor: modularize yaml2pptx into layered architecture
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)
2026-03-02 16:43:45 +08:00

253 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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 中与用户充分讨论并确认。