胡萝卜烤地瓜
胡萝卜烤地瓜
发布于 2026-07-28 / 0 阅读
0
0

NO.AB1 耦合篇:两层解耦与契约对话

⚠️ 法律与用途声明:本文讨论平台与模板仓之间的解耦设计与契约接口,基于开源工具(Conan 2 / GitHub Actions / workflow_call),属于经验分享。所有企业名、项目名、内部脚本与路径命名均已脱敏。读者须遵守所在地区法律法规。


一、平台搭完了,模板仓也设计好了,然后呢

平台系列 A(A0-A14)讲完了"地基"怎么搭:从 runner 调度到 Docker 三层构建、从 Conan Center 到 Token Broker、从制品归档到上板验证、最后到模板化推广。

接下来要讲的模板仓系列 B(B0 开始),是"建筑"——每个算法仓库的 recipe、metadata、文档、测试、benchmark 怎么自带、怎么继承。

但在这两个系列之间,有个架构师必须想清楚的问题:平台和模板仓之间,到底什么关系?它们该怎么连接,又不该连接什么?

这篇就是回答这个问题的。它是 A 和 B 的桥——NO.AB1。

二、紧耦合的乱

先说反面教材。如果平台和模板仓不解耦,会怎样?

想象一下这些场景:

  • 模板仓的 CI workflow 里直接引用了平台的某个内部脚本路径(比如 /opt/<platform>/scripts/run_build.sh)。平台一改脚本路径,所有从模板创建的仓库 CI 全挂。
  • 平台的构建矩阵展开器依赖模板仓里某个具体包名。换一个算法仓库,展开器就找不到包,崩了。
  • 模板仓的开发者为了"让 CI 跑通",去读了平台的内部脚本源码,照着脚本的行为在自己的 recipe 里做了适配性 hack。平台一升级,这个 hack 变成了坑。

这些场景的共同特征是:两边的内部实现互相穿透。平台知道模板仓的业务细节,模板仓知道平台的脚本路径。结果是——任何一边改东西,另一边都可能崩。改一个脚本要回归测试所有仓库,改一个包名要动平台代码。这不是解耦,这是互为人质

三、解耦不是零耦合,而是靠契约的合理耦合

"解耦"这个词容易引起误解——以为解耦就是两边完全不相关、各管各的。但平台和模板仓天然是有关系的:模板仓需要平台来构建、测试、发布;平台需要模板仓来提供构建需求。完全断开,两边都没法独立工作

所以真正的目标不是零耦合,而是"合理耦合"——把两边的依赖关系收敛到少数几个明确的、稳定的接口上。这些接口就是"契约"。契约之外的任何东西,两边都互不感知。

用一句话说:解耦不是不耦合,是把耦合点收敛到契约上,让契约以外的东西可以各自变化

四、两个核心契约

在整套体系里,平台和模板仓之间只有两个核心契约文件:

契约一:build-matrix.yaml(模板仓 → 平台)

模板仓用这个文件声明"我要在哪些目标上构建、用什么工具链、要不要上板测"。平台读到它,展开成 CI 矩阵。

# 这份文件是模板仓向平台下的"需求订单"
package_ref: <包名>/<版本>
defaults:
  toolchain_version: <版本>
targets:
  - id: linux-arm
    build_kind: linux
    benchmark:
      enabled: true

关键约束:这份文件只描述意图,不含任何平台内部路径或脚本名。一个算法工程师能读懂它、能改它,不需要知道平台用 Docker 还是 Podman、用 hardlink 还是 copy。

契约二:metadata.json(模板仓 → 平台 + 自身构建系统)

模板仓用这个文件声明"我叫什么、版本多少、依赖什么、C++ 标准、测试开关"。Conan recipe 从它继承所有属性,平台 CI 从它读包元数据。

{
  "name": "<包名>",
  "version": "<版本>",
  "dependencies": { "cpp": { "<库>": ["<目标>"] } },
  "build_cppstd": "17",
  "trigger_tests": false
}

关键约束:这份文件是单一数据源。Conan recipe 不自己硬编码 name/version,而是从 metadata 继承;CI 不自己解析包名,而是从 metadata 读。一处声明,处处生效。

这两个契约的共同特征是:它们是声明式的、结构化的、不含平台实现细节的。模板仓只管声明意图,平台只管读声明然后执行。两边通过这两个文件对话,对话之外互不干涉。

