Files
Structive/docs/DESIGN.zh-CN.md
T
2026-08-07 16:51:40 +08:00

13 KiB
Raw Blame History

Structive 设计理念与原则

English

1. 设计目标

Structive 的目标是给普通 C++ 对象增加一层机器可理解的结构语义,同时不强迫业务对象改用框架自己的存储模型。

它应该能够回答:

  • 哪些成员属于公开结构模型?
  • 某个属性稳定的 runtime key 是什么?
  • 哪些属性允许外部读取或写入?
  • 哪些属性参与持久化?
  • 哪些元数据属于 Presentation、Validation 或其他扩展?
  • 哪些属性位于同一个同步域?
  • 动态适配器如何在遵守 capability 的前提下读写属性?

但完成这些事情以后,底层类型仍然应该看起来像正常的 C++。

2. 第一原则:增强,而不是替代

Structive 不是第二套对象模型,而是附加在已有 C++ 类型旁边的结构层。

推荐形态:

struct Device : structive::Property_Object<Device> {
    double temperature;
    std::string name;
};

而不是:

struct Device {
    Framework_Property<double> temperature;
    Framework_Property<std::string> name;
};

这个原则保护了原生 C++ 的几个关键性质:

  1. 成员仍然是真实成员。
  2. 成员指针仍然可以作为可靠身份。
  3. 所有权规则允许时仍然可以 raw access。
  4. 未注册成员可以继续作为实现细节存在。
  5. Structive 元数据可以和存储形式独立演进。

这个原则也有明确代价:Structive 不可能拦截直接成员访问。只有通过 managed path 的访问才会获得 Structive 管理行为。

3. 必须保持两层,而不是揉成一层

Structive 把类型级描述和实例级管理分开。

3.1 类型层

Type_Descriptor<T> 产生 Object_Schema,描述:

  • 注册属性;
  • 属性 key
  • accessor
  • Attribute
  • constraint
  • object 级可继承默认值;
  • 默认 synchronization plan。

这是类型的结构定义。

3.2 实例层

Property_Object<T> 提供:

  • 同一类型共享的默认解析 lock slot 拓扑;
  • 只有真实锁策略才需要的每实例 mutex 存储;
  • 只有显式同步覆盖实例才持有的紧凑 topology;
  • managed typed read/write
  • capability view
  • 静态与运行时多属性 guard
  • managed value traversal
  • Property_Object_Base type-erased runtime access。

这是实例行为,不是 Schema 身份。

3.3 设计约束

不要把可变的实例同步状态塞进 Schema,也不要让 Schema 必须依赖某一种特定的实例管理策略。由类型级默认同步计划推导出的不可变 topology 可以由同类型所有实例共享。

以后完全可能有用户只想描述大量普通对象,却不愿意为每个对象承担同步状态成本。当前架构应该持续保留这种可能性。

4. 注册必须显式

Structive 不认为所有 C++ 成员都天然属于结构模型。

struct Device : structive::Property_Object<Device> {
    int id;
    double temperature;
    mutable int internal_cache;
};

如果 Schema 只注册 idtemperature,那么 internal_cache 对 Structive 完全不可见。

这是故意的。结构暴露本身就是 API 设计,不应该根据物理布局自动推断。

原则

Schema 才是公开结构契约;struct 的物理成员集合不是自动 Schema。

5. C++ 内部优先使用编译期身份

业务 C++ 代码通常应该通过成员指针定位 member-backed property

device.read<&Device::temperature>();
device.write<&Device::temperature>(30.0);
device.schema().property<&Device::temperature>();

这样编译器能够建立最强的“对象类型—字段”关系。

数字 index 适合编译期泛型遍历;字符串 key 适合运行时边界。

身份层级

业务 C++ 代码          → member pointer
编译期泛型代码         → property index
runtime / adapter      → declared string key

不要因为存在 key,就把本来可以静态确定的 C++ 业务代码全部字符串化。

6. Key 是结构协议标识,不是显示名称

每个 Property_Descriptor 都必须拥有非空 key,同一 Schema 内 key 必须唯一。

Key 被 Schema lookup、动态锁选择、runtime read/write 使用。因此它不只是 UI label。

即使 C++ 成员名不变,修改 key 也可能改变外部协议或持久化契约。

