# Discovery 重要决策记录

**版本：** 3.0  
**最后更新**：2026-01-20

---

本文档记录 Discovery 模块的关键设计决策，包括问题背景、可选方案、选择理由和影响。

---

## 决策 1：将自动发现抽象为独立 infra 模块

**日期**：2025-xx-xx  
**状态**：已实施

### 痛点

- Provider、Handler、Schema、Strategy Worker 等地方都各自实现了一套扫描逻辑
- 这些逻辑高度相似：遍历包 → import 模块 → 找子类 / 常量 → 过滤
- 重复代码多、风格不统一，后续修改成本高

### 可选方案

- **方案 A：保持各自实现，不抽象**  
  - 优点：短期无改动
  - 缺点：重复代码多，bug 修复和行为调整需要多处修改
- **方案 B：将共性抽象到各业务模块内部**  
  - 优点：某个模块内部能统一
  - 缺点：不同模块之间依然重复，各自封装一套
- **方案 C：抽象为独立的 infra 模块（Discovery）** ✅  
  - 优点：一次实现，多处复用；集中优化和维护
  - 缺点：需要设计通用接口和配置模型

### 选择理由

1. **职责定位清晰**：自动发现属于基础设施能力，放在 `core/infra/discovery` 更合理
2. **减少重复**：统一实现 Class / Module 发现逻辑
3. **便于演进**：缓存、日志、错误处理等可以集中优化

### 影响

- 新增 `core/infra/discovery` 模块以及对应测试
- 后续 Provider / DataSource / Strategy 等模块逐步迁移到使用 Discovery

---

## 决策 2：区分 ClassDiscovery 和 ModuleDiscovery

**日期**：2025-xx-xx  
**状态**：已实施

### 痛点

- 类发现和模块对象发现虽然都叫「发现」，但细节差异较大：
  - 类发现需要处理继承关系、类过滤、键提取
  - 模块对象发现通常只需要按名称取出某个常量（如 `SCHEMA`）
- 如果强行统一成一个超大「发现器」，API 会变得复杂且难以理解

### 可选方案

- **方案 A：一个通用 Discoverer，内部用很多分支**  
  - 优点：理论上只暴露一个入口
  - 缺点：接口复杂、类型不清晰、文档难写
- **方案 B：拆成 ClassDiscovery 和 ModuleDiscovery 两个工具** ✅  
  - 优点：职责简单、语义清晰；使用者一眼能知道自己该用哪个

### 选择理由

1. **职责单一**：ClassDiscovery 专注于类；ModuleDiscovery 专注于模块对象
2. **接口友好**：每个类的 API 都可以围绕一个清晰的主场景设计

### 影响

- `class_discovery.py` 和 `module_discovery.py` 各自维护一套清晰 API
- 文档可以分别针对「发现子类」与「发现 SCHEMA/CONFIG」给出示例

---

## 决策 3：引入 DiscoveryConfig 作为规则描述

**日期**：2025-xx-xx  
**状态**：已实施

### 痛点

- 如果把所有规则（基类、模块命名模式、过滤条件、键提取等）都写在函数参数里，会导致：
  - 函数签名变得很长
  - 同一套规则无法复用
  - 测试 / 组合不同规则很不方便

### 可选方案

- **方案 A：全部用函数参数传入**
  - 优点：初期实现简单
  - 缺点：难以复用、难以测试
- **方案 B：定义一个配置类 DiscoveryConfig** ✅
  - 优点：规则集中、可复用、便于测试和序列化

### 选择理由

1. **配置与逻辑分离**：DiscoveryConfig 只描述「发现什么、怎么发现」
2. **可复用**：同一套规则可以应用在不同路径或环境
3. **测试友好**：可以针对不同配置构造单元测试用例

### 影响

- ClassDiscovery 接受一个 DiscoveryConfig 实例作为核心依赖
- 可以在不同业务模块中共用某些 DiscoveryConfig 模板

---

## 决策 4：默认启用缓存，但支持清理

**日期**：2025-xx-xx  
**状态**：已实施

### 痛点

- 自动发现通常发生在启动阶段或初始化阶段：
  - 扫描包结构 + import 子模块成本不低
  - 在同一次进程生命周期内多次重复扫描没有意义

### 可选方案

- **方案 A：每次调用都重新扫描**
  - 优点：实现简单
  - 缺点：性能浪费，日志噪声大
- **方案 B：默认缓存结果，提供清理接口** ✅
  - 优点：性能好，行为可控

### 选择理由

1. **实践场景简单**：大部分情况下发现逻辑只需要执行一次
2. **兼容性**：提供 `clear_cache` 接口，在需要重新加载时显式调用

### 影响

- ClassDiscovery 默认启用缓存，避免重复导入模块
- 提供 `clear_cache(base_module_path=None)` 用于测试或特殊场景

---

## 决策 5：错误容忍（fail-soft）而不是 fail-fast

**日期**：2025-xx-xx  
**状态**：已实施

### 痛点

- 在开发 / 用户空间中，某些模块可能暂时写得不完整或有语法错误
- 如果发现过程因单个模块错误而中断，会显著降低容错性和可用性

### 可选方案

- **方案 A：fail-fast，只要有一个模块导入失败就抛异常**
  - 优点：问题显性化
  - 缺点：对用户不友好，尤其是可选扩展场景
- **方案 B：fail-soft，记录 warning 并跳过有问题的模块** ✅
  - 优点：主流程不被个别扩展拖垮

### 选择理由

1. **扩展友好**：Discovery 主要用于加载用户或扩展模块，应尽量容忍部分失败
2. **调试友好**：通过日志可以发现问题，同时不影响其他模块被正常发现

### 影响

- 对于导入失败、类不符合条件等情况，Discovery 模块只记录 warning
- 由调用方决定在「结果为空」的情况下如何处理（如提供默认实现）

---

## 决策 6：约定优于配置的模块命名模式

**日期**：2025-xx-xx  
**状态**：已实施

### 痛点

- 如果每次都要求调用方显式列出所有模块路径，会让配置极其冗长
- 实际上，大部分扩展点都遵循类似的目录结构和命名约定

### 可选方案

- **方案 A：完全显式配置所有模块路径**
  - 优点：最灵活
  - 缺点：配置负担大，易出错
- **方案 B：采用约定的模块命名模式，必要时再覆盖** ✅
  - 优点：大多数场景可以不开配置直接工作

### 选择理由

1. **减少配置量**：通过 `{base_module}.{name}.provider` 等约定模式，大部分场景只需给出 base_module_path
2. **统一目录结构**：鼓励 userspace 遵循一致的目录和命名风格

### 影响

- DiscoveryConfig 中支持 `module_name_pattern` / `module_pattern`
- DataSource / Strategy / Adapter 等模块在设计目录结构时遵循这些约定

---

## 相关文档

- **[overview.md](./overview.md)**：Discovery 模块概览与示例
- **[architecture.md](./architecture.md)**：Discovery 架构与组件说明
- **`core/infra/discovery/DESIGN.md`**：原始设计记录

---

**文档结束**

