19 · 可复用的设计经验¶
Quattro 不是每个项目都应复制的模板,但它处理的几个问题具有普遍性:配置所有权、跨权限生命周期、数百命令发现、热加载代码与滚动迁移。
经验一:先画状态所有权,再写安装脚本¶
最有价值的分层是:
若这四类对象都放在 ~/.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 都是单个数据库事务。
设计时先问:
- 哪些读者绝不能看到半状态?
- 哪些消费者可以短暂滞后?
- 提交点之后失败怎样单独重试?
经验七: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 管理失败。