跳转至

19 · 可复用的设计经验

Quattro 不是每个项目都应复制的模板,但它处理的几个问题具有普遍性:配置所有权、跨权限生命周期、数百命令发现、热加载代码与滚动迁移。

经验一:先画状态所有权,再写安装脚本

最有价值的分层是:

shipped default ≠ user intent ≠ generated state ≠ transient coordination

若这四类对象都放在 ~/.config,更新器无法知道什么能覆盖,用户也不知道什么值得备份。Quattro 用 /usr/share、~/.config、~/.local/state 和 runtime dir 分离它们。

适用场景:CLI 工具链、IDE、桌面环境、开发环境管理器。

不要照抄的地方:路径本身不是目的;应遵循目标平台标准(XDG、Windows Known Folders、macOS Application Support)。

经验二:生命周期阶段按能力切,不按代码目录切

root system apply、owner finalization 和 graphical first-run 的区别是“当下拥有什么能力”。把所有动作塞进 installer,必然用 sleep、sudo -u 和脆弱环境猜测补洞。

可以用下面的矩阵设计阶段:

动作需要 最早可靠阶段
修改 /etc / 安装包 root system
知道真实 HOME/UID owner provisioning
user systemd / D-Bus user session
compositor / notification UI graphical first-run

经验三:用文件发现扩展,用元数据构建产品体验

CLI router 把可执行文件作为插件单元,文件名提供快路径,元数据提供 help/list/check。这比中央表少一处同步点,又避免每次调用扫描全库。

这种模式适合:

  • 子命令数量大;
  • 每条命令可独立执行;
  • 发现/帮助不应拖慢日常路径;
  • CI 可以 lint metadata。

如果只有十条强耦合命令,普通 subcommand framework 更简单,不必为扩展而扩展。

经验四:把 UI 热路径和系统探测解耦

菜单先显示 cached guards,再批量刷新;AppLibrary 常驻共享;panel 延迟加载。用户感知延迟不应等于所有系统检查总耗时。

通用公式是:

立即可用的旧快照 + 后台单批求真 + 明确最终更新

前提是执行动作不能相信缓存,仍需在命令边界重新校验。

经验五:扩展能力越强,授权越显式

扩展 能力 合理授权
JSON 菜单字段 运行已声明 action 用户编辑配置即授权
主题 palette/template 生成配置 远程只允许数据;代码型文件拒绝
hook/Lua 任意用户代码 只从用户主动维护路径执行
QML 插件 Shell 进程内任意代码 下载后禁用,review 后 enable

不要把所有扩展统一成“插件”。能力不同,信任模型也应不同。

经验六:原子提交只包住最小核心

主题切换把完整文件树 staging 后原子 swap,但外围应用 retint 并行最终一致。若把所有应用重启都放进锁,慢或坏掉的一个应用会阻塞整个主题系统。

更新同样把 package transaction 作为 migration 屏障,却不假装 AUR、mise、hooks 与 reboot 都是单个数据库事务。

设计时先问:

  1. 哪些读者绝不能看到半状态?
  2. 哪些消费者可以短暂滞后?
  3. 提交点之后失败怎样单独重试?

经验七:marker 必须代表可验证承诺

好 marker 的三个条件:

  • 名字指向明确动作/版本;
  • 只在所有必要副作用成功后创建;
  • 重跑逻辑知道 marker 缺失时如何安全继续。

坏 marker 只是“脚本曾开始”,会把失败永久隐藏。Quattro 的 migration/finalize/first-run 都把 marker 放在成功末尾,factory reset 更用 pending 阻止半清理系统继续 provisioning。

经验八:观察也有成本

NVIDIA detector 避免唤醒 suspended GPU;菜单一次快照包列表;多个 guard reader 只跑一次。这提醒我们:read-only 不等于 free。

硬件、网络、云 API 或大仓库中的状态读取都应衡量:耗时、功耗、锁竞争和副作用。

经验九:升级器是兼容产品,不是一次性脏脚本

Quattro upgrader 的 hash-aware defaults、live overlay、reload suppression 和 reboot gate 都在解决“旧世界必须活到新世界接管”的问题。

大型迁移应具备:

  • 可从多个旧版本启动的 bootstrap;
  • 已发布默认值的识别方法;
  • 用户定制的备份/保留策略;
  • 明确 cutover 点;
  • 失败后不要跨 cutover;
  • 可重跑与测试契约。

经验十:测试边界与运行边界对齐

纯 JS 模型用 Node 测,系统脚本在 fake HOME/stub world 测,真实 UI 在 disposable VM 测。试图把所有内容都塞进 headless unit test,只会得到大量假绿;所有内容都做 GUI test 又会又慢又脆。

测试层次应该回答不同问题:

flowchart LR
    Unit[变换逻辑正确?] --> Contract[脚本/路径/调用正确?]
    Contract --> Integration[真实服务能协作?]
    Integration --> Visual[用户真的看见/能操作?]

仍需警惕的代价

文件数量与发现成本

小命令可组合,但 430 个注册入口增加命名、文档与全局一致性维护。元数据 lint 和自动目录是必要成本,不是锦上添花。

Bash 中的隐式共享状态

大量 source 组合使环境传递方便,也让变量碰撞、errexit 边界和外部命令差异更难推理。严格模式、窄 helper 和测试必须持续跟上。

同进程插件故障域

QML 插件热加载体验好,但没有沙箱;恶意或错误代码可读取用户数据、卡住 Shell。manifest 校验只验证结构,不验证行为。

滚动系统无法完全事务化

快照、marker 与重跑显著改善恢复,但固件、AUR、外部状态和正在运行的会话不会自动回滚。文档必须诚实表达提交点与覆盖范围。

最后的架构图

flowchart TB
    Defaults[发行默认值] --> Compile[合并/编译/迁移]
    User[用户意图] --> Compile
    Compile --> State[活动生成状态]
    State --> CLI[小而可组合的命令]
    CLI --> OS[系统服务/包/硬件]
    State --> Shell[常驻桌面运行时]
    Shell --> User
    OS --> User
    Update[快照 + 包事务 + marker] --> Defaults
    Tests[CLI + contract + VM] --> Compile
    Tests --> CLI
    Tests --> Shell

Omarchy Quattro 的主线可以浓缩为一句话:以清楚的状态所有权为底座,用可组合命令驱动系统副作用,以常驻 Shell 承载交互,再用 staging、marker、快照和显式 cutover 管理失败。

需要定位具体实现时,继续查 源码索引、CLI 命令全集 与 状态路径表。