这篇文章不是一份“正式的技术方案文档”,而是站在工具作者的视角,记录我在给 stock-sdk 做一次比较大的架构升级时,真实的思考过程、取舍以及落地方案

如果你也维护过一个“越写越大的 SDK / 工具库”,大概率会在下面的某些场景里看到自己的影子。


背景:当一个 SDK 开始“失控”

在很长一段时间里,stock-sdk 的代码结构其实非常简单:

  • 一个 sdk.ts
  • 一些工具函数
  • 一份类型定义

这样写在早期非常爽:

  • 新功能直接往里加
  • 一个文件就能看到所有逻辑
  • 调试也很直观

但问题在于,它会一直长

sdk.ts 的体量来到一千多行之后,一些信号开始变得明显:

  • 找一个方法要不停地滚动
  • 改一个功能,总担心影响到旁边完全不相关的逻辑
  • 测试文件也跟着一起膨胀
  • 心理上开始抗拒“再往这里加东西”

这不是某一行代码的问题,而是结构已经不再适合继续演进


我给这次重构定下的几个底线

在真正动手之前,我先给自己划了几条“红线”,否则很容易越改越乱:

  1. 对外 API 必须完全兼容(这是 SDK,不是业务项目)
  2. 可以慢慢迁,但不能一次性推翻重来
  3. 结构要能撑得住未来继续加功能
  4. 不要为了“优雅”而牺牲可读性

这几条基本决定了后面的所有设计选择。


一个核心判断:问题不在逻辑,而在“职责混在一起”

sdk.ts 拆开看,会发现里面其实混着几类完全不同的事情:

  • HTTP 请求和超时控制
  • GBK 编码解码、响应解析
  • 腾讯 / 东财等不同数据源的细节
  • 行情、K 线、分时、资金流向
  • 技术指标计算

这些代码本身并不复杂,复杂的是:

它们全都挤在同一个文件里。

所以这次升级的核心目标也很明确:

把“不同职责的代码”拆到不同层级里,而不是单纯拆文件。


新架构的整体思路(先说人话版)

我最终采用的是一个非常传统、但足够稳的分层思路:

  • Core 层:和“股票”无关的基础设施
  • Provider 层:对接不同数据源的适配器
  • Indicators 层:纯计算逻辑
  • SDK 层:一个薄薄的门面
  • Types 层:纯类型声明

换句话说:

SDK 只负责“我能做什么”,
Provider 负责“数据从哪来”,
Core 负责“怎么请求、怎么解析”。


为什么我要引入 Provider 层

这是这次重构里最关键的一步

以前的代码里,腾讯、东方财富的数据处理逻辑是交织在一起的:

  • 一个方法里判断市场
  • 根据条件拼不同 URL
  • 再写一堆 if/else 解析返回值

短期看没问题,长期看非常痛苦。

于是我干脆换了一个思路:

每一个数据源,都只干一件事:把“它自己的接口”适配成 SDK 需要的结构。

比如:

  • providers/tencent/quote.ts 只负责腾讯的行情
  • providers/eastmoney/aShareKline.ts 只负责东财的 A 股 K 线

它们的共同点只有一个:

1
(client: RequestClient, params...) => Promise<T>

这样一来:

  • 新增数据源 = 新增一个目录
  • 不需要去碰原有实现
  • 测试也可以直接按数据源拆开

Core 层:把“工程脏活”集中处理

Core 层只做一件事:

处理那些“业务不关心,但每个功能都会用到”的细节。

比如:

  • 请求超时
  • abort 控制
  • GBK 解码
  • 响应解析
  • 通用工具函数

这样做有两个非常直接的好处:

  1. Provider 的代码会变得非常“干净”
  2. SDK 层几乎不再关心底层实现细节

RequestClient 基本成了整个 SDK 的“地基”。


SDK 层:刻意做“薄”

重构后的 sdk.ts,我给自己定了一个硬指标:

不超过 200 行

