Files
zdwm/docs/module-layering.org
Zedhugh Chen 079f41ce8a refactor: 从 core 中抽出跨模块契约建立 interface/common 模块分层
引入 interface/(零实现声明层)与 common/(共享 ADT 操作层),把跨模块
契约从 core 实现里抽出来,依赖方向回到单向(base ← interface ← common ←
各实现层)。docs/module-layering.org 记录完整方案与判定标准。

interface/(纯类型与多态接口声明,零 .c):
- types.h / event.h / backend.h 从 core 移入
- effect.h 新建(effect_t 从 plan.h 抽出)

common/(多实现层共用的自包含操作,.h + .c):
- event.{h,c}:event_reset / event_cleanup
- listeners.{h,c}:listeners 维护(add / cleanup);notify 留 core
- window.{h,c}:window_list + layer_props/metadata cleanup + window_classify_layer
- workspace.{h,c}:workspace_desc(从 wm_desc.h 拆出,改真实函数)

window 相关整理:
- window_layer_type_t 上浮 interface/types.h(common 的 classify 需要)
- window_classify_layer 移 common/window
- window_info_t 独立成 core/window_info.h(state/command 共享,不寄生)
- 删除 wm_desc.h,base/window_list 并入 common/window

全项目 include 路径同步更新
2026-06-25 03:00:30 +08:00

