13 KiB
Structive 设计理念与原则
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++ 的几个关键性质:
- 成员仍然是真实成员。
- 成员指针仍然可以作为可靠身份。
- 所有权规则允许时仍然可以 raw access。
- 未注册成员可以继续作为实现细节存在。
- 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 存储;
- managed typed read/write;
- capability view;
- 静态与运行时多属性 guard;
- managed value traversal;
Property_Object_Basetype-erased runtime access。
这是实例行为,不是 Schema 身份。
3.3 设计约束
不要把实例状态塞进 Schema,也不要让 Schema 必须依赖某一种特定的实例管理策略。
以后完全可能有用户只想描述大量普通对象,却不愿意为每个对象承担同步状态成本。当前架构应该持续保留这种可能性。
4. 注册必须显式
Structive 不认为所有 C++ 成员都天然属于结构模型。
struct Device : structive::Property_Object<Device> {
int id;
double temperature;
mutable int internal_cache;
};
如果 Schema 只注册 id 和 temperature,那么 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 不应该再长出 hint、annotation、ui metadata、serializer 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 扩展解释 JSON;RPC 扩展解释 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 不应该发展成第二份 Schema;Persistence 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_speed、max_speed 的 speed_span 应该和它们进入同一个同步组:
synchronization(
sync_all_independent,
sync_group("speed", "min_speed", "max_speed", "speed_span")
)
这样一致性关系由同步计划明确表达,而不是让 getter 随意读取任意字段。
Trusted Accessor
trusted_computed_property 和 trusted_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 应该:
- 定义属于自己领域的 category。
- 复用 Core Attribute 协议。
- 只解释自己拥有或明确依赖的 category。
- 依赖 Core,而不是要求 Core 反向依赖自己。
- 把领域 fallback 行为留在扩展里。
- 优先消费已有 Schema,不复制第二棵 descriptor tree。
- 不因为 adapter 需要便利规则就修改 Core 基础语义。
19. Core 的非目标
Structive Core 不应该同时变成:
- ORM;
- JSON 库;
- GUI 框架;
- RPC 框架;
- signal/slot 系统;
- 事务引擎;
- 替代所有语言能力的“万能反射”。
Structive 应该提供足够强的结构契约,让这些系统来消费它。
20. 演进原则
修改库时依次问:
- 这是 structural model、instance management 还是 extension 的职责?
- 这个新概念本质上是不是一个 Attribute category,而不是新的 metadata channel?
- 这个错误能否在编译期发现?
- 修改后是否仍然保留原生 C++ 成员语义?
- Raw access 与 managed access 的边界是否仍然清晰?
- 是否把 synchronization 和 validation、transaction、event 混在一起了?
- Core 是否开始理解本应属于 Extension 的领域概念?
- Member pointer 是否仍然是 typed API 最自然的身份?
- 新的 convenience API 是否重复制造了另一条同语义路径?
- 是否保持了已有函数签名和功能语义,而不是悄悄改义?
如果一个功能连续违反这些问题,应先重新考虑它属于哪一层,而不是立刻实现。
21. 架构总结
Structive 最强的形态应该是少量强概念,而不是大量魔法便利接口:
普通 C++ 类型
+ 显式 Schema
+ 一套 Attribute 模型
+ 显式 Capability
+ 显式 Validation
+ 显式 Synchronization
+ 可选 Managed Instance 行为
+ Extension 自己解释自己的语义
当泛型系统能够理解业务类型、却不需要接管业务类型时,Structive 的价值最大。