原则

把 property key 当成协议级名字,重命名必须有意识。

7. 只有一套 Attribute 系统

Structive 不应该再长出 hintannotationui metadataserializer metadata 等彼此独立的元数据容器。

一个 Attribute 可以声明:

  • attribute_category:用于按 category 查询;
  • single_valued:同一个声明中该 category 是否只能有一个值;
  • inheritable:是否允许进入 object defaults
  • category 自己需要的 payload。

Core 和 Extension 使用完全相同的协议。

原则

增加语义时优先增加 Attribute category + interpreter,而不是再造一套 metadata framework。

8. Category 必须有明确所有者

Core 只解释属于 Core 的 category。

例如 External Access 和 Persistence Access 会直接影响 Core capability view,所以 Core 理解它们是合理的;但 Core 不需要理解 presentation::label

Presentation 解释 Presentation;未来 JSON 扩展解释 JSONRPC 扩展解释 RPC。

Core 只负责统一保存、遍历和提供查询机制。

依赖约束

Extension implementation
        ↓
Structive Property Core

不能为了某个扩展写起来方便,就反过来让 Core 依赖 Extension。

9. Defaults 是继承,不是隐藏行为

defaults(...) 用于支持可继承 Attribute。Property 级声明仍然可以覆盖同 category 的默认值。

当前 Core 中可继承的 category 包括 External Access、Persistence Access 和 Sensitive。

Defaults 应该用于稳定的对象级政策默认值,而不是用来制造大量隐式行为。

原则

Defaults 用来减少重复,但不能让一个属性最终生效的政策变得无法从 Schema 规则中推导。

10. Managed Access 与 Raw Access 是两种契约

Raw access

device.temperature = 30.0;

Managed access

device.write<&Device::temperature>(30.0);

Raw path 就是普通 C++,不会自动获得 Structive 锁和 capability 管理。

Managed path 会经过 descriptor 和该实例的同步拓扑。

两条路径同时存在是设计结果,不是漏洞。

原则

不要假装继承 Property_Object<T> 以后 public member 就自动变成强封装属性。如果某个子系统要求受管理同步,那么该子系统自己的编码约束必须要求使用 managed path。

11. Capability 是 Schema 的投影视图,不是第二套 Schema

同一个属性在不同 managed mode 下可以拥有不同可见性:

internal
external
persistence

Core 根据 Attribute 和 accessor 实际读写能力计算这些 capability。

External view 不应该发展成第二份 SchemaPersistence view 也不应该成为第二份 Schema。它们都只是同一 Schema 的能力投影。

原则

一份 property definition,多种 capability projection。

12. Validation = 元数据 + 显式操作

Constraint 描述候选值是否合法,但 write() 不会自动执行它。

必须保持分开的概念包括:

单字段 constraint
跨字段 invariant
locking
transaction boundary
rollback strategy
error reporting
side effects

一个通用 write() 不可能替所有业务正确猜出这些规则。

示例

auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>();
auto old_min = guard.get<&Device::min_speed>();
auto old_max = guard.get<&Device::max_speed>();
guard.set<&Device::min_speed>(candidate_min);
guard.set<&Device::max_speed>(candidate_max);
if (candidate_min > candidate_max) {
    guard.set<&Device::min_speed>(old_min);
    guard.set<&Device::max_speed>(old_max);
}

业务操作自己拥有 invariant 和 rollback 语义。

原则

不要把 write() 变成隐藏事务引擎。

13. Synchronization 就只负责 Synchronization

Synchronization plan 的本质是把 property 映射到 lock slot,支持 independent、shared、unsynchronized、grouped 等配置。

Group 表示一致性域:同一个 group 的 property 使用同一个 mutex slot。

多属性锁会对 slot 去重,并按稳定 slot 顺序获取锁。

Synchronization 不等于

  • validation
  • transaction
  • rollback
  • event emission
  • dirty tracking
  • persistence commit。

这些能力可以构建在 Structive 上层,但不能偷偷和锁耦合。

14. Computed Property 必须明确一致性边界

computed_property 通过 synchronized view 读取依赖项。这个 view 只允许读取和 computed property 本身处于同一个 resolved lock slot 的属性。

