首次提交
This commit is contained in:
@@ -0,0 +1,660 @@
|
||||
# Structive Property Core Guide
|
||||
|
||||
[中文](CORE_GUIDE.zh-CN.md)
|
||||
|
||||
## 1. Include and target
|
||||
|
||||
Include the complete Core surface with:
|
||||
|
||||
```cpp
|
||||
#include <structive/property/property.hpp>
|
||||
```
|
||||
|
||||
CMake target:
|
||||
|
||||
```cmake
|
||||
target_link_libraries(my_target PRIVATE structive::property_core)
|
||||
```
|
||||
|
||||
The Core target is header-only and requires C++20.
|
||||
|
||||
## 2. Define a managed object
|
||||
|
||||
A managed object normally derives from `Property_Object<Derived>`:
|
||||
|
||||
```cpp
|
||||
using namespace structive;
|
||||
struct Device : Property_Object<Device> {
|
||||
double temperature{25.0};
|
||||
double pressure{101.3};
|
||||
double min_speed{10.0};
|
||||
double max_speed{100.0};
|
||||
std::string name{"device-1"};
|
||||
};
|
||||
```
|
||||
|
||||
The members remain ordinary C++ members.
|
||||
|
||||
## 3. Define the type descriptor
|
||||
|
||||
Specialize `Type_Descriptor<T>` and return an `Object_Schema` through `object<T>(...)`:
|
||||
|
||||
```cpp
|
||||
template <>
|
||||
struct structive::Type_Descriptor<Device> {
|
||||
static auto get() {
|
||||
return object<Device>(
|
||||
defaults(
|
||||
external_access<External_Access::read_write>,
|
||||
persistence_access<Persistence_Access::load_store>
|
||||
),
|
||||
synchronization(
|
||||
sync_all_independent,
|
||||
sync_group<&Device::min_speed, &Device::max_speed>("speed_range")
|
||||
),
|
||||
field<&Device::temperature>(
|
||||
key<"temperature">,
|
||||
unit<"C">,
|
||||
min_value<-50.0>,
|
||||
max_value<200.0>
|
||||
),
|
||||
field<&Device::pressure>(key<"pressure">, unit<"kPa">),
|
||||
field<&Device::min_speed>(key<"min_speed">),
|
||||
field<&Device::max_speed>(key<"max_speed">),
|
||||
field<&Device::name>(key<"name">)
|
||||
);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
`field<Member>(...)` is an alias of `property<Member>(...)` and produces a member-backed `Property_Descriptor`.
|
||||
|
||||
## 4. Schema guarantees
|
||||
|
||||
The schema enforces several structural conditions:
|
||||
|
||||
- every property has a non-empty `key`;
|
||||
- property keys are unique;
|
||||
- the same member storage identity cannot be registered twice;
|
||||
- single-valued Attribute categories cannot appear more than once on the same declaration;
|
||||
- constraints must be compatible with the property value type;
|
||||
- object defaults may contain only inheritable Attributes;
|
||||
- external/persistence capabilities cannot claim operations unsupported by the accessor.
|
||||
|
||||
Many schema errors are therefore compile-time errors.
|
||||
|
||||
## 5. Access the schema
|
||||
|
||||
For a described type:
|
||||
|
||||
```cpp
|
||||
const auto& schema = type_descriptor<Device>();
|
||||
```
|
||||
|
||||
For an instance:
|
||||
|
||||
```cpp
|
||||
Device device;
|
||||
const auto& schema = device.schema();
|
||||
```
|
||||
|
||||
Property lookup supports a numeric compile-time index or a member pointer:
|
||||
|
||||
```cpp
|
||||
const auto& first = schema.property<0>();
|
||||
const auto& temperature = schema.property<&Device::temperature>();
|
||||
```
|
||||
|
||||
Use member pointers in typed business code. Use indexes mainly in generic traversal.
|
||||
|
||||
## 6. Property descriptor information
|
||||
|
||||
A property descriptor exposes:
|
||||
|
||||
```cpp
|
||||
auto key_value = temperature.key();
|
||||
```
|
||||
|
||||
and compile-time traits such as:
|
||||
|
||||
```cpp
|
||||
using Property = std::remove_cvref_t<decltype(temperature)>;
|
||||
static_assert(Property::readable);
|
||||
static_assert(Property::writable);
|
||||
```
|
||||
|
||||
Category-based Attribute lookup is available through:
|
||||
|
||||
```cpp
|
||||
static_assert(Property::has_attribute<Unit_Category>);
|
||||
const auto& attribute = temperature.attribute<Unit_Category>();
|
||||
```
|
||||
|
||||
All declared Attributes can be traversed:
|
||||
|
||||
```cpp
|
||||
temperature.for_each_attribute([](const auto& attribute) {
|
||||
// inspect attribute type/value
|
||||
});
|
||||
```
|
||||
|
||||
Constraints can be traversed separately:
|
||||
|
||||
```cpp
|
||||
temperature.for_each_constraint([](const auto& constraint_value) {
|
||||
// inspect or evaluate a constraint
|
||||
});
|
||||
```
|
||||
|
||||
## 7. Object defaults and effective Attributes
|
||||
|
||||
`defaults(...)` provides object-wide values for inheritable Attribute categories:
|
||||
|
||||
```cpp
|
||||
defaults(
|
||||
external_access<External_Access::read_write>,
|
||||
persistence_access<Persistence_Access::load_store>,
|
||||
sensitive<false>
|
||||
)
|
||||
```
|
||||
|
||||
A property may override a default:
|
||||
|
||||
```cpp
|
||||
field<&Device::name>(
|
||||
key<"name">,
|
||||
external_access<External_Access::read>
|
||||
)
|
||||
```
|
||||
|
||||
Core exposes effective access traits such as `effective_external_access_v`, `external_readable_v`, `external_writable_v`, `persistence_loadable_v`, `persistence_storable_v` and `effective_sensitive_v` for generic code.
|
||||
|
||||
Extensions can use `declared_effective_attribute<Index, Category>(schema)` for extension-owned inheritable categories when either the property or object defaults declare that category.
|
||||
|
||||
## 8. Core Attributes
|
||||
|
||||
### 8.1 Key
|
||||
|
||||
```cpp
|
||||
key<"temperature">
|
||||
```
|
||||
|
||||
Required for every property. Non-empty and unique per schema.
|
||||
|
||||
### 8.2 External access
|
||||
|
||||
```cpp
|
||||
external_access<External_Access::none>
|
||||
external_access<External_Access::read>
|
||||
external_access<External_Access::write>
|
||||
external_access<External_Access::read_write>
|
||||
```
|
||||
|
||||
This Attribute is inheritable through `defaults(...)`.
|
||||
|
||||
### 8.3 Persistence access
|
||||
|
||||
```cpp
|
||||
persistence_access<Persistence_Access::none>
|
||||
persistence_access<Persistence_Access::load>
|
||||
persistence_access<Persistence_Access::store>
|
||||
persistence_access<Persistence_Access::load_store>
|
||||
```
|
||||
|
||||
This Attribute is also inheritable.
|
||||
|
||||
### 8.4 Unit
|
||||
|
||||
```cpp
|
||||
unit<"C">
|
||||
```
|
||||
|
||||
This is descriptive metadata and is not inheritable.
|
||||
|
||||
### 8.5 Sensitive
|
||||
|
||||
```cpp
|
||||
sensitive<>
|
||||
sensitive<false>
|
||||
```
|
||||
|
||||
This is inheritable metadata. Core exposes its effective value but does not automatically redact data.
|
||||
|
||||
## 9. Constraints and validation
|
||||
|
||||
Built-in constraints:
|
||||
|
||||
```cpp
|
||||
min_value<0>
|
||||
max_value<100>
|
||||
finite
|
||||
```
|
||||
|
||||
Custom constraint:
|
||||
|
||||
```cpp
|
||||
constraint<"even">([](int value) {
|
||||
return value % 2 == 0;
|
||||
})
|
||||
```
|
||||
|
||||
Validation by member pointer:
|
||||
|
||||
```cpp
|
||||
auto error = validate_property_value<&Device::temperature>(device.schema(), candidate);
|
||||
```
|
||||
|
||||
Validation by compile-time key:
|
||||
|
||||
```cpp
|
||||
auto error = validate_property_key_value<"temperature">(device.schema(), candidate);
|
||||
```
|
||||
|
||||
A failure returns:
|
||||
|
||||
```cpp
|
||||
struct Validation_Error {
|
||||
std::string_view property_key;
|
||||
std::string_view code;
|
||||
};
|
||||
```
|
||||
|
||||
Validation is explicit and is not automatically executed by managed writes.
|
||||
|
||||
## 10. Managed internal read/write
|
||||
|
||||
Typed member-pointer access:
|
||||
|
||||
```cpp
|
||||
auto temperature = device.read<&Device::temperature>();
|
||||
device.write<&Device::temperature>(30.0);
|
||||
```
|
||||
|
||||
Compile-time key access:
|
||||
|
||||
```cpp
|
||||
auto temperature = device.read_key<"temperature">();
|
||||
device.write_key<"temperature">(30.0);
|
||||
```
|
||||
|
||||
These operations use `Managed_Access_Mode::internal`.
|
||||
|
||||
`read()` returns a value object, not a reference to storage. The read is performed while the configured shared lock is held when synchronization is enabled.
|
||||
|
||||
## 11. External capability view
|
||||
|
||||
Object-level defaults may expose properties externally:
|
||||
|
||||
```cpp
|
||||
defaults(external_access<External_Access::read_write>)
|
||||
```
|
||||
|
||||
Use:
|
||||
|
||||
```cpp
|
||||
device.external().write<&Device::temperature>(30.0);
|
||||
auto value = device.external().read<&Device::temperature>();
|
||||
```
|
||||
|
||||
A const object produces a const capability view and therefore has no write API.
|
||||
|
||||
The typed API rejects statically inaccessible operations at compile time.
|
||||
|
||||
## 12. Persistence capability view
|
||||
|
||||
Use persistence terminology instead of generic write/read:
|
||||
|
||||
```cpp
|
||||
device.persistence().load<&Device::temperature>(30.0);
|
||||
auto value = device.persistence().store<&Device::temperature>();
|
||||
```
|
||||
|
||||
Compile-time key forms are also available:
|
||||
|
||||
```cpp
|
||||
device.persistence().load_key<"temperature">(30.0);
|
||||
auto value = device.persistence().store_key<"temperature">();
|
||||
```
|
||||
|
||||
The persistence view is controlled by `Persistence_Access` metadata.
|
||||
|
||||
## 13. Synchronization plans
|
||||
|
||||
### 13.1 Default independent
|
||||
|
||||
The default `Synchronization_Plan` is independent: each synchronized property receives its own logical lock domain.
|
||||
|
||||
Explicit form:
|
||||
|
||||
```cpp
|
||||
synchronization(sync_all_independent)
|
||||
```
|
||||
|
||||
### 13.2 Shared
|
||||
|
||||
```cpp
|
||||
synchronization(sync_all_shared)
|
||||
```
|
||||
|
||||
All properties use the same lock slot unless overridden.
|
||||
|
||||
### 13.3 Unsynchronized
|
||||
|
||||
```cpp
|
||||
synchronization(sync_all_unsynchronized)
|
||||
```
|
||||
|
||||
Properties use the `unsynchronized_slot` and no real Structive mutex protects them.
|
||||
|
||||
### 13.4 Per-property override
|
||||
|
||||
By member pointer:
|
||||
|
||||
```cpp
|
||||
sync_independent<&Device::temperature>()
|
||||
sync_unsynchronized<&Device::immutable_id>()
|
||||
```
|
||||
|
||||
Runtime string forms also exist:
|
||||
|
||||
```cpp
|
||||
sync_independent("temperature")
|
||||
sync_unsynchronized("immutable_id")
|
||||
```
|
||||
|
||||
Prefer member-pointer rules when the property is statically known.
|
||||
|
||||
### 13.5 Groups
|
||||
|
||||
Member-pointer form:
|
||||
|
||||
```cpp
|
||||
sync_group<&Device::min_speed, &Device::max_speed>("speed_range")
|
||||
```
|
||||
|
||||
String form:
|
||||
|
||||
```cpp
|
||||
sync_group("speed_range", "min_speed", "max_speed")
|
||||
```
|
||||
|
||||
All properties in a group share one lock slot.
|
||||
|
||||
The resolver rejects unknown runtime keys, duplicate group names, empty groups and multiple explicit configurations of the same property.
|
||||
|
||||
## 14. Per-instance synchronization override
|
||||
|
||||
The type descriptor supplies a default synchronization plan, but an instance may override it through `Property_Synchronization`:
|
||||
|
||||
```cpp
|
||||
struct Device : Property_Object<Device> {
|
||||
Device() = default;
|
||||
explicit Device(Property_Synchronization synchronization) : Property_Object(std::move(synchronization)) {}
|
||||
};
|
||||
Device shared_device{
|
||||
property_synchronization(synchronization(sync_all_shared))
|
||||
};
|
||||
```
|
||||
|
||||
For schema-aware compile-time synchronization specifications:
|
||||
|
||||
```cpp
|
||||
auto policy = property_synchronization<Device>(
|
||||
synchronization(
|
||||
sync_all_independent,
|
||||
sync_group<&Device::temperature, &Device::pressure>("environment")
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
The schema remains the same; only the instance lock topology changes.
|
||||
|
||||
## 15. Inspect resolved synchronization
|
||||
|
||||
For a typed member:
|
||||
|
||||
```cpp
|
||||
auto slot = device.lock_slot<&Device::temperature>();
|
||||
```
|
||||
|
||||
For the complete view:
|
||||
|
||||
```cpp
|
||||
auto resolved = device.resolved_synchronization();
|
||||
auto count = resolved.lock_count;
|
||||
auto slot = resolved.slot(0);
|
||||
auto uses_lock = resolved.uses_lock(0);
|
||||
```
|
||||
|
||||
`Resolved_Synchronization_View::unsynchronized_slot` identifies unsynchronized properties.
|
||||
|
||||
## 16. Multi-property static guards
|
||||
|
||||
Shared guard:
|
||||
|
||||
```cpp
|
||||
auto guard = device.lock_shared<&Device::temperature, &Device::pressure>();
|
||||
auto temperature = guard.get<&Device::temperature>();
|
||||
auto pressure = guard.get<&Device::pressure>();
|
||||
```
|
||||
|
||||
Unique guard:
|
||||
|
||||
```cpp
|
||||
auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>();
|
||||
auto old_min = guard.get<&Device::min_speed>();
|
||||
guard.set<&Device::min_speed>(20.0);
|
||||
guard.set<&Device::max_speed>(120.0);
|
||||
```
|
||||
|
||||
The guard only permits access to properties inside its held synchronization set.
|
||||
|
||||
Capability views provide equivalent static guards constrained by their access mode:
|
||||
|
||||
```cpp
|
||||
auto guard = device.external().lock_shared<&Device::temperature>();
|
||||
```
|
||||
|
||||
## 17. Dynamic-key guards
|
||||
|
||||
When a property set is known only at runtime:
|
||||
|
||||
```cpp
|
||||
std::array<std::string_view, 2> keys{"temperature", "pressure"};
|
||||
auto guard = device.lock_shared(keys);
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```cpp
|
||||
auto guard = device.lock_unique({"min_speed", "max_speed"});
|
||||
```
|
||||
|
||||
Dynamic guards validate keys and capability visibility at runtime. Lock slots are sorted and deduplicated before acquisition.
|
||||
|
||||
Read/write through a dynamic guard can still use compile-time members/keys after the guard is acquired; access outside the held set throws `std::logic_error`.
|
||||
|
||||
## 18. Traversal
|
||||
|
||||
Schema traversal:
|
||||
|
||||
```cpp
|
||||
schema.for_each_property([](auto index, const auto& property) {
|
||||
// compile-time index and descriptor
|
||||
});
|
||||
```
|
||||
|
||||
Managed value traversal:
|
||||
|
||||
```cpp
|
||||
device.for_each_readable([](auto index, const auto& descriptor, const auto& value) {
|
||||
// one managed read per property
|
||||
});
|
||||
```
|
||||
|
||||
Locked traversal acquires the complete readable synchronization set first:
|
||||
|
||||
```cpp
|
||||
device.external().for_each_readable_locked([](auto index, const auto& descriptor, const auto& value) {
|
||||
// all selected readable properties are held under the guard
|
||||
});
|
||||
```
|
||||
|
||||
For coordinated writable access:
|
||||
|
||||
```cpp
|
||||
device.with_all_writable_locked([](auto& guard) {
|
||||
// use guard.get / guard.set
|
||||
});
|
||||
```
|
||||
|
||||
The persistence view provides `with_all_loadable_locked(...)`.
|
||||
|
||||
## 19. Computed property
|
||||
|
||||
A synchronized computed property receives a read view:
|
||||
|
||||
```cpp
|
||||
computed_property<Device, double>([](const auto& view) {
|
||||
return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>();
|
||||
}, key<"speed_span">, external_access<External_Access::read>)
|
||||
```
|
||||
|
||||
The computed property itself and every dependency read through the view must resolve to the same lock slot.
|
||||
|
||||
For example:
|
||||
|
||||
```cpp
|
||||
synchronization(
|
||||
sync_all_independent,
|
||||
sync_group("speed", "min_speed", "max_speed", "speed_span")
|
||||
)
|
||||
```
|
||||
|
||||
Computed properties are read-only.
|
||||
|
||||
## 20. Trusted accessor properties
|
||||
|
||||
Core also supports member-function-based trusted access:
|
||||
|
||||
```cpp
|
||||
trusted_computed_property<&Device::get_temperature>(key<"temperature">)
|
||||
```
|
||||
|
||||
and getter/setter pairs:
|
||||
|
||||
```cpp
|
||||
trusted_accessor_property<&Device::get_temperature, &Device::set_temperature>(key<"temperature">)
|
||||
```
|
||||
|
||||
These accessors invoke the object member functions directly and mark themselves as trusted object access. They do not use the synchronized computed-view dependency restriction. Use them when the member functions themselves define the correct synchronization/consistency contract.
|
||||
|
||||
## 21. Runtime type-erased access
|
||||
|
||||
Any `Property_Object<T>` is also a `Property_Object_Base`:
|
||||
|
||||
```cpp
|
||||
Property_Object_Base& erased = device;
|
||||
```
|
||||
|
||||
Introspection:
|
||||
|
||||
```cpp
|
||||
erased.runtime_object_type();
|
||||
erased.runtime_property_count();
|
||||
```
|
||||
|
||||
Runtime write:
|
||||
|
||||
```cpp
|
||||
double value = 35.0;
|
||||
auto result = erased.runtime_write(
|
||||
Managed_Access_Mode::external,
|
||||
"temperature",
|
||||
typeid(double),
|
||||
&value
|
||||
);
|
||||
```
|
||||
|
||||
Runtime read uses a callback:
|
||||
|
||||
```cpp
|
||||
static void read_double(void* context, std::size_t, std::string_view, const std::type_info& type, const void* value) {
|
||||
if (type == typeid(double)) {
|
||||
*static_cast<double*>(context) = *static_cast<const double*>(value);
|
||||
}
|
||||
}
|
||||
double output = 0.0;
|
||||
auto result = erased.runtime_read(
|
||||
Managed_Access_Mode::external,
|
||||
"temperature",
|
||||
&output,
|
||||
&read_double
|
||||
);
|
||||
```
|
||||
|
||||
Possible results:
|
||||
|
||||
```cpp
|
||||
Runtime_Access_Result::ok
|
||||
Runtime_Access_Result::unknown_property
|
||||
Runtime_Access_Result::not_readable
|
||||
Runtime_Access_Result::not_writable
|
||||
Runtime_Access_Result::type_mismatch
|
||||
```
|
||||
|
||||
The runtime path uses the same capability and synchronization rules as typed managed access.
|
||||
|
||||
## 22. Lock policy
|
||||
|
||||
Default:
|
||||
|
||||
```cpp
|
||||
struct Device : Property_Object<Device, Shared_Mutex_Policy> {
|
||||
};
|
||||
```
|
||||
|
||||
Equivalent shorthand:
|
||||
|
||||
```cpp
|
||||
struct Device : Property_Object<Device> {
|
||||
};
|
||||
```
|
||||
|
||||
A no-op lock policy exists:
|
||||
|
||||
```cpp
|
||||
struct Device : Property_Object<Device, No_Lock_Policy> {
|
||||
};
|
||||
```
|
||||
|
||||
`No_Lock_Policy` uses `Null_Shared_Mutex`; it removes real mutual exclusion and should only be selected when external ownership guarantees make that correct.
|
||||
|
||||
## 23. Raw object access
|
||||
|
||||
`unsafe_object()` exposes the derived object directly:
|
||||
|
||||
```cpp
|
||||
Device& raw = device.unsafe_object();
|
||||
```
|
||||
|
||||
Direct field access is also normal C++:
|
||||
|
||||
```cpp
|
||||
device.temperature = 40.0;
|
||||
```
|
||||
|
||||
These paths intentionally bypass Structive-managed access. They are useful when the caller already owns the required synchronization or when a field is being handled outside Structive’s managed contract.
|
||||
|
||||
## 24. Recommended usage rules
|
||||
|
||||
- Use member pointers for typed business access.
|
||||
- Use string keys at runtime integration boundaries.
|
||||
- Treat keys as protocol identifiers.
|
||||
- Keep object-wide access policy in `defaults(...)` and override only exceptions.
|
||||
- Use synchronization groups for fields that must be observed consistently.
|
||||
- Validate explicitly at the business operation boundary.
|
||||
- Use static multi-property guards for cross-field invariants.
|
||||
- Prefer synchronized computed properties over trusted accessors.
|
||||
- Use runtime access for adapters, not ordinary typed code.
|
||||
- Use raw access only when the caller intentionally owns the missing managed guarantees.
|
||||
@@ -0,0 +1,660 @@
|
||||
# Structive Property Core 完整指南
|
||||
|
||||
[English](CORE_GUIDE.md)
|
||||
|
||||
## 1. Include 与 CMake Target
|
||||
|
||||
完整 Core 接口:
|
||||
|
||||
```cpp
|
||||
#include <structive/property/property.hpp>
|
||||
```
|
||||
|
||||
CMake:
|
||||
|
||||
```cmake
|
||||
target_link_libraries(my_target PRIVATE structive::property_core)
|
||||
```
|
||||
|
||||
Core 是 header-only,要求 C++20。
|
||||
|
||||
## 2. 定义 Managed Object
|
||||
|
||||
通常让业务对象继承 `Property_Object<Derived>`:
|
||||
|
||||
```cpp
|
||||
using namespace structive;
|
||||
struct Device : Property_Object<Device> {
|
||||
double temperature{25.0};
|
||||
double pressure{101.3};
|
||||
double min_speed{10.0};
|
||||
double max_speed{100.0};
|
||||
std::string name{"device-1"};
|
||||
};
|
||||
```
|
||||
|
||||
这些成员仍然都是普通 C++ 成员。
|
||||
|
||||
## 3. 定义 Type Descriptor
|
||||
|
||||
特化 `Type_Descriptor<T>`,通过 `object<T>(...)` 返回 `Object_Schema`:
|
||||
|
||||
```cpp
|
||||
template <>
|
||||
struct structive::Type_Descriptor<Device> {
|
||||
static auto get() {
|
||||
return object<Device>(
|
||||
defaults(
|
||||
external_access<External_Access::read_write>,
|
||||
persistence_access<Persistence_Access::load_store>
|
||||
),
|
||||
synchronization(
|
||||
sync_all_independent,
|
||||
sync_group<&Device::min_speed, &Device::max_speed>("speed_range")
|
||||
),
|
||||
field<&Device::temperature>(
|
||||
key<"temperature">,
|
||||
unit<"C">,
|
||||
min_value<-50.0>,
|
||||
max_value<200.0>
|
||||
),
|
||||
field<&Device::pressure>(key<"pressure">, unit<"kPa">),
|
||||
field<&Device::min_speed>(key<"min_speed">),
|
||||
field<&Device::max_speed>(key<"max_speed">),
|
||||
field<&Device::name>(key<"name">)
|
||||
);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
`field<Member>(...)` 是 `property<Member>(...)` 的别名,产生 member-backed `Property_Descriptor`。
|
||||
|
||||
## 4. Schema 提供的静态保证
|
||||
|
||||
Schema 会检查:
|
||||
|
||||
- 每个 property 都必须拥有非空 `key`;
|
||||
- 同一 Schema 内 key 唯一;
|
||||
- 同一个成员存储不能重复注册;
|
||||
- single-valued Attribute category 不能在同一声明中重复;
|
||||
- constraint 必须能作用于 property value type;
|
||||
- `defaults(...)` 只能放 inheritable Attribute;
|
||||
- External/Persistence capability 不能声明 accessor 实际不支持的读写能力。
|
||||
|
||||
因此大量结构错误会直接成为编译期错误。
|
||||
|
||||
## 5. 获取 Schema
|
||||
|
||||
类型级:
|
||||
|
||||
```cpp
|
||||
const auto& schema = type_descriptor<Device>();
|
||||
```
|
||||
|
||||
实例级:
|
||||
|
||||
```cpp
|
||||
Device device;
|
||||
const auto& schema = device.schema();
|
||||
```
|
||||
|
||||
可以通过编译期 index 或成员指针定位 property:
|
||||
|
||||
```cpp
|
||||
const auto& first = schema.property<0>();
|
||||
const auto& temperature = schema.property<&Device::temperature>();
|
||||
```
|
||||
|
||||
业务 typed code 优先成员指针;index 主要用于泛型遍历。
|
||||
|
||||
## 6. Property Descriptor
|
||||
|
||||
获取 key:
|
||||
|
||||
```cpp
|
||||
auto key_value = temperature.key();
|
||||
```
|
||||
|
||||
静态能力:
|
||||
|
||||
```cpp
|
||||
using Property = std::remove_cvref_t<decltype(temperature)>;
|
||||
static_assert(Property::readable);
|
||||
static_assert(Property::writable);
|
||||
```
|
||||
|
||||
按 category 查询 Attribute:
|
||||
|
||||
```cpp
|
||||
static_assert(Property::has_attribute<Unit_Category>);
|
||||
const auto& attribute = temperature.attribute<Unit_Category>();
|
||||
```
|
||||
|
||||
遍历全部声明 Attribute:
|
||||
|
||||
```cpp
|
||||
temperature.for_each_attribute([](const auto& attribute) {
|
||||
// 根据 attribute 类型处理
|
||||
});
|
||||
```
|
||||
|
||||
单独遍历 constraint:
|
||||
|
||||
```cpp
|
||||
temperature.for_each_constraint([](const auto& constraint_value) {
|
||||
// 检查 constraint
|
||||
});
|
||||
```
|
||||
|
||||
## 7. Object Defaults 与 Effective Attribute
|
||||
|
||||
`defaults(...)` 给 inheritable Attribute 提供对象级默认值:
|
||||
|
||||
```cpp
|
||||
defaults(
|
||||
external_access<External_Access::read_write>,
|
||||
persistence_access<Persistence_Access::load_store>,
|
||||
sensitive<false>
|
||||
)
|
||||
```
|
||||
|
||||
Property 可以覆盖:
|
||||
|
||||
```cpp
|
||||
field<&Device::name>(
|
||||
key<"name">,
|
||||
external_access<External_Access::read>
|
||||
)
|
||||
```
|
||||
|
||||
Core 提供 `effective_external_access_v`、`external_readable_v`、`external_writable_v`、`persistence_loadable_v`、`persistence_storable_v`、`effective_sensitive_v` 等有效属性计算入口。
|
||||
|
||||
Extension 自己的 inheritable category 可以通过 `declared_effective_attribute<Index, Category>(schema)` 读取 property 声明或 object default 中的有效值。
|
||||
|
||||
## 8. Core Attribute
|
||||
|
||||
### 8.1 Key
|
||||
|
||||
```cpp
|
||||
key<"temperature">
|
||||
```
|
||||
|
||||
每个 property 必须存在,不能为空,同一 Schema 内唯一。
|
||||
|
||||
### 8.2 External Access
|
||||
|
||||
```cpp
|
||||
external_access<External_Access::none>
|
||||
external_access<External_Access::read>
|
||||
external_access<External_Access::write>
|
||||
external_access<External_Access::read_write>
|
||||
```
|
||||
|
||||
支持在 `defaults(...)` 中继承。
|
||||
|
||||
### 8.3 Persistence Access
|
||||
|
||||
```cpp
|
||||
persistence_access<Persistence_Access::none>
|
||||
persistence_access<Persistence_Access::load>
|
||||
persistence_access<Persistence_Access::store>
|
||||
persistence_access<Persistence_Access::load_store>
|
||||
```
|
||||
|
||||
同样支持继承。
|
||||
|
||||
### 8.4 Unit
|
||||
|
||||
```cpp
|
||||
unit<"C">
|
||||
```
|
||||
|
||||
描述元数据,不支持 object default 继承。
|
||||
|
||||
### 8.5 Sensitive
|
||||
|
||||
```cpp
|
||||
sensitive<>
|
||||
sensitive<false>
|
||||
```
|
||||
|
||||
支持继承。Core 会计算 effective value,但不会自动执行脱敏或隐藏输出。
|
||||
|
||||
## 9. Constraint 与 Validation
|
||||
|
||||
内置 constraint:
|
||||
|
||||
```cpp
|
||||
min_value<0>
|
||||
max_value<100>
|
||||
finite
|
||||
```
|
||||
|
||||
自定义 constraint:
|
||||
|
||||
```cpp
|
||||
constraint<"even">([](int value) {
|
||||
return value % 2 == 0;
|
||||
})
|
||||
```
|
||||
|
||||
按成员指针验证:
|
||||
|
||||
```cpp
|
||||
auto error = validate_property_value<&Device::temperature>(device.schema(), candidate);
|
||||
```
|
||||
|
||||
按编译期 key 验证:
|
||||
|
||||
```cpp
|
||||
auto error = validate_property_key_value<"temperature">(device.schema(), candidate);
|
||||
```
|
||||
|
||||
失败结果:
|
||||
|
||||
```cpp
|
||||
struct Validation_Error {
|
||||
std::string_view property_key;
|
||||
std::string_view code;
|
||||
};
|
||||
```
|
||||
|
||||
Validation 是显式操作,不会在 managed write 中自动执行。
|
||||
|
||||
## 10. Internal Managed Read/Write
|
||||
|
||||
成员指针形式:
|
||||
|
||||
```cpp
|
||||
auto temperature = device.read<&Device::temperature>();
|
||||
device.write<&Device::temperature>(30.0);
|
||||
```
|
||||
|
||||
编译期 key 形式:
|
||||
|
||||
```cpp
|
||||
auto temperature = device.read_key<"temperature">();
|
||||
device.write_key<"temperature">(30.0);
|
||||
```
|
||||
|
||||
这些操作属于 `Managed_Access_Mode::internal`。
|
||||
|
||||
`read()` 返回值对象而不是底层存储引用。启用同步时,读取发生在配置的 shared lock 持有期间。
|
||||
|
||||
## 11. External Capability View
|
||||
|
||||
可以通过 object defaults 默认开放 external access:
|
||||
|
||||
```cpp
|
||||
defaults(external_access<External_Access::read_write>)
|
||||
```
|
||||
|
||||
使用:
|
||||
|
||||
```cpp
|
||||
device.external().write<&Device::temperature>(30.0);
|
||||
auto value = device.external().read<&Device::temperature>();
|
||||
```
|
||||
|
||||
Const object 返回 const capability view,因此没有 write API。
|
||||
|
||||
Typed API 对静态不可访问操作直接在编译期拒绝。
|
||||
|
||||
## 12. Persistence Capability View
|
||||
|
||||
Persistence 使用 load/store 语义:
|
||||
|
||||
```cpp
|
||||
device.persistence().load<&Device::temperature>(30.0);
|
||||
auto value = device.persistence().store<&Device::temperature>();
|
||||
```
|
||||
|
||||
编译期 key 版本:
|
||||
|
||||
```cpp
|
||||
device.persistence().load_key<"temperature">(30.0);
|
||||
auto value = device.persistence().store_key<"temperature">();
|
||||
```
|
||||
|
||||
能力由 `Persistence_Access` 元数据决定。
|
||||
|
||||
## 13. Synchronization Plan
|
||||
|
||||
### 13.1 Independent
|
||||
|
||||
默认 `Synchronization_Plan` 就是 independent:每个同步 property 拥有独立逻辑锁域。
|
||||
|
||||
显式写法:
|
||||
|
||||
```cpp
|
||||
synchronization(sync_all_independent)
|
||||
```
|
||||
|
||||
### 13.2 Shared
|
||||
|
||||
```cpp
|
||||
synchronization(sync_all_shared)
|
||||
```
|
||||
|
||||
除显式 override 外,全部 property 共用一个 lock slot。
|
||||
|
||||
### 13.3 Unsynchronized
|
||||
|
||||
```cpp
|
||||
synchronization(sync_all_unsynchronized)
|
||||
```
|
||||
|
||||
property 使用 `unsynchronized_slot`,Structive 不提供真实 mutex 保护。
|
||||
|
||||
### 13.4 单 Property Override
|
||||
|
||||
成员指针形式:
|
||||
|
||||
```cpp
|
||||
sync_independent<&Device::temperature>()
|
||||
sync_unsynchronized<&Device::immutable_id>()
|
||||
```
|
||||
|
||||
运行时字符串形式:
|
||||
|
||||
```cpp
|
||||
sync_independent("temperature")
|
||||
sync_unsynchronized("immutable_id")
|
||||
```
|
||||
|
||||
静态已知 property 优先成员指针形式。
|
||||
|
||||
### 13.5 Group
|
||||
|
||||
成员指针形式:
|
||||
|
||||
```cpp
|
||||
sync_group<&Device::min_speed, &Device::max_speed>("speed_range")
|
||||
```
|
||||
|
||||
字符串形式:
|
||||
|
||||
```cpp
|
||||
sync_group("speed_range", "min_speed", "max_speed")
|
||||
```
|
||||
|
||||
同一 group 的 property 共用 lock slot。
|
||||
|
||||
Resolver 会拒绝未知 key、重复 group name、空 group,以及同一 property 被重复显式配置。
|
||||
|
||||
## 14. 每实例覆盖同步策略
|
||||
|
||||
Type Descriptor 提供默认同步计划,但单个实例可以通过 `Property_Synchronization` 覆盖:
|
||||
|
||||
```cpp
|
||||
struct Device : Property_Object<Device> {
|
||||
Device() = default;
|
||||
explicit Device(Property_Synchronization synchronization) : Property_Object(std::move(synchronization)) {}
|
||||
};
|
||||
Device shared_device{
|
||||
property_synchronization(synchronization(sync_all_shared))
|
||||
};
|
||||
```
|
||||
|
||||
如果使用成员指针同步规则,可以显式提供对象类型完成 Schema materialization:
|
||||
|
||||
```cpp
|
||||
auto policy = property_synchronization<Device>(
|
||||
synchronization(
|
||||
sync_all_independent,
|
||||
sync_group<&Device::temperature, &Device::pressure>("environment")
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
Schema 本身不变,只改变该实例的 lock topology。
|
||||
|
||||
## 15. 查看解析后的同步拓扑
|
||||
|
||||
单字段:
|
||||
|
||||
```cpp
|
||||
auto slot = device.lock_slot<&Device::temperature>();
|
||||
```
|
||||
|
||||
完整 view:
|
||||
|
||||
```cpp
|
||||
auto resolved = device.resolved_synchronization();
|
||||
auto count = resolved.lock_count;
|
||||
auto slot = resolved.slot(0);
|
||||
auto uses_lock = resolved.uses_lock(0);
|
||||
```
|
||||
|
||||
`Resolved_Synchronization_View::unsynchronized_slot` 表示无同步 property。
|
||||
|
||||
## 16. 静态多属性 Guard
|
||||
|
||||
Shared guard:
|
||||
|
||||
```cpp
|
||||
auto guard = device.lock_shared<&Device::temperature, &Device::pressure>();
|
||||
auto temperature = guard.get<&Device::temperature>();
|
||||
auto pressure = guard.get<&Device::pressure>();
|
||||
```
|
||||
|
||||
Unique guard:
|
||||
|
||||
```cpp
|
||||
auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>();
|
||||
auto old_min = guard.get<&Device::min_speed>();
|
||||
guard.set<&Device::min_speed>(20.0);
|
||||
guard.set<&Device::max_speed>(120.0);
|
||||
```
|
||||
|
||||
Guard 只允许访问自己持有同步集合中的 property。
|
||||
|
||||
Capability view 也提供受其 access mode 限制的静态 guard:
|
||||
|
||||
```cpp
|
||||
auto guard = device.external().lock_shared<&Device::temperature>();
|
||||
```
|
||||
|
||||
## 17. 动态 Key Guard
|
||||
|
||||
运行时才知道属性集合时:
|
||||
|
||||
```cpp
|
||||
std::array<std::string_view, 2> keys{"temperature", "pressure"};
|
||||
auto guard = device.lock_shared(keys);
|
||||
```
|
||||
|
||||
或者:
|
||||
|
||||
```cpp
|
||||
auto guard = device.lock_unique({"min_speed", "max_speed"});
|
||||
```
|
||||
|
||||
动态 guard 会在运行时检查 key 和 capability。获取锁前会对 lock slot 排序、去重。
|
||||
|
||||
Guard 获取以后仍然可以通过编译期成员或 key 读写,但访问未包含在 held set 中的 property 会抛出 `std::logic_error`。
|
||||
|
||||
## 18. Traversal
|
||||
|
||||
Schema 遍历:
|
||||
|
||||
```cpp
|
||||
schema.for_each_property([](auto index, const auto& property) {
|
||||
// compile-time index + descriptor
|
||||
});
|
||||
```
|
||||
|
||||
Managed value 遍历:
|
||||
|
||||
```cpp
|
||||
device.for_each_readable([](auto index, const auto& descriptor, const auto& value) {
|
||||
// 每个 property 单独 managed read
|
||||
});
|
||||
```
|
||||
|
||||
Locked traversal 会先获取完整 readable 同步集合:
|
||||
|
||||
```cpp
|
||||
device.external().for_each_readable_locked([](auto index, const auto& descriptor, const auto& value) {
|
||||
// 全部选中 property 在 guard 下访问
|
||||
});
|
||||
```
|
||||
|
||||
集中 writable 操作:
|
||||
|
||||
```cpp
|
||||
device.with_all_writable_locked([](auto& guard) {
|
||||
// guard.get / guard.set
|
||||
});
|
||||
```
|
||||
|
||||
Persistence view 对应提供 `with_all_loadable_locked(...)`。
|
||||
|
||||
## 19. Computed Property
|
||||
|
||||
同步 computed property 接收 read view:
|
||||
|
||||
```cpp
|
||||
computed_property<Device, double>([](const auto& view) {
|
||||
return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>();
|
||||
}, key<"speed_span">, external_access<External_Access::read>)
|
||||
```
|
||||
|
||||
Computed property 自身与通过 view 读取的每个 dependency 必须解析到同一个 lock slot。
|
||||
|
||||
例如:
|
||||
|
||||
```cpp
|
||||
synchronization(
|
||||
sync_all_independent,
|
||||
sync_group("speed", "min_speed", "max_speed", "speed_span")
|
||||
)
|
||||
```
|
||||
|
||||
Computed property 是只读属性。
|
||||
|
||||
## 20. Trusted Accessor Property
|
||||
|
||||
Core 还支持基于成员函数的 trusted access:
|
||||
|
||||
```cpp
|
||||
trusted_computed_property<&Device::get_temperature>(key<"temperature">)
|
||||
```
|
||||
|
||||
以及 getter/setter:
|
||||
|
||||
```cpp
|
||||
trusted_accessor_property<&Device::get_temperature, &Device::set_temperature>(key<"temperature">)
|
||||
```
|
||||
|
||||
它们直接调用对象成员函数,并标记为 trusted object access,不受 synchronized computed view 的 dependency slot 检查。只有当成员函数本身拥有明确同步/一致性契约时才应该使用。
|
||||
|
||||
## 21. Runtime Type-Erased Access
|
||||
|
||||
任何 `Property_Object<T>` 同时也是 `Property_Object_Base`:
|
||||
|
||||
```cpp
|
||||
Property_Object_Base& erased = device;
|
||||
```
|
||||
|
||||
Runtime introspection:
|
||||
|
||||
```cpp
|
||||
erased.runtime_object_type();
|
||||
erased.runtime_property_count();
|
||||
```
|
||||
|
||||
Runtime write:
|
||||
|
||||
```cpp
|
||||
double value = 35.0;
|
||||
auto result = erased.runtime_write(
|
||||
Managed_Access_Mode::external,
|
||||
"temperature",
|
||||
typeid(double),
|
||||
&value
|
||||
);
|
||||
```
|
||||
|
||||
Runtime read 通过 callback 返回值:
|
||||
|
||||
```cpp
|
||||
static void read_double(void* context, std::size_t, std::string_view, const std::type_info& type, const void* value) {
|
||||
if (type == typeid(double)) {
|
||||
*static_cast<double*>(context) = *static_cast<const double*>(value);
|
||||
}
|
||||
}
|
||||
double output = 0.0;
|
||||
auto result = erased.runtime_read(
|
||||
Managed_Access_Mode::external,
|
||||
"temperature",
|
||||
&output,
|
||||
&read_double
|
||||
);
|
||||
```
|
||||
|
||||
结果枚举:
|
||||
|
||||
```cpp
|
||||
Runtime_Access_Result::ok
|
||||
Runtime_Access_Result::unknown_property
|
||||
Runtime_Access_Result::not_readable
|
||||
Runtime_Access_Result::not_writable
|
||||
Runtime_Access_Result::type_mismatch
|
||||
```
|
||||
|
||||
Runtime path 与 typed managed access 使用同一套 capability 和 synchronization 规则。
|
||||
|
||||
## 22. Lock Policy
|
||||
|
||||
默认:
|
||||
|
||||
```cpp
|
||||
struct Device : Property_Object<Device, Shared_Mutex_Policy> {
|
||||
};
|
||||
```
|
||||
|
||||
等价简写:
|
||||
|
||||
```cpp
|
||||
struct Device : Property_Object<Device> {
|
||||
};
|
||||
```
|
||||
|
||||
也提供 no-op lock policy:
|
||||
|
||||
```cpp
|
||||
struct Device : Property_Object<Device, No_Lock_Policy> {
|
||||
};
|
||||
```
|
||||
|
||||
`No_Lock_Policy` 使用 `Null_Shared_Mutex`,它取消真实互斥;只有外部所有权规则能够保证正确性时才应该使用。
|
||||
|
||||
## 23. Raw Object Access
|
||||
|
||||
`unsafe_object()` 可以拿到底层 derived object:
|
||||
|
||||
```cpp
|
||||
Device& raw = device.unsafe_object();
|
||||
```
|
||||
|
||||
普通成员访问当然也仍然存在:
|
||||
|
||||
```cpp
|
||||
device.temperature = 40.0;
|
||||
```
|
||||
|
||||
这些入口故意绕过 Structive managed access。适合调用方已经持有正确同步、或者明确在 Structive 管理契约之外操作的情况。
|
||||
|
||||
## 24. 推荐使用规则
|
||||
|
||||
- Typed 业务访问优先 member pointer。
|
||||
- Runtime integration boundary 使用 string key。
|
||||
- Key 按协议标识对待。
|
||||
- 稳定的对象级 capability 放 `defaults(...)`,特殊字段单独 override。
|
||||
- 需要一致观察的字段放同一个 synchronization group。
|
||||
- Validation 在业务操作边界显式执行。
|
||||
- 跨字段 invariant 使用静态 multi-property guard。
|
||||
- 默认优先 synchronized computed property。
|
||||
- Runtime access 用于 adapter,不要把普通 typed code 动态化。
|
||||
- Raw access 只在调用方明确拥有被绕过的管理保证时使用。
|
||||
+424
@@ -0,0 +1,424 @@
|
||||
# Structive Design Philosophy and Principles
|
||||
|
||||
[中文](DESIGN.zh-CN.md)
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
Structive exists to give ordinary C++ object types a machine-readable structural layer without forcing those types into a framework-specific storage model.
|
||||
|
||||
The library should make it possible to ask questions such as:
|
||||
|
||||
- Which members are part of the public structural model?
|
||||
- What is the stable runtime key of a property?
|
||||
- Which properties are externally readable or writable?
|
||||
- Which properties participate in persistence?
|
||||
- Which metadata belongs to presentation, validation or another extension?
|
||||
- Which properties share a synchronization domain?
|
||||
- How can generic runtime code read or write a property safely with respect to declared capabilities?
|
||||
|
||||
It should answer those questions while leaving the underlying type recognizably C++.
|
||||
|
||||
## 2. Primary principle: enhance, do not replace
|
||||
|
||||
Structive is not a replacement object model. It is a structural layer attached to an existing C++ type.
|
||||
|
||||
The preferred shape is:
|
||||
|
||||
```cpp
|
||||
struct Device : structive::Property_Object<Device> {
|
||||
double temperature;
|
||||
std::string name;
|
||||
};
|
||||
```
|
||||
|
||||
not:
|
||||
|
||||
```cpp
|
||||
struct Device {
|
||||
Framework_Property<double> temperature;
|
||||
Framework_Property<std::string> name;
|
||||
};
|
||||
```
|
||||
|
||||
This principle protects several properties of normal C++ code:
|
||||
|
||||
1. Members remain real members.
|
||||
2. Member pointers remain meaningful identities.
|
||||
3. Raw access remains possible where ownership rules permit it.
|
||||
4. Unregistered members can remain implementation details.
|
||||
5. Generic Structive metadata can evolve independently from storage representation.
|
||||
|
||||
The cost of this principle is intentional: Structive cannot intercept raw member access. Managed behavior only applies when callers use the managed path.
|
||||
|
||||
## 3. Two layers, not one
|
||||
|
||||
Structive separates type-level description from instance-level management.
|
||||
|
||||
### 3.1 Type layer
|
||||
|
||||
`Type_Descriptor<T>` produces an `Object_Schema` describing:
|
||||
|
||||
- registered properties;
|
||||
- declared property keys;
|
||||
- accessors;
|
||||
- Attributes;
|
||||
- constraints;
|
||||
- object-level inheritable defaults;
|
||||
- a default synchronization plan.
|
||||
|
||||
This is the structural definition of the type.
|
||||
|
||||
### 3.2 Instance layer
|
||||
|
||||
`Property_Object<T>` provides:
|
||||
|
||||
- resolved lock-slot topology;
|
||||
- per-instance mutex storage;
|
||||
- managed typed reads and writes;
|
||||
- capability views;
|
||||
- static and runtime multi-property guards;
|
||||
- traversal over managed values;
|
||||
- type-erased runtime access through `Property_Object_Base`.
|
||||
|
||||
This is instance behavior, not schema identity.
|
||||
|
||||
### 3.3 Design rule
|
||||
|
||||
Do not move instance state into the schema, and do not make schema metadata depend on one particular instance-management policy.
|
||||
|
||||
A future user may want to describe a large number of plain objects without paying per-object synchronization cost. The architecture should continue to leave that possibility open.
|
||||
|
||||
## 4. Registration is explicit
|
||||
|
||||
Structive does not assume every C++ member belongs to the structural model.
|
||||
|
||||
```cpp
|
||||
struct Device : structive::Property_Object<Device> {
|
||||
int id;
|
||||
double temperature;
|
||||
mutable int internal_cache;
|
||||
};
|
||||
```
|
||||
|
||||
If only `id` and `temperature` are registered, `internal_cache` is invisible to Structive.
|
||||
|
||||
This is deliberate. Structural exposure is an API decision and should not be inferred from physical layout.
|
||||
|
||||
### Rule
|
||||
|
||||
**The schema is the public structural contract; the struct layout is not automatically the schema.**
|
||||
|
||||
## 5. Prefer compile-time identity inside C++
|
||||
|
||||
Business C++ code should normally identify member-backed properties by member pointer:
|
||||
|
||||
```cpp
|
||||
device.read<&Device::temperature>();
|
||||
device.write<&Device::temperature>(30.0);
|
||||
device.schema().property<&Device::temperature>();
|
||||
```
|
||||
|
||||
This gives the compiler the strongest relationship between the object type and the selected field.
|
||||
|
||||
Numeric indexes are useful for generic compile-time traversal. String keys are useful at runtime boundaries.
|
||||
|
||||
### Identity hierarchy
|
||||
|
||||
```text
|
||||
business C++ code → member pointer
|
||||
compile-time generic code → property index
|
||||
runtime/adapters → declared string key
|
||||
```
|
||||
|
||||
Do not unnecessarily convert compile-time code to strings merely because a key exists.
|
||||
|
||||
## 6. Keys are structural protocol identifiers
|
||||
|
||||
Every `Property_Descriptor` requires a non-empty `key` Attribute. Keys must be unique inside one schema.
|
||||
|
||||
A key is not merely a UI label. It is the runtime identity used by schema lookup, dynamic lock selection and runtime access.
|
||||
|
||||
Changing a key may therefore change an external protocol or persistence contract even when the C++ member name stays unchanged.
|
||||
|
||||
### Rule
|
||||
|
||||
**Treat property keys as protocol-level names. Rename them deliberately.**
|
||||
|
||||
## 7. One Attribute system
|
||||
|
||||
Structive has one Attribute protocol. It should not grow parallel metadata channels such as “hint”, “annotation”, “UI metadata” and “serializer metadata” with separate storage machinery.
|
||||
|
||||
An Attribute may declare:
|
||||
|
||||
- `attribute_category` for category-based lookup;
|
||||
- `single_valued` when only one Attribute of that category may appear in a declaration;
|
||||
- `inheritable` when it may participate in object defaults;
|
||||
- any payload required by the category owner.
|
||||
|
||||
Core and extensions use this same mechanism.
|
||||
|
||||
### Rule
|
||||
|
||||
**Add semantics by adding an Attribute category and an interpreter, not by adding a second metadata framework.**
|
||||
|
||||
## 8. Category ownership
|
||||
|
||||
Core should interpret only categories that belong to Core.
|
||||
|
||||
For example, Core understands access and persistence capability because those affect Core-managed views. It does not need to understand `presentation::label`.
|
||||
|
||||
Presentation owns presentation semantics. A future JSON extension should own JSON-specific semantics. An RPC extension should own RPC-specific semantics.
|
||||
|
||||
The Core still stores and traverses all Attributes uniformly.
|
||||
|
||||
### Dependency rule
|
||||
|
||||
```text
|
||||
extension implementation
|
||||
↓
|
||||
Structive Property Core
|
||||
```
|
||||
|
||||
Never reverse this dependency merely to make an extension convenient.
|
||||
|
||||
## 9. Defaults are inheritance, not hidden mutation
|
||||
|
||||
`defaults(...)` supports inheritable Attribute categories. Property declarations remain able to override those defaults.
|
||||
|
||||
Current Core inheritable categories include external access, persistence access and sensitivity.
|
||||
|
||||
This mechanism should be used for stable object-wide policy defaults, not as a general mechanism for implicit behavior.
|
||||
|
||||
### Rule
|
||||
|
||||
**Defaults should reduce repetition without making a property’s effective policy impossible to determine from schema rules.**
|
||||
|
||||
## 10. Managed access and raw access are distinct contracts
|
||||
|
||||
Raw access:
|
||||
|
||||
```cpp
|
||||
device.temperature = 30.0;
|
||||
```
|
||||
|
||||
Managed access:
|
||||
|
||||
```cpp
|
||||
device.write<&Device::temperature>(30.0);
|
||||
```
|
||||
|
||||
The raw path is normal C++. It does not acquire Structive locks or enforce Structive capability views.
|
||||
|
||||
The managed path uses the descriptor and synchronization topology.
|
||||
|
||||
This duality is intentional rather than accidental.
|
||||
|
||||
### Rule
|
||||
|
||||
Do not pretend that inheriting `Property_Object<T>` turns public C++ members into encapsulated properties. If a subsystem requires managed synchronization, its coding rules must require the managed path.
|
||||
|
||||
## 11. Capabilities are views, not copies of the object model
|
||||
|
||||
The same property may have different visibility under different managed modes:
|
||||
|
||||
```text
|
||||
internal
|
||||
external
|
||||
persistence
|
||||
```
|
||||
|
||||
Core derives those capabilities from Attributes and the accessor’s actual read/write abilities.
|
||||
|
||||
An external view should not become a second schema. A persistence view should not become a second schema. They are projections over one schema.
|
||||
|
||||
### Rule
|
||||
|
||||
**One property definition, multiple capability projections.**
|
||||
|
||||
## 12. Validation is metadata plus an explicit operation
|
||||
|
||||
Structive constraints describe candidate validity, but `write()` does not automatically run them.
|
||||
|
||||
This separation is essential because these are different concerns:
|
||||
|
||||
```text
|
||||
single-property constraint
|
||||
cross-property invariant
|
||||
locking
|
||||
transaction boundary
|
||||
rollback strategy
|
||||
error reporting
|
||||
side effects
|
||||
```
|
||||
|
||||
A generic `write()` cannot correctly guess all of them.
|
||||
|
||||
### Example
|
||||
|
||||
```cpp
|
||||
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);
|
||||
}
|
||||
```
|
||||
|
||||
The business operation owns the invariant and rollback semantics.
|
||||
|
||||
### Rule
|
||||
|
||||
**Do not turn `write()` into a hidden transaction engine.**
|
||||
|
||||
## 13. Synchronization is synchronization only
|
||||
|
||||
A synchronization plan maps properties to lock slots. It supports independent, shared, unsynchronized and grouped configurations.
|
||||
|
||||
The purpose of a group is to define a consistency domain: properties in the group share a mutex slot.
|
||||
|
||||
Multi-property lock operations deduplicate slots and acquire them in stable slot order.
|
||||
|
||||
### Synchronization does not mean
|
||||
|
||||
- validation;
|
||||
- transaction;
|
||||
- rollback;
|
||||
- event emission;
|
||||
- dirty tracking;
|
||||
- persistence commit.
|
||||
|
||||
Those may be built above Structive, but should not be silently coupled to locking.
|
||||
|
||||
## 14. Computed properties must have explicit consistency boundaries
|
||||
|
||||
`computed_property` reads through a synchronized view. The view only permits access to properties in the same resolved lock slot.
|
||||
|
||||
Therefore a computed property that depends on `min_speed` and `max_speed` should share their synchronization group:
|
||||
|
||||
```cpp
|
||||
synchronization(
|
||||
sync_all_independent,
|
||||
sync_group("speed", "min_speed", "max_speed", "speed_span")
|
||||
)
|
||||
```
|
||||
|
||||
This makes consistency explicit rather than relying on a getter that casually reads unrelated fields.
|
||||
|
||||
### Trusted accessors
|
||||
|
||||
`trusted_computed_property` and `trusted_accessor_property` invoke trusted member functions on the object. They bypass synchronized-view dependency checking by design.
|
||||
|
||||
### Rule
|
||||
|
||||
**Use synchronized computed properties by default. Use trusted accessors only when the accessor itself owns or guarantees the required synchronization semantics.**
|
||||
|
||||
## 15. Runtime access is a boundary feature
|
||||
|
||||
`Property_Object_Base` intentionally provides a type-erased runtime interface using:
|
||||
|
||||
- string key;
|
||||
- `Managed_Access_Mode`;
|
||||
- `std::type_info`;
|
||||
- explicit result codes.
|
||||
|
||||
This is appropriate for adapters that do not know the concrete type at compile time.
|
||||
|
||||
It is not a reason to make normal typed C++ code dynamic.
|
||||
|
||||
### Rule
|
||||
|
||||
**Keep compile-time code compile-time; cross dynamic boundaries only where the application actually has a dynamic boundary.**
|
||||
|
||||
## 16. Runtime errors and compile-time errors have different jobs
|
||||
|
||||
Structive uses compile-time rejection where the selection is statically known:
|
||||
|
||||
- missing member registration;
|
||||
- duplicate keys;
|
||||
- duplicate member storage identity;
|
||||
- invalid Attribute category duplication;
|
||||
- non-inheritable Attributes inside `defaults(...)`;
|
||||
- impossible access capability for an accessor.
|
||||
|
||||
Runtime failures are reserved for runtime inputs and runtime configuration:
|
||||
|
||||
- unknown dynamic key;
|
||||
- inaccessible dynamic property;
|
||||
- invalid synchronization plan key;
|
||||
- duplicate runtime synchronization configuration;
|
||||
- runtime type mismatch.
|
||||
|
||||
### Rule
|
||||
|
||||
**Do not defer a statically knowable schema error to runtime. Do not force dynamic adapter input into compile-time machinery.**
|
||||
|
||||
## 17. Synchronization policy should remain replaceable
|
||||
|
||||
`Property_Object<Derived, Lock_Policy>` supports a lock policy, with `Shared_Mutex_Policy` as the default and `No_Lock_Policy` available.
|
||||
|
||||
This is an important architectural seam. Core semantics should not become inseparable from one specific mutex type or one global scheduler.
|
||||
|
||||
`No_Lock_Policy` removes actual mutual exclusion; it does not magically make concurrent access safe.
|
||||
|
||||
## 18. Extension design rules
|
||||
|
||||
A good Structive extension should:
|
||||
|
||||
1. Define categories that belong to its own domain.
|
||||
2. Reuse the Core Attribute protocol.
|
||||
3. Interpret only the categories it owns or explicitly depends on.
|
||||
4. Depend on Core, never require Core to depend on it.
|
||||
5. Keep domain-specific fallback behavior inside the extension.
|
||||
6. Prefer reading the existing schema over duplicating a parallel descriptor tree.
|
||||
7. Avoid changing Core behavior merely because an adapter needs a convenience rule.
|
||||
|
||||
## 19. Non-goals
|
||||
|
||||
Structive Core should not become all of the following at once:
|
||||
|
||||
- an ORM;
|
||||
- a JSON library;
|
||||
- a GUI framework;
|
||||
- an RPC framework;
|
||||
- a signal/slot system;
|
||||
- a transaction engine;
|
||||
- a general-purpose reflection replacement for every language feature.
|
||||
|
||||
Structive should provide a strong structural contract that those systems can consume.
|
||||
|
||||
## 20. Evolution rules
|
||||
|
||||
When changing the library, apply these questions in order:
|
||||
|
||||
1. Does this belong to the structural model, instance management, or an extension?
|
||||
2. Is this new concept really an Attribute category rather than a new metadata channel?
|
||||
3. Can the error be detected at compile time?
|
||||
4. Does the change preserve native C++ member semantics?
|
||||
5. Does it preserve the distinction between raw and managed access?
|
||||
6. Is synchronization being mixed with validation, transaction or events?
|
||||
7. Does Core now know something that should belong to an extension?
|
||||
8. Does the API still make member pointers the natural typed identity?
|
||||
9. Does a new convenience API duplicate an existing semantic path?
|
||||
10. Does the change preserve existing function-signature meaning rather than silently redefining it?
|
||||
|
||||
If a feature fails these questions, reconsider its layer before implementing it.
|
||||
|
||||
## 21. Architectural summary
|
||||
|
||||
Structive should remain a small number of strong concepts rather than a large number of magical conveniences:
|
||||
|
||||
```text
|
||||
ordinary C++ type
|
||||
+ explicit schema
|
||||
+ one Attribute model
|
||||
+ explicit capabilities
|
||||
+ explicit validation
|
||||
+ explicit synchronization
|
||||
+ optional managed instance behavior
|
||||
+ extension-owned interpretation
|
||||
```
|
||||
|
||||
The library is strongest when generic systems can understand a type without taking ownership of that type.
|
||||
@@ -0,0 +1,424 @@
|
||||
# Structive 设计理念与原则
|
||||
|
||||
[English](DESIGN.md)
|
||||
|
||||
## 1. 设计目标
|
||||
|
||||
Structive 的目标是给普通 C++ 对象增加一层机器可理解的结构语义,同时不强迫业务对象改用框架自己的存储模型。
|
||||
|
||||
它应该能够回答:
|
||||
|
||||
- 哪些成员属于公开结构模型?
|
||||
- 某个属性稳定的 runtime key 是什么?
|
||||
- 哪些属性允许外部读取或写入?
|
||||
- 哪些属性参与持久化?
|
||||
- 哪些元数据属于 Presentation、Validation 或其他扩展?
|
||||
- 哪些属性位于同一个同步域?
|
||||
- 动态适配器如何在遵守 capability 的前提下读写属性?
|
||||
|
||||
但完成这些事情以后,底层类型仍然应该看起来像正常的 C++。
|
||||
|
||||
## 2. 第一原则:增强,而不是替代
|
||||
|
||||
Structive 不是第二套对象模型,而是附加在已有 C++ 类型旁边的结构层。
|
||||
|
||||
推荐形态:
|
||||
|
||||
```cpp
|
||||
struct Device : structive::Property_Object<Device> {
|
||||
double temperature;
|
||||
std::string name;
|
||||
};
|
||||
```
|
||||
|
||||
而不是:
|
||||
|
||||
```cpp
|
||||
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 存储;
|
||||
- managed typed read/write;
|
||||
- capability view;
|
||||
- 静态与运行时多属性 guard;
|
||||
- managed value traversal;
|
||||
- `Property_Object_Base` type-erased runtime access。
|
||||
|
||||
这是实例行为,不是 Schema 身份。
|
||||
|
||||
### 3.3 设计约束
|
||||
|
||||
不要把实例状态塞进 Schema,也不要让 Schema 必须依赖某一种特定的实例管理策略。
|
||||
|
||||
以后完全可能有用户只想描述大量普通对象,却不愿意为每个对象承担同步状态成本。当前架构应该持续保留这种可能性。
|
||||
|
||||
## 4. 注册必须显式
|
||||
|
||||
Structive 不认为所有 C++ 成员都天然属于结构模型。
|
||||
|
||||
```cpp
|
||||
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:
|
||||
|
||||
```cpp
|
||||
device.read<&Device::temperature>();
|
||||
device.write<&Device::temperature>(30.0);
|
||||
device.schema().property<&Device::temperature>();
|
||||
```
|
||||
|
||||
这样编译器能够建立最强的“对象类型—字段”关系。
|
||||
|
||||
数字 index 适合编译期泛型遍历;字符串 key 适合运行时边界。
|
||||
|
||||
### 身份层级
|
||||
|
||||
```text
|
||||
业务 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 只负责统一保存、遍历和提供查询机制。
|
||||
|
||||
### 依赖约束
|
||||
|
||||
```text
|
||||
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:
|
||||
|
||||
```cpp
|
||||
device.temperature = 30.0;
|
||||
```
|
||||
|
||||
Managed access:
|
||||
|
||||
```cpp
|
||||
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 下可以拥有不同可见性:
|
||||
|
||||
```text
|
||||
internal
|
||||
external
|
||||
persistence
|
||||
```
|
||||
|
||||
Core 根据 Attribute 和 accessor 实际读写能力计算这些 capability。
|
||||
|
||||
External view 不应该发展成第二份 Schema;Persistence view 也不应该成为第二份 Schema。它们都只是同一 Schema 的能力投影。
|
||||
|
||||
### 原则
|
||||
|
||||
**一份 property definition,多种 capability projection。**
|
||||
|
||||
## 12. Validation = 元数据 + 显式操作
|
||||
|
||||
Constraint 描述候选值是否合法,但 `write()` 不会自动执行它。
|
||||
|
||||
必须保持分开的概念包括:
|
||||
|
||||
```text
|
||||
单字段 constraint
|
||||
跨字段 invariant
|
||||
locking
|
||||
transaction boundary
|
||||
rollback strategy
|
||||
error reporting
|
||||
side effects
|
||||
```
|
||||
|
||||
一个通用 `write()` 不可能替所有业务正确猜出这些规则。
|
||||
|
||||
### 示例
|
||||
|
||||
```cpp
|
||||
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` 应该和它们进入同一个同步组:
|
||||
|
||||
```cpp
|
||||
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 应该:
|
||||
|
||||
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 最强的形态应该是少量强概念,而不是大量魔法便利接口:
|
||||
|
||||
```text
|
||||
普通 C++ 类型
|
||||
+ 显式 Schema
|
||||
+ 一套 Attribute 模型
|
||||
+ 显式 Capability
|
||||
+ 显式 Validation
|
||||
+ 显式 Synchronization
|
||||
+ 可选 Managed Instance 行为
|
||||
+ Extension 自己解释自己的语义
|
||||
```
|
||||
|
||||
当泛型系统能够理解业务类型、却不需要接管业务类型时,Structive 的价值最大。
|
||||
@@ -0,0 +1,267 @@
|
||||
# Structive Extension Architecture
|
||||
|
||||
[中文](EXTENSIONS.zh-CN.md)
|
||||
|
||||
## 1. Extension role
|
||||
|
||||
Structive extensions add domain-specific interpretation without changing the Core structural model.
|
||||
|
||||
The intended dependency is:
|
||||
|
||||
```text
|
||||
application / adapter
|
||||
↓
|
||||
Structive extension
|
||||
↓
|
||||
Structive Property Core
|
||||
```
|
||||
|
||||
Core does not include or link extension code.
|
||||
|
||||
## 2. Extension principle
|
||||
|
||||
An extension should normally add two things:
|
||||
|
||||
1. one or more Attribute categories that express domain metadata;
|
||||
2. interpretation code that consumes those Attributes.
|
||||
|
||||
It should not create a parallel property registry when the existing `Object_Schema` already contains the required structure.
|
||||
|
||||
## 3. Current presentation extension
|
||||
|
||||
The current extension module defines four Attribute categories:
|
||||
|
||||
```cpp
|
||||
presentation::Label_Category
|
||||
presentation::Description_Category
|
||||
presentation::Group_Category
|
||||
presentation::Order_Category
|
||||
```
|
||||
|
||||
Convenience Attribute values:
|
||||
|
||||
```cpp
|
||||
presentation::label<"Temperature">
|
||||
presentation::description<"Current device temperature">
|
||||
presentation::group<"Environment">
|
||||
presentation::order<10>
|
||||
```
|
||||
|
||||
They can be attached directly to a normal Core property:
|
||||
|
||||
```cpp
|
||||
field<&Device::temperature>(
|
||||
key<"temperature">,
|
||||
unit<"C">,
|
||||
presentation::label<"Temperature">,
|
||||
presentation::description<"Current device temperature">,
|
||||
presentation::group<"Environment">,
|
||||
presentation::order<10>
|
||||
)
|
||||
```
|
||||
|
||||
No wrapper such as `hint(...)` is required.
|
||||
|
||||
## 4. Presentation interpretation
|
||||
|
||||
Include:
|
||||
|
||||
```cpp
|
||||
#include <structive/property/extensions/presentation.hpp>
|
||||
```
|
||||
|
||||
Link:
|
||||
|
||||
```cmake
|
||||
target_link_libraries(my_target PRIVATE structive::property_extensions)
|
||||
```
|
||||
|
||||
Describe a property by member pointer:
|
||||
|
||||
```cpp
|
||||
auto info = presentation::describe<&Device::temperature>(device.schema());
|
||||
```
|
||||
|
||||
The result contains:
|
||||
|
||||
```cpp
|
||||
struct Presentation_Info {
|
||||
std::string_view key;
|
||||
std::string_view label;
|
||||
std::string_view description;
|
||||
std::string_view group;
|
||||
std::size_t order;
|
||||
bool has_order;
|
||||
};
|
||||
```
|
||||
|
||||
If no presentation label is declared, the extension uses the property key as the display label. This fallback is presentation policy and intentionally lives outside Core.
|
||||
|
||||
## 5. Inheritable extension Attributes
|
||||
|
||||
The presentation `group` Attribute is inheritable, so it may be placed in `defaults(...)`:
|
||||
|
||||
```cpp
|
||||
return object<Device>(
|
||||
defaults(
|
||||
external_access<External_Access::read>,
|
||||
presentation::group<"Environment">
|
||||
),
|
||||
field<&Device::temperature>(
|
||||
key<"temperature">,
|
||||
presentation::label<"Temperature">
|
||||
),
|
||||
field<&Device::pressure>(
|
||||
key<"pressure">,
|
||||
presentation::label<"Pressure">
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
The extension resolves an effective declared value through the same Core default mechanism.
|
||||
|
||||
`label`, `description` and `order` are not inheritable and therefore cannot be placed in `defaults(...)`.
|
||||
|
||||
## 6. Designing a new extension Attribute
|
||||
|
||||
Example:
|
||||
|
||||
```cpp
|
||||
namespace my_adapter {
|
||||
struct Json_Name_Category {};
|
||||
template <Fixed_String Value>
|
||||
struct Json_Name_Attribute {
|
||||
using attribute_category = Json_Name_Category;
|
||||
static constexpr bool single_valued = true;
|
||||
static constexpr bool inheritable = false;
|
||||
static constexpr auto value = Value;
|
||||
};
|
||||
template <Fixed_String Value>
|
||||
inline constexpr Json_Name_Attribute<Value> json_name{};
|
||||
}
|
||||
```
|
||||
|
||||
Use it in a schema:
|
||||
|
||||
```cpp
|
||||
field<&Device::temperature>(
|
||||
key<"temperature">,
|
||||
my_adapter::json_name<"temp">
|
||||
)
|
||||
```
|
||||
|
||||
The Core stores it without needing to know what JSON means.
|
||||
|
||||
## 7. Reading extension Attributes
|
||||
|
||||
For a property descriptor:
|
||||
|
||||
```cpp
|
||||
using Property = std::remove_cvref_t<decltype(property)>;
|
||||
if constexpr (Property::has_attribute<my_adapter::Json_Name_Category>) {
|
||||
const auto& value = property.attribute<my_adapter::Json_Name_Category>();
|
||||
}
|
||||
```
|
||||
|
||||
For inheritable categories, use effective declared lookup:
|
||||
|
||||
```cpp
|
||||
if constexpr (has_declared_effective_attribute_v<Schema, Index, My_Category>) {
|
||||
const auto& value = declared_effective_attribute<Index, My_Category>(schema);
|
||||
}
|
||||
```
|
||||
|
||||
This checks the property declaration first and then object defaults.
|
||||
|
||||
## 8. Multi-valued metadata
|
||||
|
||||
Core uniqueness is category-driven. An Attribute with `single_valued = true` and a non-void `attribute_category` may only appear once in one declaration.
|
||||
|
||||
When a domain needs repeated annotations, design that metadata so it does not claim single-valued category uniqueness, then consume it through `for_each_attribute(...)`.
|
||||
|
||||
Do not force naturally repeated metadata into one large unrelated object merely to satisfy a single-valued design.
|
||||
|
||||
## 9. Recommended extension boundaries
|
||||
|
||||
A future extension may reasonably own metadata and interpretation for areas such as:
|
||||
|
||||
- serialization naming and omission rules;
|
||||
- RPC exposure rules;
|
||||
- UI labels, groups and editor hints;
|
||||
- database column mapping;
|
||||
- configuration-file mapping;
|
||||
- domain documentation generation.
|
||||
|
||||
These are examples of extension domains, not currently implemented features.
|
||||
|
||||
The important boundary is that Core should not gain a direct dependency on their libraries or domain types.
|
||||
|
||||
## 10. Core metadata may still be consumed
|
||||
|
||||
An extension is allowed to interpret Core-owned categories when those categories are part of the extension's input contract.
|
||||
|
||||
For example, an external adapter may use:
|
||||
|
||||
- property `key` as the default protocol name;
|
||||
- `External_Access` to determine exposure;
|
||||
- `sensitive` to decide whether its own logs should omit a value.
|
||||
|
||||
The extension may choose a policy based on those attributes, but it should not change what Core itself means by them.
|
||||
|
||||
## 11. Fallback behavior belongs to the consumer
|
||||
|
||||
The presentation extension demonstrates the intended rule:
|
||||
|
||||
```text
|
||||
no label Attribute
|
||||
↓
|
||||
presentation extension chooses property key as label
|
||||
```
|
||||
|
||||
Core does not invent that fallback because a different consumer may want a different behavior.
|
||||
|
||||
The same principle should apply to future adapters. Defaults that exist only to make one domain pleasant should remain in that domain.
|
||||
|
||||
## 12. Avoid extension-to-Core semantic leakage
|
||||
|
||||
Bad direction:
|
||||
|
||||
```text
|
||||
JSON adapter needs alias support
|
||||
↓
|
||||
Core adds JSON_Alias_Category and JSON naming logic
|
||||
```
|
||||
|
||||
Preferred direction:
|
||||
|
||||
```text
|
||||
JSON extension defines Json_Name_Category
|
||||
↓
|
||||
JSON extension interprets it
|
||||
↓
|
||||
Core remains domain-neutral
|
||||
```
|
||||
|
||||
## 13. Linked code versus header-only metadata
|
||||
|
||||
Attribute definitions can often be header-only. Interpretation may be header-only or linked depending on implementation needs.
|
||||
|
||||
The current presentation extension demonstrates a linked extension target. `presentation::describe()` performs compile-time Attribute selection and calls linked `make_presentation_info()` for the concrete result construction/fallback step.
|
||||
|
||||
Do not move linked implementation into Core merely because the extension is small.
|
||||
|
||||
## 14. Extension review checklist
|
||||
|
||||
Before accepting a new extension feature, ask:
|
||||
|
||||
1. Does this concept belong to Core or to one consumer domain?
|
||||
2. Can it be represented through the existing Attribute protocol?
|
||||
3. Does the extension own the Attribute category it interprets?
|
||||
4. Is an inheritable Attribute truly an object-wide default policy?
|
||||
5. Is fallback behavior kept inside the extension?
|
||||
6. Is the existing schema reused rather than duplicated?
|
||||
7. Does the dependency still point from extension to Core?
|
||||
8. Has Core remained free of third-party domain types?
|
||||
9. Does the extension preserve the existing meaning of Core access, validation and synchronization?
|
||||
|
||||
If the answer to the dependency or semantic-boundary questions is no, the layering should be redesigned before code is added.
|
||||
@@ -0,0 +1,267 @@
|
||||
# Structive Extension 架构指南
|
||||
|
||||
[English](EXTENSIONS.md)
|
||||
|
||||
## 1. Extension 的职责
|
||||
|
||||
Structive Extension 的作用是在不修改 Core 结构模型的前提下增加领域语义解释。
|
||||
|
||||
推荐依赖方向:
|
||||
|
||||
```text
|
||||
application / adapter
|
||||
↓
|
||||
Structive extension
|
||||
↓
|
||||
Structive Property Core
|
||||
```
|
||||
|
||||
Core 不 include、不 link Extension。
|
||||
|
||||
## 2. Extension 的基本模式
|
||||
|
||||
一个 Extension 通常只需要增加两类东西:
|
||||
|
||||
1. 一个或多个属于自己领域的 Attribute category;
|
||||
2. 解释这些 Attribute 的实现代码。
|
||||
|
||||
如果已有 `Object_Schema` 已经包含需要的结构信息,就不应该再复制第二份 property registry。
|
||||
|
||||
## 3. 当前 Presentation Extension
|
||||
|
||||
当前扩展模块定义四个 Attribute category:
|
||||
|
||||
```cpp
|
||||
presentation::Label_Category
|
||||
presentation::Description_Category
|
||||
presentation::Group_Category
|
||||
presentation::Order_Category
|
||||
```
|
||||
|
||||
对应便利 Attribute:
|
||||
|
||||
```cpp
|
||||
presentation::label<"Temperature">
|
||||
presentation::description<"Current device temperature">
|
||||
presentation::group<"Environment">
|
||||
presentation::order<10>
|
||||
```
|
||||
|
||||
它们直接挂到普通 Core property 上:
|
||||
|
||||
```cpp
|
||||
field<&Device::temperature>(
|
||||
key<"temperature">,
|
||||
unit<"C">,
|
||||
presentation::label<"Temperature">,
|
||||
presentation::description<"Current device temperature">,
|
||||
presentation::group<"Environment">,
|
||||
presentation::order<10>
|
||||
)
|
||||
```
|
||||
|
||||
不需要 `hint(...)` 一类额外包装。
|
||||
|
||||
## 4. Presentation 解释
|
||||
|
||||
Include:
|
||||
|
||||
```cpp
|
||||
#include <structive/property/extensions/presentation.hpp>
|
||||
```
|
||||
|
||||
Link:
|
||||
|
||||
```cmake
|
||||
target_link_libraries(my_target PRIVATE structive::property_extensions)
|
||||
```
|
||||
|
||||
通过成员指针描述 property:
|
||||
|
||||
```cpp
|
||||
auto info = presentation::describe<&Device::temperature>(device.schema());
|
||||
```
|
||||
|
||||
返回:
|
||||
|
||||
```cpp
|
||||
struct Presentation_Info {
|
||||
std::string_view key;
|
||||
std::string_view label;
|
||||
std::string_view description;
|
||||
std::string_view group;
|
||||
std::size_t order;
|
||||
bool has_order;
|
||||
};
|
||||
```
|
||||
|
||||
没有声明 presentation label 时,Presentation Extension 使用 property key 作为显示 label。这个 fallback 是 Presentation 自己的政策,不属于 Core。
|
||||
|
||||
## 5. 可继承 Extension Attribute
|
||||
|
||||
`presentation::group` 是 inheritable,因此可以放入 `defaults(...)`:
|
||||
|
||||
```cpp
|
||||
return object<Device>(
|
||||
defaults(
|
||||
external_access<External_Access::read>,
|
||||
presentation::group<"Environment">
|
||||
),
|
||||
field<&Device::temperature>(
|
||||
key<"temperature">,
|
||||
presentation::label<"Temperature">
|
||||
),
|
||||
field<&Device::pressure>(
|
||||
key<"pressure">,
|
||||
presentation::label<"Pressure">
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
Extension 通过 Core 同一套 default 机制解析有效声明。
|
||||
|
||||
`label`、`description`、`order` 不是 inheritable,因此不能放进 `defaults(...)`。
|
||||
|
||||
## 6. 设计一个新的 Extension Attribute
|
||||
|
||||
例如:
|
||||
|
||||
```cpp
|
||||
namespace my_adapter {
|
||||
struct Json_Name_Category {};
|
||||
template <Fixed_String Value>
|
||||
struct Json_Name_Attribute {
|
||||
using attribute_category = Json_Name_Category;
|
||||
static constexpr bool single_valued = true;
|
||||
static constexpr bool inheritable = false;
|
||||
static constexpr auto value = Value;
|
||||
};
|
||||
template <Fixed_String Value>
|
||||
inline constexpr Json_Name_Attribute<Value> json_name{};
|
||||
}
|
||||
```
|
||||
|
||||
在 Schema 中直接使用:
|
||||
|
||||
```cpp
|
||||
field<&Device::temperature>(
|
||||
key<"temperature">,
|
||||
my_adapter::json_name<"temp">
|
||||
)
|
||||
```
|
||||
|
||||
Core 负责存储它,但完全不需要知道 JSON 是什么。
|
||||
|
||||
## 7. 读取 Extension Attribute
|
||||
|
||||
单个 property descriptor:
|
||||
|
||||
```cpp
|
||||
using Property = std::remove_cvref_t<decltype(property)>;
|
||||
if constexpr (Property::has_attribute<my_adapter::Json_Name_Category>) {
|
||||
const auto& value = property.attribute<my_adapter::Json_Name_Category>();
|
||||
}
|
||||
```
|
||||
|
||||
可继承 category 使用 effective declared lookup:
|
||||
|
||||
```cpp
|
||||
if constexpr (has_declared_effective_attribute_v<Schema, Index, My_Category>) {
|
||||
const auto& value = declared_effective_attribute<Index, My_Category>(schema);
|
||||
}
|
||||
```
|
||||
|
||||
它先检查 property declaration,再检查 object defaults。
|
||||
|
||||
## 8. Multi-Valued Metadata
|
||||
|
||||
Core 的唯一性由 category 协议决定。拥有非 void `attribute_category` 且 `single_valued = true` 的 Attribute,在同一个声明中只能出现一次。
|
||||
|
||||
如果某个领域天然需要重复 annotation,就不要错误地宣称它是 single-valued category;可以通过 `for_each_attribute(...)` 遍历并消费多项数据。
|
||||
|
||||
不要为了满足 single-valued 设计,把本来可以重复的独立元数据硬塞进一个巨大对象。
|
||||
|
||||
## 9. 合理的未来 Extension 边界
|
||||
|
||||
未来 Extension 可以合理拥有例如:
|
||||
|
||||
- serialization name / omission policy;
|
||||
- RPC exposure rule;
|
||||
- UI label / group / editor hint;
|
||||
- database column mapping;
|
||||
- configuration-file mapping;
|
||||
- domain documentation generation。
|
||||
|
||||
这些只是合理领域示例,不代表当前已经实现。
|
||||
|
||||
关键边界是 Core 不能因此直接依赖这些领域库或类型。
|
||||
|
||||
## 10. Extension 可以消费 Core Metadata
|
||||
|
||||
Extension 可以读取 Core-owned category,只要这些 category 本来就是它的输入契约。
|
||||
|
||||
例如 External Adapter 可以使用:
|
||||
|
||||
- property `key` 作为默认协议名字;
|
||||
- `External_Access` 判断是否暴露;
|
||||
- `sensitive` 决定自己的日志是否隐藏值。
|
||||
|
||||
Extension 可以基于这些信息建立自己领域的政策,但不能改变 Core 对这些 Attribute 的基础含义。
|
||||
|
||||
## 11. Fallback 行为属于 Consumer
|
||||
|
||||
Presentation Extension 当前就是示范:
|
||||
|
||||
```text
|
||||
没有 label Attribute
|
||||
↓
|
||||
Presentation Extension 使用 property key 作为 label
|
||||
```
|
||||
|
||||
Core 不应该发明这个 fallback,因为另一个 consumer 完全可能需要另一种策略。
|
||||
|
||||
未来 Adapter 也应该遵守这个原则。只为了某个领域“好用”的默认逻辑应该留在那个领域内部。
|
||||
|
||||
## 12. 防止 Extension 语义泄漏进 Core
|
||||
|
||||
错误方向:
|
||||
|
||||
```text
|
||||
JSON Adapter 需要 alias
|
||||
↓
|
||||
Core 增加 JSON_Alias_Category 和 JSON 命名逻辑
|
||||
```
|
||||
|
||||
正确方向:
|
||||
|
||||
```text
|
||||
JSON Extension 定义 Json_Name_Category
|
||||
↓
|
||||
JSON Extension 自己解释
|
||||
↓
|
||||
Core 保持领域中立
|
||||
```
|
||||
|
||||
## 13. Header-Only Metadata 与 Linked Implementation
|
||||
|
||||
Attribute 定义通常可以 header-only。具体解释逻辑可以根据实现需要选择 header-only 或 linked。
|
||||
|
||||
当前 Presentation Extension 使用 linked target:`presentation::describe()` 在模板层完成 Attribute 选择,然后调用链接实现 `make_presentation_info()` 生成最终结果并处理 fallback。
|
||||
|
||||
不能因为扩展代码量小,就把它的 linked implementation 搬进 Core。
|
||||
|
||||
## 14. Extension 审核清单
|
||||
|
||||
新增 Extension 功能前检查:
|
||||
|
||||
1. 这个概念属于 Core,还是只属于某个 consumer domain?
|
||||
2. 能否直接使用现有 Attribute 协议表达?
|
||||
3. Extension 是否拥有自己解释的 category?
|
||||
4. Inheritable Attribute 是否真的属于对象级默认政策?
|
||||
5. Fallback 是否保留在 Extension 内?
|
||||
6. 是否复用已有 Schema,而不是复制结构树?
|
||||
7. 依赖是否仍然从 Extension 指向 Core?
|
||||
8. Core 是否仍然没有第三方领域类型?
|
||||
9. Extension 是否保持了 Core access、validation、synchronization 的原有语义?
|
||||
|
||||
如果依赖方向或语义边界不成立,应先重新设计分层,再写代码。
|
||||
Reference in New Issue
Block a user