425 lines
14 KiB
Markdown
425 lines
14 KiB
Markdown
# 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.
|