内部和外部集成
集成会将 Lino 生成的应用程序连接到其他模块、内部服务和第三方系统。选择同步调用、in-process 集成、HTTP 或事件时,应考虑耦合、延迟、可靠性和数据所有权。
目标不只是调用另一个系统,而是声明一个边界:谁提供数据,谁消费数据,哪个契约是公开的,失败时会发生什么,以及操作必须立即完成还是可以稍后处理。
创建集成
集成表示与另一个上下文、内部服务或外部系统的显式通信。它们应具有名称、契约、身份验证、timeout、错误策略和明确职责。
lino integration new --name <ServiceName> lino integration list
在创建另一个集成之前,使用 integration list 检查现有集成。避免使用 CommonIntegration 这类通用集成,因为它们往往会在没有清晰边界的情况下累积职责。
创建集成之前,先定义它解决的问题:查询外部注册表、发送收费请求、验证订阅、同步用户、消费内部模块,或向另一个系统发布数据。如果集成没有明确意图,它可能还没有准备好成为生成的契约。
- 按被集成的上下文命名:优先使用
Billing、Identity、Catalog或Shipping,而不是通用名称。 - 分离契约和实现:领域和应用不应直接依赖传输细节。
- 假设外部失败:任何远程集成都可能变慢、不可用、返回部分错误或更改契约。
集成 resources
resources 表示由集成暴露或消费的对象,例如 customers、invoices、tenants、subscriptions、users 或 documents。
lino integration resource new --service <ServiceName> --module <ModuleName> --entity <EntityName> lino integration resource list --service <ServiceName> --module <ModuleName> --entity <EntityName>
集成 resource 不需要等同于领域实体。它通常是数据交换契约、外部视图,或上下文之间通信所需的最小表示。将整个实体复制到外部契约中,通常会泄露内部细节并增加演进成本。
- 使用被集成系统的词汇来建模 resource。
- 不要把内部实体作为外部契约暴露出去。
- 定义哪些字段是标识符、过滤条件和返回数据。
- 在存在分页、身份验证和调用限制时进行文档说明。
当 resource 表示另一个模块的数据时,只包含消费者需要的内容。同样的原则也出现在 shadow entities 中:消费模块保留一个小的本地副本,与自己的 Use Case 对齐,而不是依赖生产模块的完整模型。
集成操作
操作描述集成 resource 上可用的动作:创建、查询、更新、取消、发送、同步或验证。
lino integration operation new --service <ServiceName> --module <ModuleName> --entity <EntityName> lino integration operation list --service <ServiceName> --module <ModuleName> --entity <EntityName>
每个操作都应定义意图、输入、输出、调用方式、错误行为,以及是否可以安全地重新执行。
- 查询应具有 timeout,并处理不可用情况。
- 远程命令应尽可能具备幂等性。
- 如果业务接受异步处理,外部失败不应破坏主事务。
- 使用日志和 correlation id 跟踪系统之间的调用。
幂等性在写操作中尤其重要。如果外部系统已执行动作之后某次尝试失败,重试可能会重复收费、创建重复记录或发送两次消息。尽可能使用幂等键、外部标识符或文档化的重新执行规则。
消费集成
在建模集成、resource 和操作之后,使用消费功能将应用程序连接到生成的契约。目标是让 Use Case 依赖一个具名抽象,具有可预期的输入和输出,而不是把 HTTP 调用、URL 组装、headers、响应 parsing 和错误处理分散到多个 handlers 中。
lino integration consume
Use Cases 应依赖抽象,而不是散落在代码中的 HTTP 细节。这会让测试、实现替换和统一的失败处理更容易。它也避免每个应用 endpoint 以不同方式重新发明 timeout、身份验证、correlation id、序列化和错误映射。
- 在调用外部服务之前验证数据。
- 使用 cancellation token 和 timeout。
- 将外部响应映射为可预期的内部错误。
- 不要在未经转换的情况下把原始外部 payload 保存为领域规则。
HTTP vs. in-process
当模块一起运行时,in-process 集成简单且快速。HTTP 会在进程之间创建更明确的契约,但会增加延迟、网络故障以及对韧性的需求。
in-process 集成不应意味着可以自由访问一切。即使在同一个 runtime 内,消费者也应通过契约通信,而不是通过内部实体、DbContext 或另一个模块的 repository。网络成本较低,但耦合风险仍然存在。
| 场景 | 常见选择 | 说明 |
|---|---|---|
| 同一个模块化单体中的模块 | In-process 或内部契约 | 保留边界;避免共享实体和持久化。 |
| 拥有独立 deployment 的独立服务 | HTTP 或消息传递 | 处理网络、身份验证、版本控制和不可用。 |
| 异步且可容忍延迟的流程 | 集成事件 | 设计幂等且可追踪的消费者。 |
当生产者和消费者具有不同 runtimes,或希望让边界在运维上显式时,HTTP 更合适。当同一个应用托管这些上下文且响应必须立即返回时,in-process 是合适的,但依赖关系仍必须通过契约声明。
事件 vs. 同步集成
当需要响应来完成当前操作时,使用同步集成。当消费者可以稍后响应且 eventual consistency 可以接受时,使用事件。
这个选择首先是业务决策,其次才是技术决策。如果订单必须在支付服务确认授权之后才能创建,同步调用可能是 Use Case 的核心部分。如果创建订单只需要触发通知、投影更新或同步到另一个模块,事件通常更合适。
| 需求 | 方法 | 技术注意事项 |
|---|---|---|
| 必须立即响应 | 同步集成 | Timeout、fallback、可预期错误和稳定契约。 |
| 可容忍延迟的后续效果 | 集成事件 | Outbox、幂等性、retries 和关联日志。 |
| 频繁读取少量外部数据 | Shadow Entity 或本地投影 | 同步、最终更新和最小模型。 |
