
03. 领域模型与类型系统1. 背景与原理1.1 为什么需要独立的领域内核SOVD 的实体模型Area/Component/App与其传输方式HTTP是两件事。如果实体直接定义在 Axum handler 里会导致无法在车载进程内嵌使用不想起 HTTP 服务也要能维护拓扑无法编写不依赖网络的单元测试客户端与服务端无法共享语义因此opensovd-core被设计为零 HTTP 依赖的纯领域层只依赖tokio::sync、indexmap、serde_json、thiserror。1.2 类型驱动的关系建模SOVD 的实体关系容易出错的地方是用 Component 的 ID 去查 App。项目用类型化的引用规避EntityRef{kind:EntityKind,id:String}kind与id绑定成一个值构造器EntityRef::component(id)/::app(id)/::area(id)强制分型使得拿错种类在构造期就可见。2. 当前实现架构2.1 实体类型opensovd-core/src/entity/// 三者字段高度对称差异仅在关系字段Component{entity_ref,name,area_id:OptionString,metadata:HashMapString,String,tags:VecString,translation_id:OptionString,data_provider:OptionBoxdynDataProvider}App{entity_ref,name,is_located_on:String,// 必填宿主 Componentarea_id:OptionString,metadata,tags,translation_id,data_provider}Area{entity_ref,name,metadata,tags,translation_id,data_provider}构造采用消费式 builder#[must_use]Component::new(id,name).with_area_id(powertrain).with_tags([sensor]).with_metadata(..).with_data_provider(provider)EntityCollection是发现/批导入的传输载体三个Vec平铺 entity_refs()顺序 components→apps→areas。2.2 协议 DTOopensovd-models/类型用途EntityReference { id, name, translation_id?, href, tags? }集合响应中的实体条目Entities ItemsEntityReference集合包装{ items: [...] }EntityCapabilities22 字段能力自描述 HATEOAS 链接表ResponseT { #[serde(flatten)] data: T, schema?: Value }通用响应信封GenericError { error_code, vendor_code?, message, translation_id?, parameters? }错误响应DataCategoryidentData / currentData / storedData / sysInfo / CustomVersionInfoV/SovdInfoV/VendorInfo版本发现UriReference/JsonPointer强类型 URI 与 JSON Pointer2.3 层次关系图┌──────────── 传输/序列化层 (models) ────────────┐ │ EntityReference / EntityCapabilities / │ │ ResponseT / GenericError / DataCategory │ └──────────────────────┬─────────────────────────┘ │ handler 组装 ┌──────────────────────┴─────────────────────────┐ │ 领域层 (core) │ │ EntityRef/EntityKind ── Component/App/Area │ │ Topology ── DataProvider ── DiscoveryProvider │ └────────────────────────────────────────────────┘注意层次错位的隐患Metadatacore::data含is_readable/is_writable/schema但Metadatamodels::data没有这两个字段——可写性与 schema 在 HTTP 层丢失。3. 核心流程与算法3.1 EntityRef 的分型构造implEntityRef{pubfncomponent(id:implIntoString)-Self{Self{kind:EntityKind::Component,id:id.into()}}// app() / area() 同构}EntityKind实现Display输出component/app/area用于日志与错误消息EntityRef派生Hash Eq可直接作为HashMap键拓扑事件与错误类型都依赖它。3.2 实体 → DTO 的组装算法以 component capabilities 为例let belongs_to entity.area_id().map(|_| { format!({base}/components/{}/belongs-to, encode_path_segment(component_id)).into() }); let data entity.data_provider().map(|_| { format!({base}/components/{}/data, encode_path_segment(component_id)).into() }); Ok(Json(Response { data: EntityCapabilities { id: component_id, name: entity.name().to_string(), translation_id, variant, hosts, belongs_to, data, ..Default::default() }, schema: query.include_schema.then(EntityCapabilities::schema), }))算法要点链接按需生成只有当实体确实有area_id/data_provider时才给出对应 href——这是 SOVD 能力即存在性的体现避免客户端拿到 404 链接。include-schema惰性求值bool::then(|| ...)只在请求要求时才生成 schema生成成本不低见 05 章。..Default::default()未实现的能力字段全部为None序列化时由skip_serializing_if省略。variant取自 metadata(!entity.metadata().is_empty()).then(|| entity.metadata().clone())——把自由键值对映射为 SOVD 的变体标识。3.3 根能力与非空才暴露let topo topology.read().await; let components (topo.components().len() 0).then(|| format!({base}/components).into()); let apps (topo.apps().len() 0).then(|| format!({base}/apps).into()); let areas (topo.areas().len() 0).then(|| format!({base}/areas).into());根实体的id/name为空字符串SOVDServer 语义仅按是否有实体动态给出集合链接。注意这里对每个集合调用了.len()而IndexMap::len()是 O(1)无性能问题。3.4 实体校验现状App::new(id, name, is_located_on)只做IntoString转换不校验is_located_on指向的 Component 是否存在area_id指向的 Area 是否存在ID 是否为空或含非法字符孤儿引用在写入期被容忍只在读取期暴露component_of_app返回Ok(None)路由层把None映射成 404。4. 待完善与风险4.1 模型完整性严重Function实体完全缺失高SOVD 层级是 Areas Components Apps Functions但EntityKind只有三值根能力中functions恒为None。这是模型层面的结构性缺口后续补齐成本高于新增一个能力字段。Metadata.is_readable/is_writable在 HTTP 层丢失高models 层Metadata无此字段客户端从data列表无法判断某项是否可写只能尝试 PUT 看是否 400。应在 models 层补齐并纳入 schema。subcomponents/subareas/depends-on未建模中能力字段存在但实体模型无对应字段层级嵌套不被支持。4.2 类型设计问题中App::component_id()恒为Some中is_located_on: String是必填字段但 accessor 返回Optionstr导致调用侧出现恒真分支topology.rs:83的if let Some(comp)、component_of_app的let Some(..) else。这让无宿主 App这一状态无法表达也让代码读者误解。建议要么改字段为OptionString要么 accessor 返回str。data_provider: OptionBoxdyn DataProvider不可 cheap clone中导致 handler 必须在持锁期间调用 provider无法把Arc克隆出来后放锁是 07 章 中读锁跨 await问题的根因。建议改为OptionArcdyn DataProvider。EntityCollection无去重低add_*直接 push重复 ID 会被后者覆盖而无告警。4.3 校验与健壮性中无引用完整性校验中App::new不校验宿主存在性with_area_id不校验 Area 存在性。建议在TopologyWriteGuard::add_*提供严格模式或在Topology::validate()中做全量检查可作为调试/测试工具。ID 无字符合法性校验中路由层依赖encode_path_segment做转义说明上游允许任意字符串 ID但空 ID、超长 ID、含控制字符的 ID 会生成怪异 URI。建议在new()或写入拓扑时校验。Debug实现手写但不一致低三个实体的Debug都手写以隐藏 provider但EntityRef、EntityCollection无自定义Debug日志中实体信息不完整。4.4 建议的改进优先级优先级事项P0modelsMetadata补齐is_readable/is_writable/schemaP1data_provider改为Arcdyn ...解锁读锁跨 await 优化P1引入Function实体或在文档中明确暂不支持 Function 层级P2App::component_id()返回类型与字段必填性对齐P2增加Topology::validate()引用完整性检查工具P3ID 合法性校验与EntityCollection去重5. 关键代码位置内容路径EntityKind/EntityRef/EntityCollectionopensovd-core/src/entity/mod.rs:18-130Component 定义与 builderopensovd-core/src/entity/component.rs:12-78App 定义与 builderopensovd-core/src/entity/app.rs:12-103Area 定义与 builderopensovd-core/src/entity/area.rs:16-...能力模型22 字段opensovd-models/src/discovery.rs:52-157响应信封ResponseT/ItemsTopensovd-models/src/lib.rs:30-45错误模型opensovd-models/src/error.rs:10-78capabilities 组装componentopensovd-server/src/routes/entities/component.rs:95-127capabilities 组装app / area / root.../app.rs:88-110、.../area.rs:75-100、.../mod.rs:55-72