它只做三件事:

  • 创建并持有 RequestClient
  • 组合 Provider 方法
  • 做极少量的参数整理

所有真正的逻辑,都被推到了更合适的地方。

结果也很直观:

  • 原来 1000+ 行的文件
  • 现在稳定在 150 行左右

阅读体验和心理负担完全不是一个量级。


分层架构图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
┌─────────────────────────────────────────────────────────────────────────┐
│ Application Layer │
│ (用户代码 / Playground / Demo) │
└─────────────────────────────────────┬───────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────┐
│ SDK Facade │
│ StockSDK (src/sdk.ts) │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ A股行情方法 │ │ K线数据方法 │ │ 技术指标方法 │ │ 扩展方法 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────┬───────────────────────────────────┘

┌─────────────────────────────┼─────────────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ Providers Layer │ │ Indicators Layer │ │ Core Layer │
│ (数据源适配器) │ │ (指标计算) │ │ (基础设施) │
├───────────────────┤ ├───────────────────┤ ├───────────────────┤
│ ┌───────────────┐ │ │ ┌───────────────┐ │ │ ┌───────────────┐ │
│ │ tencent/ │ │ │ │ ma.ts │ │ │ │ request.ts │ │
│ │ - quote.ts │ │ │ ├───────────────┤ │ │ ├───────────────┤ │
│ │ - flow.ts │ │ │ │ macd.ts │ │ │ │ parser.ts │ │
│ │ - timeline │ │ │ ├───────────────┤ │ │ ├───────────────┤ │
│ └───────────────┘ │ │ │ boll.ts │ │ │ │ utils.ts │ │
│ ┌───────────────┐ │ │ ├───────────────┤ │ │ ├───────────────┤ │
│ │ eastmoney/ │ │ │ │ kdj.ts │ │ │ │ constants.ts │ │
│ │ - aShareKline │ │ │ ├───────────────┤ │ │ └───────────────┘ │
│ │ - hkKline.ts │ │ │ │ ... │ │ │ │
│ │ - usKline.ts │ │ │ └───────────────┘ │ │ │
│ └───────────────┘ │ │ │ │ │
└───────────────────┘ └───────────────────┘ └───────────────────┘
│ │ │
└─────────────────────────────┼─────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────┐
│ Types Layer │
│ (类型定义,纯声明) │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ common.ts │ │ quote.ts │ │ kline.ts │ │ indicator.ts│ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
模式 应用场景 说明
门面模式 (Facade) StockSDK 统一对外接口,隐藏内部复杂性
适配器模式 (Adapter) providers/ 不同数据源适配为统一接口
策略模式 (Strategy) 指标计算、市场识别 可替换的算法实现
工厂模式 (Factory) 请求客户端创建 统一创建请求实例
依赖注入 (DI) Provider 函数 注入 RequestClient

渐进式迁移,而不是“一把梭”

这次升级我刻意没有一次性重写,而是拆成了几个阶段:

  1. 先抽 Core(风险最低)
  2. 再拆 Types
  3. 然后一个一个迁 Provider
  4. 最后才动测试结构

每一步都有一个明确标准:

  • 测试必须全绿
  • 对外 API 不变

这样哪怕中途停下来,项目也是健康的。


一个数字对比,最能说明问题

项目 重构前 重构后
sdk.ts 行数 1198 ~150
最大单文件 1198 <300
模块数量 15+
新增数据源成本

总代码量几乎没变,但可维护性完全不是一个级别


写在最后

这次架构升级并没有引入什么“很新”的东西:

  • 没有复杂框架
  • 没有花哨模式
  • 大多数设计都很传统

但它解决了一个非常现实的问题:

这个 SDK 还能不能被我和别人继续放心地往下写。

如果你也在维护一个“慢慢变大的工具项目”,我的经验只有一句话:

当你开始抗拒改代码的时候,问题往往已经不是业务复杂,而是结构该升级了。

希望这次 stock-sdk 的重构思路,能对你有所参考。

Happy Coding!