docs: 新增模块分层与系统托盘设计文档

This commit is contained in:
2026-06-24 13:54:21 +08:00
parent bf3de7fb92
commit e1113c0052
2 changed files with 372 additions and 0 deletions

116
docs/module-layering.org Normal file
View File

@@ -0,0 +1,116 @@
#+title: ZDWM 模块分层与依赖方向
#+OPTIONS: toc:2
* 动机
随着模块增加,跨模块的"契约"和"共享数据结构"开始散落:有的进了 =core/=(和窗口管理逻辑混在一起),有的不得不靠依赖倒置塞进消费方目录(如 tray 契约放 =bar/= 导致 backend 反向依赖 bar。结果是 core 职责不清、=bar ↔ backend= 出现交叉依赖、依赖方向不再单向。
本文档定义一套分层约定,把"跨模块共享的东西"从具体实现里抽出来,集中到两个目录,使依赖方向回到干净的单向。
* 核心标准:按"谁直接消费类型"归类
判断一个跨模块的东西进哪个目录,不看它"属于哪个模块",也不只看"是声明还是实现",而是看**它的类型被哪些层直接消费**
- 类型被**两个或以上对等的实现层**直接消费 → 类型放 =interface/=(跨模块声明)
- 只有**操作函数**被多个实现层共用、且这些层不该互相依赖 → 操作放 =common/=
- 类型 + 操作都只被一个实现层(及其依赖者)用 → 整个 ADT 放 =common/=,或就近放该实现层
关键推论:**只需消费"类型"的层,不该被迫背上"操作函数"的依赖**。这是把类型和操作分到两个目录的根本理由。
* 两个目录的定位
** =src/interface/= :跨模块共享的【声明】,零 .c
只放类型定义、多态接口的函数声明。绝对不放假装、零实现代码。被所有实现层依赖,自身不依赖任何实现层。
** =src/common/= :被多实现层共用的【操作函数】,.h + .c
放自包含的 ADT 操作(构造/清理/查询)。操作只依赖 =interface/= 里的类型(和 =base/= 工具),不依赖任何实现层的内部状态。被"需要这些操作的层"依赖。
注意:=common/= 不是"什么都往里塞的杂物间"。只有当操作被**多个不该互相依赖的实现层**共用时,才进 =common/=;只被单一实现层用的操作,就近放该层。
* 依赖方向
#+begin_example
interface/ ← 零依赖(纯声明),所有实现层都依赖它
common/ ← 依赖 interface用类型+ base/(工具),被需要操作的层依赖
core/ runtime/ (依赖 common + interface)
backend/ bar/ (只依赖 interface —— 不碰 common 的操作)
#+end_example
不变量:
- =interface/= 不依赖任何实现层。
- =common/= 只依赖 =interface/==base/=,不依赖任何实现层。
- 实现层之间core/backend/bar/runtime不直接相互依赖跨模块契约都经由 =interface/= 或 =common/=
- 因此 =bar ↔ backend= 这类对等层之间不再有交叉依赖:都通过 =interface/tray.h= 通信。
* 判定示例effect / plan / event
用上面的标准,逐个判定现有跨模块类型:
** =effect_t= → interface类型无 .c
消费方corepolicy/plan 产生 =effect_t=+ backend执行 =effect_t=)。两个对等层直接消费类型,所以类型进 =interface/effect.h==effect_t= 是纯数据,构造它的活(=plan_push_*=)属于 plan 的操作,所以 effect 在 interface 里只有类型、没有 .c。
#+begin_src c
/* backend 只消费展开后的 effect 数组,从不碰 plan_t */
bool backend_apply_effect(backend_t *backend, const effect_t *effects, size_t effect_count);
#+end_src
** =plan_t= → common完整 ADT类型 + 操作)
消费方:只 corepolicy 产生、收集 effect和 runtime提交后 reset/cleanup。backend 完全不消费 =plan_t=——runtime 提交时把 plan 拆成 =effect_t= 数组喂给 backend。所以 =plan_t= 非跨模块,类型和操作整个 ADT 都落 =common/plan.h= + =common/plan.c=。
** =event_t= → interface类型+ common操作
=event_t= 类型被 backend产生/填充)和 core/runtime路由/清理)直接消费 → 类型进 =interface/event.h=
=event_reset= / =event_cleanup= 这些操作被 backend 和 runtime 共用,而两者对等、不该互相依赖,操作不能归属任一层 → 操作落 =common/event.h= + =common/event.c=。
这是"类型和操作分两个目录"的典型:类型跨模块必须在 interfacebackend 要产 =event_t=),操作对等共用必须在 common。
* 汇总表
| 东西 | 类型在哪 | 操作在哪 | 判定理由 |
|------------------------------+-------------------------+-------------------------------------+--------------------------------------------------------------------------|
| =effect_t= | =interface/effect.h= | (无,构造归 plan 操作) | 类型跨模块core 产 + backend 执行),纯数据 |
| =plan_t= | =common/plan.h= | =common/plan.c= | 非跨模块,只 core/runtime 用,整个 ADT 在 common |
| =event_t= | =interface/event.h= | =common/event.h= + =common/event.c= | 类型跨模块;操作对等共用 |
| 后端接口(=backend_t= 等) | =interface/backend.h= | =backend/x11/*.c= | 多态接口:声明在 interface实现在具体后端 |
| tray 契约(=tray_t= 等) | =interface/tray.h= | =backend/x11/tray.c= | 多态接口bar 用、backend 实现,声明在 interface 消除 =bar↔backend= 交叉 |
| 通知协议listeners | =interface/listeners.h= | =core/listeners.c= | 多态接口core 发布、bar 订阅,声明在 interface |
| 基础类型(=window_id_t= 等) | =interface/types.h= | (按需) | 所有层共用 |
* 多态接口 vs 共享 ADT.c 归属的不同
=interface/= 里有两类声明,它们的 .c 归属不同,务必区分:
- **多态接口**=backend.h==tray.h==listeners.h=):声明一组"由谁来实现"的函数实现依赖具体实现层xcb / 未来 wayland。这类 .c **必须在实现层**=backend/x11/==core/=),不能在 interface——否则 interface 就"知道"了具体实现。
- **共享 ADT 的操作**plan、event 的操作):实现自包含、不依赖任何实现层。这类 .c 进 =common/=
换句话说:=interface/= 永远零 .c。多态接口的 .c 在实现层,共享操作的 .c 在 =common/=
* 和 include/zdwm/ 的区别
- =include/zdwm/= 是**对外公共 API**(给用户配置、插件使用)。
- =src/interface/= 是**内部跨模块契约**(给各子系统互相通信)。
两者职责不同,不合并。像 =backend.h==event.h==effect.h= 是内部的,不该进 =include/zdwm/= 暴露给外部。
* 迁移指引
把现有代码按本分层整理时,建议顺序:
1.=src/interface/==src/common/= 目录,加入 CMakeLists。
2. 先迁移"纯类型、零依赖"的声明进 =interface/==types.h==effect.h=(从 =core/plan.h= 抽出 =effect_t=)、=event.h= 的类型部分。
3. 多态接口声明迁移:=backend.h==listeners.h==interface/==.c= 留原处)。
4. 共享 ADT 迁移:=plan.h= + =plan.c= 整体进 =common/==event.h= 操作部分 + =event.c==common/=
5. 更新全项目 include 路径(="core/plan.h"=="common/plan.h"= 等)。
6. 每步后编译验证,确保依赖方向单向、无循环。
注意边界:遇到"看起来该进 interface、但其类型依赖还在 core"的情况(如某个 event 子结构引用了 =core/window.h= 的类型),要么把那个被引用的类型也提到 =interface/=,要么承认它暂不进 interface——**进 interface 的前提是它的所有依赖都在 interface**,不能硬塞。
* tray 的落点
tray 的契约(=tray_t= / =tray_api_t=)是 bar 与 backend 之间的多态接口,按本分层落 =interface/tray.h=(声明),实现在 =backend/x11/tray.c=。这样 bar 和 backend 都只依赖 =interface/tray.h=,彻底消除 =bar ↔ backend= 交叉依赖——这正是引入这套分层的最初动机之一。详见 [[file:tray-design.org][tray-design.org]]。