137 lines
12 KiB
Org Mode
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#+title: ZDWM 模块分层与依赖方向
#+OPTIONS: toc:2
* 动机
随着模块增加,跨模块的"契约"和"共享数据结构"开始散落:有的进了 =core/=(和窗口管理逻辑混在一起),有的不得不靠依赖倒置塞进消费方目录(如 tray 契约放 =bar/= 导致 backend 反向依赖 bar。结果是 core 职责不清、=bar ↔ backend= 出现交叉依赖、依赖方向不再单向。
本文档定义一套分层约定,把"跨模块共享的东西"从具体实现里抽出来,集中到两个目录,使依赖方向回到干净的单向。
* 核心标准:按"谁直接消费类型"归类
判断一个跨模块的东西进哪个目录,不看它"属于哪个模块",也不只看"是声明还是实现",而是看**它的类型被哪些层直接消费**
- 类型被**两个或以上对等的实现层**直接消费 → 类型放 =interface/=(跨模块声明)
- 只有**操作函数**被多个实现层共用、且这些层不该互相依赖 → 操作放 =common/=
- 类型 + 操作都只被一个实现层(及其依赖者)用 → 整个 ADT 放 =common/=,或就近放该实现层
关键推论:**只需消费"类型"的层,不该被迫背上"操作函数"的依赖**。这是把类型和操作分到两个目录的根本理由。
* 目录的定位
先明确三层关系:=base/= 是**项目无关**的通用底层(任何 C 项目都能直接用),=interface/= 和 =common/= 是**项目业务相关**的通用内容。后两者依赖前者(业务依赖基础设施)。
** =src/base/= :项目无关的通用底层,零业务依赖
放任何 C 项目都能直接用的纯工具(=memory= / =array= / =macros= / =color= / =log= / =time= / =process=)。不依赖 interface/common/任何实现层——它根本不知道 ZDWM 的业务。=interface/= 和 =common/= 都可以依赖它。
** =src/interface/= :跨模块共享的【声明】,零 .c
只放类型定义、多态接口的函数声明。绝对不放假装、零实现代码。被所有实现层依赖,自身不依赖任何实现层(可依赖 =base/= 的纯类型)。
** =src/common/= :被多实现层共用的【操作函数】,.h + .c
放自包含的 ADT 操作(构造/清理/查询)。操作只依赖 =interface/= 里的类型(和 =base/= 工具),不依赖任何实现层的内部状态。被"需要这些操作的层"依赖。
注意:=common/= 不是"什么都往里塞的杂物间"。只有当操作被**多个不该互相依赖的实现层**共用时,才进 =common/=;只被单一实现层用的操作,就近放该层。
* 依赖方向
#+begin_example
base/ ← 项目无关的通用底层(任何 C 项目可用),零业务依赖
interface/ ← 零业务依赖(纯类型声明,含 backend 抽象接口);可依赖 base
common/ ← 项目业务通用的共享 ADT 操作;依赖 interface + base
core/ runtime/ backend/ bar/ ← 实现层,都依赖 common + interface+ base
#+end_example
注意:没有实现层"只依赖 interface"——所有实现层都用 =common/= 的业务通用操作。interface 的纯净性约束的是 interface *内部*(含 backend 抽象接口不依赖 interface 之外的任何东西),不是约束某个实现层的依赖深度。
不变量:
- =base/= 不依赖任何业务层interface/common/实现层),是项目无关的纯工具。
- =interface/=(含 backend 抽象接口)不依赖 =interface/= 之外的任何业务内容;可依赖 =base/= 的纯类型(业务层依赖基础设施,合理)。
- =common/= 只依赖 =interface/==base/=,不依赖任何实现层。
- 所有实现层core/runtime/backend/bar都依赖 =common/= + =interface/=+ =base/=);它们之间不直接相互依赖跨模块契约,都经由 =interface/==common/=
- 因此 =bar ↔ backend= 这类对等层之间不再有交叉依赖:都通过 =interface/tray.h= 通信。
* 判定示例effect / plan / event / listeners
用上面的标准,逐个判定现有跨模块类型:
** =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= → 留 core完整 ADT类型 + 操作)
消费方:只 corepolicy 产生、收集 effect和 runtime提交后 reset/cleanup。两者之间是 *runtime→core 的天然单向依赖*runtime 是桥接层,本就依赖 core 的 policy/state 等)。把 =plan_t==core/plan.h= + =core/plan.c=runtime 正向 include 即可,不破坏任何方向——所以它*不必*进 common。
这把 common 的门槛精确化common 是为"对等、无天然依赖"的消费者中立化(见下文 =listeners_t=);若消费者之间已有单向依赖(如 plan 的 core+runtime共享操作放被依赖方core即可不必上浮 common。
** =event_t= → interface类型+ common操作
=event_t= 类型进 =interface/event.h= 的关键理由interface 内的 =backend.h= 契约直接 *#include 了 event.h*——=backend_poll_event= / =backend_next_event= 的参数是 =event_t *=,且契约注释讨论 event 的所有权与 =event_cleanup= 语义。一份完整的后端契约要让读者看到 event_t 全貌(字段、联合、生命周期责任),而非甩一个前向声明。所以 event_t 必须在 interface若放 common=backend.h= 要么退化为前向声明(契约不完整),要么 interface 反向依赖 common破坏零依赖
=event_reset= / =event_cleanup= 这些操作被 backend 和 runtime 共用,两者对等、不该互相依赖 → 操作落 =common/event.h= + =common/event.c=。于是 event 是"类型在 interface、操作在 common"的典型。
** =listeners_t= → common类型 + 维护)+ corenotify
=listeners_t= 类型被 bar订阅、core发布、runtime生命周期消费但 **backend 完全不碰它**。判定关键它的消费者bar/config/runtime都*既用类型又用维护操作*=add_*= / =cleanup=),没有哪个层"只需类型、不需操作"——按核心推论(只有"只需消费类型"的层才迫使类型单独上提 interface类型不必进 interface。与 =event_t= 的对照就在这里:=event_t= 因被 interface 内的 =backend.h= 契约需要完整定义而必须留 interface=listeners_t= 则*不被 interface 内任何契约依赖*=backend.h= 完全不碰它),所以能整个进 common
- 类型 =listeners_t= + 维护操作 =add_*= / =cleanup=:自包含容器操作,被 bar/config/runtime 多个对等层共用 → 整个 ADT 进 =common/listeners.{h,c}=
- =notify_*=(触发):单消费者(只 core/runtime 发布侧调用),且依赖 core 内部(把 =state_t= 翻译成 =zdwm_window_t= 等 DTO→ 声明 + 实现都留 =core/listeners=
真正的跨层契约(回调签名 =zdwm_window_added=+ DTO已在 =include/zdwm/listeners.h=(比 interface 更公开的那层),=listeners_t= 只是这些回调的容器,所以它整个待在 common 自洽,不必单独占一个 interface 头。
* 汇总表
| 东西 | 类型在哪 | 操作在哪 | 判定理由 |
|------------------------------+-----------------------+-------------------------------------------+--------------------------------------------------------------------------|
| =effect_t= | =interface/effect.h= | (无,构造归 plan 操作) | 类型跨模块core 产 + backend 执行),纯数据 |
| =plan_t= | =core/plan.h= | =core/plan.c= | 消费者 runtime→core 单向,放 core 不破坏方向,不必 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 | =common/listeners.h= | =common/listeners.c= + =core/listeners.c= | 消费者 bar↔core 对等,必须 common 中立化notify 依赖 core 留 core |
| 基础类型(=window_id_t= 等) | =interface/types.h= | (按需) | 所有层共用 |
* 多态接口 vs 共享 ADT.c 归属的不同
=interface/= 里有两类声明,它们的 .c 归属不同,务必区分:
- **多态接口**=backend.h==tray.h=):声明一组"由谁来实现"的函数实现依赖具体实现层xcb / 未来 wayland。这类 .c **必须在实现层**=backend/x11/==core/=),不能在 interface——否则 interface 就"知道"了具体实现。
- **共享 ADT 的操作**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==interface/==.c= 留原处)。=listeners= 走共享 ADT、不进 interface=listeners_t= 类型 + =add_*= / =cleanup= 整体进 =common/listeners.{h,c}==notify_*==core/listeners=(详见上方判定示例)。
4. 共享 ADT 迁移:=event.h= 操作部分 + =event.c==common/=。(=plan= 留 core消费者 runtime→core 单向,不必上浮。)
5. 更新全项目 include 路径(="core/types.h"=="interface/types.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]]。