五、单向依赖:模板仓指向平台,平台不回指

契约定了之后,还有一个方向性的约束:依赖是单向的

# 模板仓单向依赖平台(伪代码)
template_repo = {
    "knows": ["build-matrix.yaml → platform", "metadata.json → platform + conan"],
    "does_not_know": ["platform 内部脚本路径", "platform Docker 配置", "platform 缓存策略"],
}

platform = {
    "knows": ["读 build-matrix.yaml + metadata.json → 执行构建"],
    "does_not_know": ["模板仓的具体算法代码", "模板仓的业务依赖选择", "模板仓的 benchmark 用例"],
}

模板仓知道平台(通过契约引用平台的 CI workflow、Conan remote),但平台不知道模板仓(平台只读契约文件,不关心是哪个仓库、什么算法)。

这个方向性很重要。它意味着:

  • 加一个新模板仓不需要改平台。新仓库只要写好 build-matrix 和 metadata,平台的 CI 自动消费它们。
  • 平台升级不需要通知模板仓(只要契约不变)。平台换 Docker 策略、改缓存算法,模板仓无感。

如果依赖是双向的(平台也知道模板仓),加新仓库就得改平台代码,平台升级就得回归所有仓库——又回到互为人质的状态。

六、契约稳定,则各自狂奔

这套设计最终的效果是:只要契约不变,平台和模板仓可以各自独立演进

  • 平台把 Docker 从三层模型升级到 overlayfs(A3 的演进),模板仓不知道也不需要知道。
  • 平台把缓存治理从手动改成自动 systemd timer(A3),模板仓的 CI 行为不变。
  • 模板仓把 C++ 标准从 17 升到 23(B7),平台不关心,只要 metadata.json 里 build_cppstd 字段值变了,CI 自动用新标准构建。
  • 模板仓加了一个新目标(比如 RISC-V),平台不关心,只要 build-matrix.yaml 里多了一个 target 条目,展开器自动处理。

两边各自狂奔的前提,是契约稳定。所以契约文件的设计要特别慎重——字段不能频繁变、格式要前向兼容、变更要走版本管理。契约一旦不稳,两边就会被绑在一起,回到紧耦合。

七、架构师视角的几条判断

这篇是架构师视角的核心篇。把它收束成几条判断:

解耦不是零耦合,是把耦合点收敛到契约。两边完全断开没法工作,乱缠一起也没法维护。目标是只通过少数几个稳定的声明式文件对话。

契约要声明式、结构化、不含实现细节。build-matrix.yaml 和 metadata.json 只描述要什么,不描述怎么做。这是契约能稳定的前提。

依赖单向:模板仓指向平台。平台不回指模板仓。加新仓库不改平台,升级平台不动仓库。

契约稳定是各自演进的前提。字段不频繁变、变更走版本管理、前向兼容。契约一不稳,解耦就是空话。

两个契约文件是整套体系的窄接口。整个平台和所有业务仓库之间,只通过这两个文件对话。接口越窄,耦合越可控。这是高内聚低耦合在体系层面的终极落地。

八、结语:桥搭好了,该过桥了

这篇把平台(A 系列)和模板仓(B 系列)之间的桥搭好了——两个契约文件、单向依赖、契约稳定则各自演进。

接下来就该过桥了。B 系列从 NO.B0 开始,我们站在模板仓的视角,看一个算法仓库长什么样:metadata 怎么驱动 Conan recipe、文档怎么自动化、测试怎么自带、benchmark 怎么开箱就用。

那是一个全新的视角——不是地基怎么搭,而是建筑怎么设计。


参考链接:

[1] GitHub Actions:可复用 workflow(workflow_call,契约实现基础) https://docs.github.com/en/actions/how-tos/sharing-workflows-reusing-workflows

[2] Conan 2:recipe 与 metadata 驱动 https://docs.conan.io/2/reference/conanfile/attributes.html

[3] YAML 声明式配置格式 https://yaml.org/

[4] 高内聚低耦合原则 https://en.wikipedia.org/wiki/Coupling_(computer_programming)

[5] 接口隔离原则(契约窄接口的架构理论基础) https://en.wikipedia.org/wiki/Interface_segregation_principle

[6] 单向依赖与 DAG 架构 https://martinfowler.com/articles/injection.html


评论