所以依赖 min_speedmax_speedspeed_span 应该和它们进入同一个同步组:

synchronization(
    sync_all_independent,
    sync_group("speed", "min_speed", "max_speed", "speed_span")
)

这样一致性关系由同步计划明确表达,而不是让 getter 随意读取任意字段。

Trusted Accessor

trusted_computed_propertytrusted_accessor_property 会直接调用对象上的受信任成员函数,设计上绕开 synchronized view 的依赖检查。

原则

默认优先 synchronized computed property。只有 accessor 自身明确拥有或保证同步语义时才使用 trusted accessor。

15. Runtime Access 是边界能力

Property_Object_Base 提供 type-erased runtime interface,核心输入包括:

  • string key
  • Managed_Access_Mode
  • std::type_info
  • 显式 result code。

它适用于编译期不知道具体类型的 adapter。

它不应该成为把所有正常 typed C++ 代码动态化的理由。

原则

能在编译期确定的代码就留在编译期;只有真正跨动态边界时才进入 runtime path。

16. Compile-time Error 和 Runtime Error 分工明确

Structive 对静态可知问题尽量在编译期拒绝:

  • member 未注册;
  • key 重复;
  • 同一成员存储被重复注册;
  • single-valued category 重复;
  • 非 inheritable Attribute 被放入 defaults(...)
  • accessor 实际能力与声明 capability 冲突。

Runtime failure 留给 runtime 输入和 runtime 配置:

  • 动态 key 不存在;
  • 动态属性对该 view 不可访问;
  • synchronization plan 引用了未知 key
  • runtime 同步规则重复配置同一个 property;
  • runtime type mismatch。

原则

静态可知的 Schema 错误不要拖到运行时;真正动态的 adapter 输入也不要硬伪装成编译期问题。

17. 同步策略必须保持可替换

Property_Object<Derived, Lock_Policy> 支持同步策略,默认是 Shared_Mutex_Policy,同时提供 No_Lock_Policy

这是重要架构缝隙。Core 语义不应该和某一种 mutex 或某一个全局 scheduler 焊死。

No_Lock_Policy 只是取消真实互斥,不代表并发访问自动安全。

18. Extension 设计规则

一个好的 Structive Extension 应该:

  1. 定义属于自己领域的 category。
  2. 复用 Core Attribute 协议。
  3. 只解释自己拥有或明确依赖的 category。
  4. 依赖 Core,而不是要求 Core 反向依赖自己。
  5. 把领域 fallback 行为留在扩展里。
  6. 优先消费已有 Schema,不复制第二棵 descriptor tree。
  7. 不因为 adapter 需要便利规则就修改 Core 基础语义。

19. Core 的非目标

Structive Core 不应该同时变成:

  • ORM
  • JSON 库;
  • GUI 框架;
  • RPC 框架;
  • signal/slot 系统;
  • 事务引擎;
  • 替代所有语言能力的“万能反射”。

Structive 应该提供足够强的结构契约,让这些系统来消费它。

20. 演进原则

修改库时依次问:

  1. 这是 structural model、instance management 还是 extension 的职责?
  2. 这个新概念本质上是不是一个 Attribute category,而不是新的 metadata channel
  3. 这个错误能否在编译期发现?
  4. 修改后是否仍然保留原生 C++ 成员语义?
  5. Raw access 与 managed access 的边界是否仍然清晰?
  6. 是否把 synchronization 和 validation、transaction、event 混在一起了?
  7. Core 是否开始理解本应属于 Extension 的领域概念?
  8. Member pointer 是否仍然是 typed API 最自然的身份?
  9. 新的 convenience API 是否重复制造了另一条同语义路径?
  10. 是否保持了已有函数签名和功能语义,而不是悄悄改义?

如果一个功能连续违反这些问题,应先重新考虑它属于哪一层,而不是立刻实现。

21. 架构总结

Structive 最强的形态应该是少量强概念,而不是大量魔法便利接口:

普通 C++ 类型
    + 显式 Schema
    + 一套 Attribute 模型
    + 显式 Capability
    + 显式 Validation
    + 显式 Synchronization
    + 可选 Managed Instance 行为
    + Extension 自己解释自己的语义

当泛型系统能够理解业务类型、却不需要接管业务类型时,Structive 的价值最大。