ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Teable v2 领域层(domain)架构解析:聚合、实体、值对象与规格模式的落地实践

Teable v2 领域层(domain)架构解析:聚合、实体、值对象与规格模式的落地实践 Teable v2 领域层domain架构解析聚合、实体、值对象与规格模式的落地实践【免费下载链接】teable✨ AI Spreadsheet for Business项目地址: https://gitcode.com/GitHub_Trending/te/teableteable 是一个面向企业业务的 AI Spreadsheet 开源项目其新一代架构中packages/v2/core承担了与框架无关的领域建模职责。本文以 domain/ARCHITECTURE.md 为骨架结合仓库内的领域源码与测试完整剖析 domain 层的职责边界、依赖约束、核心抽象聚合根/实体/值对象/规格/领域事件以及 table 聚合的构建与更新流程。读完本文你将掌握 teable v2 领域层的组织方式、规范与最佳实践并能基于同样的模式在自己的业务代码中落地 DDD。一、domain 层在 teable v2 中的定位1.1 职责领域模型的核心packages/v2/core/src/domain是整个 v2 内核的领域模型核心它只负责回答业务规则是什么不关心数据怎么存、接口怎么暴露。按架构笔记的声明它的职责包括聚合Aggregates以Table数据表为代表的聚合根管理字段、视图、记录等内部模型实体Entities拥有唯一标识、生命周期内状态可变的对象如Field字段、View视图、TableRecord记录值对象Value Objects以值语义存在、不可变、靠内容判等的对象如TableId、TableName、FieldId、SelectOption规格Specs把领域约束、查询条件与变更意图建模为显式对象供仓库层与 visitor 消费领域事件Domain Events聚合内状态变化产生的事件如TableCreated、FieldUpdated、RecordCreated供下游实时同步、presence 等模块消费。1.2 依赖约束只依赖纯 TS 运行时库架构笔记明确列出了 domain 层的依赖白名单Depends only on TS/JS, neverthrow, zod, nanoid, ts-pattern.这意味着领域层不依赖任何 NestJS、Prisma、Express 等框架或基础设施从而保证领域逻辑可以在纯 Node 环境、测试环境甚至浏览器端复用框架层HTTP 控制器、应用服务只是领域模型的薄壳领域层可以被独立单测无需数据库或网络。其中neverthrow用于显式 Result 错误处理如_unsafeUnwrap()/_unsafeUnwrapErr()贯穿所有领域测试zod用于入参校验如Table.createRecordInputSchemananoid风格的IdGenerator负责生成带前缀的随机 ID。1.3 持久化边界port/adapter 之外的世界笔记中最关键的一条架构纪律是Repository-driven persistence lifecycle details belong behind ports/adapters; application code should observe aggregates and domain events, not drive adapter-private post-persist steps.翻译过来就是持久化生命周期细节如事务提交后清理缓存、发布事件、重建物化视图等属于端口/适配器ports/adapters的私有范畴。应用层与领域层只负责观察聚合与领域事件而不要去驱动 adapter 私有的后置步骤。这正是 adapter-repository-postgres 这类包存在的意义——领域模型完全不知道 SQL 的存在却知道哪些事件被产生、哪些规格需要落库。二、目录结构shared / base / table / formuladomain 层按职责拆分为四个子目录每个子目录都配有一份ARCHITECTURE.md架构笔记子目录职责关键内容shared/领域公共基类与通用值对象/规格框架AggregateRoot、ValueObject、Entity、DomainEvent、DomainError、IdGenerator、RehydratedValueObject以及specification/规格框架And/Or/Not Spec、graph/拓扑排序、pagination/分页、sort/排序base/Base空间基领域概念BaseId、BaseName、Base聚合、BaseBuilder、BaseCreated事件table/Table 聚合含字段/视图/规格/事件Table、TableBuilder、TableMutator、fields/、views/、records/、specs/、events/formula/供领域值对象使用的公式解析与类型推断visitor.ts表达式返回类型推断、functions/函数注册表、CellValueType、typed-value体系从源码结构看domain 目录table/是体量最大的子域其内部又细分为events/、fields/、methods/、records/、specs/、views/六个子模块形成一个完整的高内聚聚合。三、核心抽象从 ValueObject 到 AggregateRootshared/子目录承载了整套 DDD 基础抽象shared/ARCHITECTURE.md 对此有完整说明下面结合源码逐一展开。3.1 ValueObject值语义与相等契约ValueObject.ts 是一个极其精简的抽象基类export abstract class ValueObject { abstract equals(other: this): boolean; }它只强制子类实现equals从而在整个领域层建立统一的按值判等契约。以 DomainBasics.spec.ts 中的测试为证class TestValueObject extends ValueObject { constructor(private readonly value: string) { super(); } equals(other: TestValueObject): boolean { return this.value other.value; } } // expect(left.equals(right)).toBe(true); // a a // expect(left.equals(other)).toBe(false); // a ! b3.2 EntityID 访问器Entity.ts 是所有实体与聚合根的公共基类只做一件事——持有 ID 并提供访问器export abstract class EntityId { protected constructor(private readonly idValue: Id) {} id(): Id { return this.idValue; } }3.3 AggregateRoot收集与释放领域事件AggregateRoot.ts 在Entity之上增加了领域事件收集机制export abstract class AggregateRootId extends EntityId { private readonly domainEvents: IDomainEvent[] []; addDomainEvent(event: IDomainEvent): void { this.domainEvents.push(event); } /** Records multiple domain events. Used by external event generators (like spec visitors). */ recordDomainEvents(events: ReadonlyArrayIDomainEvent): void { ... } pullDomainEvents(): IDomainEvent[] { const events [...this.domainEvents]; this.domainEvents.length 0; return events; } }关键点是pullDomainEvents()取出即清空——事件被应用层/仓库层拉取发布后聚合不会重复发布同一批事件。DomainBasics.spec.ts验证了这一点aggregate.record(event); const events aggregate.pullDomainEvents(); expect(events).toEqual([event]); expect(aggregate.pullDomainEvents()).toEqual([]); // 第二次拉取为空注释还说明recordDomainEvents是供外部事件生成器如 spec visitors使用的——这正好呼应了后文FieldUpdateSemanticsVisitor的定位。3.4 DomainEvent统一事件形状与类型守卫DomainEvent.ts 定义了领域事件的标准接口export interface IDomainEvent { readonly name: DomainEventName; readonly occurredAt: OccurredAt; requestId?: string; // 由 EventBus 在发布时从 ExecutionContext 注入用于全链路追踪 }同时提供了两个工具函数hasDomainEventName(event, eventName)按事件名判等createDomainEventGuardTEvent(eventName)生成类型守卫把IDomainEvent收窄为具体事件类型。事件名本身是强类型值对象DomainEventNameDomainEventName.ts并内置了tableCreated()、computedActivityBatchChanged()等工厂方法。requestId字段的存在表明领域事件会携带全链路追踪标识由 EventBus 在发布时注入。3.5 RehydratedValueObject延迟填充的值对象RehydratedValueObject.ts 解决一个现实问题聚合从仓库重建时部分字段值可能尚未就绪。它允许先创建空占位在rehydrate之后才可访问值protected valueResult(typeName: string): Resultstring, DomainError { if (typeof this.rawValue ! string || this.rawValue.length 0) { return err(domainError.invariant({ message: ${typeName} is not available before rehydrate })); } return ok(this.rawValue); }DomainBasics.spec.ts验证empty()构造的占位对象isRehydrated()为false此时调用value()返回错误rehydrate()之后才返回正常值。笔记中的示例 DbTableName.ts持久化表名与 schema 拆分正是这一抽象的真实用例。3.6 DomainError结构化领域错误模型DomainError.ts 提供结构化的错误码/标签与断言工具。领域层所有失败路径都以ResultT, DomainError形式返回配合neverthrow把业务校验失败从异常机制中剥离出来——这也是TableBuilder.spec.ts中大量_unsafeUnwrapErr()断言能工作的前提。四、规格Spec模式领域约束的显式化teable v2 的领域层对 DDD 的 Specification 模式应用得非常彻底规格散落在三个层面4.1 规格框架shared/specificationspecification 目录 提供规格的基础设施ISpecification.ts规格接口定义accept(visitor)协议AndSpec/OrSpec/NotSpec布尔组合规格SpecBuilder.ts/composeAndSpecs.ts规格的构建与 AND 组合工具ISpecVisitor.ts/NoopSpecVisitor.ts/AbstractSpecFilterVisitor.ts访客框架。4.2 查询规格table/specstable/specs 目录 存放 Table 维度的查询与变更规格例如查询类TableByIdSpec、TableByBaseIdSpec、TableByNameSpec、TableByNameLikeSpec、TableByIncomingReferenceToTableSpec变更类TableAddFieldSpec、TableRemoveFieldSpec、TableRenameSpec、TableDuplicateFieldSpec、TableUpdateFieldNameSpec、TableUpdateViewColumnMetaSpec等。table/ARCHITECTURE.md特别指出Table.updateTableMutator组合出的变更规格复用查询规格如TableByNameSpec与纯变更规格如TableAddFieldSpec但二者会分别传递给不同的消费方——查询规格交给仓库做筛选变更规格交给 visitor 做事件生成与落库。4.3 字段规格fields/specsfields/specs 目录 是一组谓词式规格用于描述字段的类型特征例如FieldIsLinkSpec、FieldIsFormulaSpec、FieldIsLookupSpec、FieldIsRollupSpec计算/引用类FieldIsNumberSpec、FieldIsDateSpec、FieldIsUserSpec、FieldIsAttachmentSpec基础类型类FieldIsPrimarySpec、FieldIsComputedSpec、FieldIsNumberLikeSpec等交叉判定。这些规格与FieldSpecBuilder结合让取所有链接字段取主字段这类语义以组合方式表达例如TableBuilder.spec.ts中buildFieldSpec((builder) builder.isLink())的用法。五、FieldUpdateSemanticsVisitor规格访客驱动实时语义分类架构笔记专门点名的第三个示例是 FieldUpdateSemanticsVisitor.ts它的用途是对字段更新事件做语义分类供下游 realtime/presence 处理。该 visitor 的核心挑战源码注释直白地说明了Table 的规格在accept()中会擦除 visitor 的返回值所以它不再走标准的 visitor 分发而是直接按具体规格类型instanceof分派visit(spec: object): FieldUpdateSpecSemantics | undefined { if (spec instanceof TableUpdateFieldNameSpec) return this.visitTableUpdateFieldName(spec); if (spec instanceof TableUpdateFieldDbFieldNameSpec) return this.visitTableUpdateFieldDbFieldName(spec); // ... 数十个分支 }每个分支产出FieldUpdateSpecSemantics即更新了哪些属性 每个属性在 realtime/presence 中的路径export type FieldUpdateSpecSemantics { readonly updatedProperties: ReadonlyArraystring; readonly propertySemantics: ReadonlyRecordstring, FieldUpdatedPropertySemantics; };其中FieldUpdatedPropertySemantics包含realtimePath实时同步通道中的 JSON 路径如[options]presencePathpresence在线协作状态通道中的路径mayRequirePresence该属性更新是否可能需要 presence 通知。它还区分了顶层属性与选项支撑属性两类语义顶层属性如name、dbFieldName、type、aiConfig直接映射到自身路径而选项类属性formatting、defaultValue、showAs、options等会归并到options根路径下并用具体的 presence key如preventAutoNewOptions、relationship标注。visitTableUpdateFieldConstraints甚至会根据previousNotNull/nextNotNull、previousUnique/nextUnique是否真的变化动态决定是否产出notNull/unique语义。这个 visitor 是规格 visitor模式的典型落地领域层只负责声明改了什么语义解释如何影响实时协作以独立 visitor 的形式旁挂避免污染规格本体。六、Table 聚合构建、更新与记录6.1 TableBuilder流式构建聚合Table.ts 与 TableBuilder.ts 提供了聚合构建入口。TableBuilder.spec.ts 完整展示了流式构建范式const builder Table.builder() .withBaseId(baseId) .withName(tableName); builder.field().singleLineText().withName(titleName).done(); builder.field().number().withName(amountName).primary().done(); builder.field().rating().withName(starsName).withMax(RatingMax.five()).done(); builder .field() .singleSelect() .withName(statusName) .withOptions([todoOption, doneOption]) .done(); builder.view().defaultGrid().done(); const buildResult builder.build(); buildResult._unsafeUnwrap(); // 校验失败时在这里抛错从测试可以提炼出 TableBuilder 的完整约束集这些就是领域不变量的直接证据不变量错误信息取自断言至少需要一个字段at least one Field至少需要一个视图at least one View必须有 BaseIdBaseId is required必须有表名TableName is required主字段必须存在Primary Field must exist主字段唯一错误消息包含primary字段名唯一Field names must be unique视图名唯一View names must be unique字段/视图名必填FieldName is required/ViewName is required聚合为Table builder errorsdetails.errors构建成功后的关键行为也被测试锁定支持grid/kanban/calendar/gallery/form/plugin六种视图类型支持singleLineText/longText/number/rating/singleSelect/multipleSelect/checkbox/attachment/date/user/button基础字段类型允许把非首个字段设为主字段primaryFieldId().equals(table.getFields()[1].id())公式/汇总字段可以引用后声明的字段builder 会事后解析依赖resolveFormulaFields.ts支撑链接字段支持oneOne/manyMany/oneMany/manyOne四种关系其中manyMany会生成junction_fieldId中间表名并创建对称字段symmetricFieldId()每种视图都会初始化列元数据columnMeta网格/插件视图的主字段列默认不显示visible而表单视图只对特定字段标记visible。6.2 Table.update TableMutator不可变更新流查询/构建之外领域层还提供不可变更新入口Table.update(...) TableMutator.ts 组合出变更规格集合并返回新的聚合状态。变更规格如TableRenameSpec、TableAddFieldSpec与查询规格分离传递正如table/ARCHITECTURE.md所述这保证了筛选与变更两种关注点在规格层面就被清晰隔离。6.3 记录模型与公式字段records/TableRecord.ts 是记录实体records/TableRecordFields.ts 以FieldId为键组织字段值对records/下还有 56 个条件规格文件records/specs用于记录查询条件的规格化表达resolveFormulaFields.ts 在构建期解析公式依赖与结果类型其行为由 resolveFormulaFields.spec.ts 保障。七、formula 子域仅供类型推断不做求值domain/formula 与独立的 packages/formula 包分工不同领域内的formula/只做解析与类型推断辅助不做表达式求值。其visitor.ts推断表达式返回类型functions/是带参数校验与返回类型的函数注册表typed-value.ts/typed-value-converter.ts负责类型化值及其归一化。领域值对象 FormulaExpression.ts 在创建时即完成解析与类型推断formula/ARCHITECTURE.md明确给出该示例从而在聚合构建期就能发现引用不存在的字段等错误。八、测试体系领域不变量的事实来源domain 层的测试密度非常高且全部是纯 Vitest 单测无需数据库。除了前面反复引用的两个值得继续深入的有Table.spec.ts聚合行为与不变量的主测试TableSpecs.spec.ts 与 TableSpecBuilder.spec.ts规格组合与构建FieldSpecs.spec.ts字段类型谓词规格IdValueObjects.spec.tsTable/Field/View ID 格式校验graph/topologicalSort.spec.ts公式依赖等场景的拓扑排序。由于领域层不依赖任何框架与基础设施这些测试可以直接在packages/v2/core包内运行pnpm --filter teable/v2-core test一类命令快速反馈不变量是否被破坏。九、小结这套领域架构给我们的启示回顾 teable v2 的 domain 层设计可以提炼出几条可复用的工程原则依赖白名单是领域层的第一纪律。只用纯 TS 库neverthrow、zod、nanoid、ts-pattern领域逻辑才能在框架与存储之外独立演化聚合是事件源。AggregateRoot.pullDomainEvents()取走即清空事件既服务于下游实时同步也服务于 adapter 的持久化后置步骤规格让约束可组合、可访问。查询规格与变更规格分离传递visitor 旁挂解释逻辑如FieldUpdateSemanticsVisitor既保持聚合内聚又让 realtime/presence 等横切关注点有干净的接入点构建与更新分离。TableBuilder负责一次性构建并校验全部不变量Table.update TableMutator负责增量变更二者共用同一套值对象与规格体系测试即规格文档。TableBuilder.spec.ts、DomainBasics.spec.ts等文件把每一条不变量写成可执行断言既是回归保护也是新开发者理解领域规则最快的入口。对于想为 teable 贡献或扩展 v2 内核的开发者最佳切入点就是packages/v2/core/src/domain先读 domain/ARCHITECTURE.md 把握边界再顺着本文提到的每个示例文件TableBuilder.spec.ts、DomainBasics.spec.ts、FieldUpdateSemanticsVisitor.ts深入即可快速建立完整的领域心智模型。【免费下载链接】teable✨ AI Spreadsheet for Business项目地址: https://gitcode.com/GitHub_Trending/te/teable创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表