新闻详情

新闻详情

首页 / 资讯中心 / 详情

cal.diy 依赖注入工程实践:用 moduleLoader 实现构建期安全的松耦合架构

发布时间:2026/9/9 23:22:37来源:尧图网络
cal.diy 依赖注入工程实践:用 moduleLoader 实现构建期安全的松耦合架构
cal.diy 依赖注入工程实践用 moduleLoader 实现构建期安全的松耦合架构【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy本指南讲解 cal.diyCal.com 开源调度基础设施在后端服务与仓储层强制推行的依赖注入Dependency Injection, DI规范核心是围绕evyweb/ioctopus容器与自研的moduleLoader 模式让类在容器中如何被创建、依赖谁都由类型系统在构建期校验而不是等到运行时才暴露缺失依赖。读完本文你将掌握 cal.diy 团队定义的四层模式Application Service / Domain Service / Repository / 装饰器并能在自己的 feature 中按 Token → Service → Module → Container 四步接入这套 DI 体系同时避开最常见的四种错误写法。为什么 cal.diy 强制依赖注入依赖注入的目的是实现松耦合类不负责创建自己依赖的对象而是通过构造函数把依赖注入进来。在 cal.diy 这类体量庞大的 monorepo 中Booking、EventType、Availability、Webhook 等大量 feature 相互依赖若每个服务直接new出自己的依赖代码会迅速演变成难以测试、难以替换实现的紧耦合网络。文档给出的反例是直接在类内部实例化依赖class BookingService { private repository new BookingRepository(); private emailService new EmailService(); private calendarService new GoogleCalendarService(); async createBooking(data: CreateBookingDTO) { const booking await this.repository.create(data); await this.emailService.sendConfirmation(booking); await this.calendarService.createEvent(booking); return booking; } }这种写法把 BookingRepository、EmailService、GoogleCalendarService 的构造方式全部硬编码进 BookingService无法替换为 mock、无法更换具体实现例如把 GoogleCalendarService 换成另一个日历适配器、也难以单独测试 createBooking。规范要求的正确写法是让依赖全部经由构造函数注入类只声明我需要什么不关心它从哪来class BookingService { constructor( private readonly repository: BookingRepository, private readonly emailService: EmailService, private readonly calendarService: CalendarService, ) {} async createBooking(data: CreateBookingDTO) { const booking await this.repository.create(data); await this.emailService.sendConfirmation(booking); await this.calendarService.createEvent(booking); return booking; } }这样 BookingService 变为纯业务逻辑载体任何测试只需注入桩stub依赖即可验证 createBooking 的编排逻辑。配套的四层模式Required PatternsDI 不是孤立实践它依赖一组清晰的分层约定规则文档要求同时遵守Application Services编排用例use case在领域服务与仓储之间做协调例如上面例子中的createBooking流程。Domain Services承载不属于任何单一实体的业务逻辑避免把规则塞进实体内部。Repositories抽象数据访问、隔离底层技术选型Prisma 等让业务层只面向仓储接口编程。Caching Proxies透明地包装仓储或服务以增加缓存行为不改变被包装对象的外部契约。Decorators在不污染领域逻辑的前提下叠加横切关注点如日志、指标埋点。这些层共同保证了业务层面向抽象这一 DI 的根基——调用方只依赖接口与仓储抽象具体实例由容器按模块绑定关系组装。moduleLoader构建期安全的类型化 DI 基础设施cal.diy 在 package.json 依赖中选用evyweb/ioctopus作为底层 DI 容器并在其之上封装了模块加载器moduleLoader模式。规则文档点明其核心价值如果一个服务新增了依赖TypeScript 会在构建期build time而不是运行时捕捉到缺失的绑定——因为绑定某个类时声明出来的依赖集合必须与构造函数签名严格一致多一个、少一个都编译不过。这套基础设施的实现集中在 packages/features/di/di.ts其中最关键的是bindModuleToClassOnToken。看它的类型签名di.ts即可理解类型安全从何而来它把类类型约束为new (deps: any) any随后通过条件类型推断出构造函数依赖对象的形状depsMap: Record keyof (TClass extends new (deps: infer TDeps) any ? TDeps : never), ModuleLoader ;也就是说一旦MyService构造函数声明了{ bookingRepo, userRepo }编译器就要求depsMap必须恰好提供bookingRepo与userRepo两个键。与此同时di.ts 还做了运行时兜底校验dep与depsMap二者不可同时提供、也不可同时缺失否则直接抛出明确错误。三个核心概念概念载体文件职责Tokenfeature 内di/tokens.ts唯一标识容器中的每个服务/仓储通常是Symbol。每个可注入类都要有对应 TokenModule*.module.ts用bindModuleToClassOnToken声明某个类绑定到某个 Token以及它需要哪些依赖模块。导出moduleLoader对象Container*.container.ts创建容器实例并对外暴露 getter 函数如getBookingAccessService()消费方只调用 getter 获取服务不接触容器细节ModuleLoader是核心抽象di.ts 将其类型定义为export type ModuleLoader { token: string | symbol; loadModule: (container: Container) void };从 di.ts 的实现可以看到 moduleLoader 的真正威力——自动递归解析依赖loadModule先把自身 module 以moduleToken载入容器然后遍历depsMap或单一dep逐个调用其loadModule。于是加载一个顶级服务时它依赖的仓储、仓储依赖的 Prisma 客户端都会被级联载入调用方完全无需关心依赖树有多深。五步落地一个 DI 服务下面按规则文档的步骤结合仓库真实文件结构演示在一个 featuremyfeature中接入 DI。文档约定 feature 的 DI 工件都放在该 feature 的di/目录例如真实存在的 packages/features/bookings/di、packages/features/watchlist/di。第一步在 feature 的 di 目录定义 Token每个可注入类需要一个 Token。Token 定义在 feature 内的tokens.ts// packages/features/myfeature/di/tokens.ts export const MY_FEATURE_DI_TOKENS { MY_SERVICE: Symbol(MyService), MY_SERVICE_MODULE: Symbol(MyServiceModule), };注意 Token 与模块 Token 成对出现MY_SERVICE/MY_SERVICE_MODULE——前者用于最终取用服务后者用于把 module 注册进容器时避免重复加载。随后把 feature 的 Token 合并进中央 Token 文件真实案例见 packages/features/di/tokens.ts// packages/features/di/tokens.ts import { MY_FEATURE_DI_TOKENS } from calcom/features/myfeature/di/tokens; export const DI_TOKENS { // ...existing tokens ...MY_FEATURE_DI_TOKENS, };仓库中的中央 tokens.ts 即采用这种模式先定义基础设施级 TokenPRISMA_CLIENT、READ_ONLY_PRISMA_CLIENT、REDIS_CLIENT、各仓储及服务 Token再通过展开运算符并入 BOOKING、EVENT_TYPE、FEATURE_OPT_IN、FLAGS、HASHED_LINK、OAUTH、WATCHLIST、WEBHOOK 等各 feature 的 Token。feature 内部需要独立 Token 空间时也允许像 packages/features/di/watchlist/Watchlist.tokens.ts 那样单独维护一组Symbol再汇总到中央文件。第二步用构造函数注入定义服务类多依赖的服务构造函数接收一个 deps 接口对象业务方法统一通过this.deps访问依赖// packages/features/myfeature/services/MyService.ts export interface IMyServiceDeps { bookingRepo: BookingRepository; userRepo: UserRepository; } export class MyService { constructor(private deps: IMyServiceDeps) {} async doSomething() { const bookings await this.deps.bookingRepo.findMany({...}); const user await this.deps.userRepo.findById({...}); } }单一依赖的服务/仓储直接传入该依赖即可// packages/features/myfeature/repositories/MyRepository.ts export class MyRepository { constructor(private prismaClient: PrismaClient) {} async findById(id: string) { return this.prismaClient.myModel.findUnique({ where: { id } }); } }这正是 depsMap / dep 两种绑定形态的分界点构造函数形参是对象类型就用depsMap描述构造函数只收一个参数就用dep描述二者在 di.ts 中被强制互斥。第三步创建带 moduleLoader 的 Module 文件多依赖场景使用depsMap把依赖名映射到依赖自己的 moduleLoader// packages/features/myfeature/di/MyService.module.ts import { bindModuleToClassOnToken, createModule, type ModuleLoader } from calcom/features/di/di; import { MyService } from calcom/features/myfeature/services/MyService; import { moduleLoader as bookingRepositoryModuleLoader } from ./BookingRepository.module; import { moduleLoader as userRepositoryModuleLoader } from ./UserRepository.module; import { MY_FEATURE_DI_TOKENS } from ./tokens; const thisModule createModule(); const token MY_FEATURE_DI_TOKENS.MY_SERVICE; const moduleToken MY_FEATURE_DI_TOKENS.MY_SERVICE_MODULE; const loadModule bindModuleToClassOnToken({ module: thisModule, moduleToken, token, classs: MyService, depsMap: { bookingRepo: bookingRepositoryModuleLoader, userRepo: userRepositoryModuleLoader, }, }); export const moduleLoader: ModuleLoader { token, loadModule, }; export type { MyService };单一依赖用dep取代depsMap// packages/features/myfeature/di/MyRepository.module.ts const loadModule bindModuleToClassOnToken({ module: thisModule, moduleToken, token, classs: MyRepository, dep: prismaModuleLoader, });仓库里 Prisma 客户端本身就是按此模式暴露的模块见 packages/features/di/modules/Prisma.tsprismaModule.bind(token).toFactory(() prisma, singleton)并把 read-only Prisma 客户端一并绑定为READ_ONLY_PRISMA_CLIENT最终导出moduleLoader供所有仓储模块引用。共享的日志服务也遵循同样约定packages/features/di/shared/services/logger.service.ts其depsMap: {}表明一个零依赖类同样可以走这套声明式绑定。第四步创建使用 moduleLoader 的 Container容器负责把整个依赖图装配起来并对外暴露类型化的 getter// packages/features/myfeature/di/MyService.container.ts import { createContainer } from calcom/features/di/di; import { type MyService, moduleLoader as myServiceModuleLoader } from ./MyService.module; const myServiceContainer createContainer(); export function getMyService(): MyService { myServiceModuleLoader.loadModule(myServiceContainer); return myServiceContainer.getMyService(myServiceModuleLoader.token); }仓库中 packages/features/di/containers/BookingAccessService.ts 就是这一模式的精确实例创建单例容器getter 内先bookingAccessServiceModuleLoader.loadModule(container)再container.get(...)实现按需懒加载。与之对照较早的容器如 packages/features/di/containers/AvailableSlots.ts采用启动时一次性container.load(...)全部模块的写法——它能工作但手动列出十几个load调用既不安全也无法自动解析依赖树这正是规则文档第 4 条错误要反对的写法新代码应统一走 moduleLoader。第五步消费方通过 getter 使用服务业务代码不再 import 类本身去实例化而是导入容器 getterimport { getMyService } from calcom/features/myfeature/di/MyService.container; const myService getMyService(); await myService.doSomething();替换实现时只需改动 Module 中的绑定例如把GoogleCalendarService换成新的CalendarService实现所有消费方零改动。必须避开的四个常见错误规则文档总结了实践中反复出现的四类反模式这里结合源码逐一拆解。错误 1把仓储/服务类写成全静态方法。静态方法直接绕过容器与实例状态无法注入依赖也难以在测试中替换// Bad - Static methods bypass DI export class BookingRepository { static async findById(id: string) { return prisma.booking.findUnique({ where: { id } }); } } // Good - Instance methods with constructor injection export class BookingRepository { constructor(private prismaClient: PrismaClient) {} async findById(id: string) { return this.prismaClient.booking.findUnique({ where: { id } }); } }错误 2绕过容器手工new实例。手工拼装会复制依赖知识改一处构造签名就要全局搜索修补// Bad - Manual instantiation bypasses DI const bookingRepo new BookingRepository(prisma); const myService new MyService({ bookingRepo }); // Good - Use the DI containers getter function import { getMyService } from calcom/features/myfeature/di/MyService.container; const myService getMyService();从源码结构看仓库中的容器层正是为避免这类散落的手工组装而存在的packages/features/di/containers 目录下每个文件都收敛了一个服务如getBookingAccessService、getAvailableSlotsService的唯一获取入口。错误 3服务直接 import Prisma 客户端。这会让业务代码与数据库访问深度耦合绕过仓储抽象// Bad - Service imports Prisma directly import prisma from calcom/prisma; export class MyService { async doSomething() { const bookings await prisma.booking.findMany({...}); } } // Good - Service depends on repository via DI export class MyService { constructor(private deps: IMyServiceDeps) {} async doSomething() { const bookings await this.deps.bookingRepo.findMany({...}); } }仓库的实际走向是数据访问集中在 repositories 层服务通过注入拿到仓储。规则文档对应的还有>// Bad - Manual module loading is not type-safe const container createContainer(); container.load(DI_TOKENS.PRISMA_MODULE, prismaModule); container.load(DI_TOKENS.BOOKING_REPOSITORY_MODULE, bookingRepositoryModule); // Good - Use moduleLoader for type-safe dependency loading export function getMyService(): MyService { myServiceModuleLoader.loadModule(container); return container.getMyService(myServiceModuleLoader.token); }手工container.load丢失了依赖是否齐全的编译期校验——漏掉某个依赖的加载只有在运行时get时才可能暴露。而 moduleLoader 的递归加载di.ts保证依赖树必然完整。引入 DI 的收益规则文档把该模式的影响级别标为HIGH理由是它同时带来多项工程收益构建期安全Build-time safety服务新增依赖后模块绑定处会立刻出现类型错误缺失依赖在编译阶段而非运行时被拦截。可测试性Testability所有依赖都经构造函数注入测试中可轻松替换为 mock。仓库的 packages/features/di/containers/FeatureRepository.integration-test.ts 即是围绕 DI 装配的服务编写集成测试的实例。一致性Consistency所有实例都经由同样的绑定-装配路径创建杜绝有的地方注入、有的地方 new的双轨制。可维护性Maintainability替换某个依赖只需更新对应 Module 的绑定消费方无感知。自动依赖解析Automatic dependency resolutionmoduleLoader 递归加载全部依赖调用方不必关心依赖树的层级与顺序。自我文档化Self-documenting每个 Module 显式声明自己依赖谁depsMap就是类依赖关系的一份可读清单。这套模式的设计思路源于 Cal.diy 工程博客对 2026 年后后端工程化方向的总结而其落地形态——Token 集中注册、Module 声明绑定、Container 按需装配——在 packages/features/di 目录中可以直接阅读到真实代码适合作为新 feature 接入 DI 时的参照样板。若想了解该规范在整个规则体系中的位置及其余工程约束可继续查阅 agents/rules/README.md。【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

用 Anthropic Cybersecurity Skills 编写 CMMC Level 2 就绪报告:110 项 800-171 控制、SPRS 评分与 POAM 完整实战 2026/9/10 0:04:44

用 Anthropic Cybersecurity Skills 编写 CMMC Level 2 就绪报告:110 项 800-171 控制、SPRS 评分与 POAM 完整实战

用 Anthropic Cybersecurity Skills 编写 CMMC Level 2 就绪报告:110 项 800-171 控制、SPRS 评分与 POA&M 完整实战 【免费下载链接】Anthropic-Cybersecurity-Skills 817 structured cybersecurity skills for AI agents Mapped to 6 frameworks: MITRE ATT&…

阅读更多 →
SpringBoot+Vue民宿预订管理系统开发实践:从架构设计到部署上线 2026/9/10 0:04:44

SpringBoot+Vue民宿预订管理系统开发实践:从架构设计到部署上线

如果你正在做“SpringBoot Vue 旅游民宿预订管理系统”这类前后端分离项目,这篇文章应该能帮你少走不少弯路。我会从项目选型、数据库设计、后端核心实现、前端工程化到部署上线,按真实开发流程完整拆一遍,所有代码和配置都是可以直接照着改…

阅读更多 →
Spring Boot+Vue+Node.js售后服务系统开发实战 2026/9/10 0:04:44

Spring Boot+Vue+Node.js售后服务系统开发实战

1. 项目拆解:售后服务系统的核心业务与技术选型先说说这个项目到底是干什么的。很多人一听“手机数码电脑售后服务系统”,第一反应就是“不就是个报修单吗”。实际做进去才会发现,这个系统远不止填个单子、派个活儿那么简单。它要处理的是一整…

阅读更多 →
AI搜索的信任缺口:企业内容如何在答案时代自证可信 2026/9/10 0:04:44

AI搜索的信任缺口:企业内容如何在答案时代自证可信

当用户向豆包或DeepSeek询问“哪家工厂的数控设备稳定性好”时,大模型给出的回答并非来自企业官网的自我陈述,而是基于对全网信息源的语义评估与可信度排序。这一机制决定了企业内容在AI搜索时代的核心困境:传统SEO时代靠外链数量和关键词密度…

阅读更多 →
AI搜索重构内容生态:企业从“流量争夺”转向“答案共建” 2026/9/10 0:04:44

AI搜索重构内容生态:企业从“流量争夺”转向“答案共建”

当用户习惯从“翻找网页”转向“直接提问”,AI搜索正在重塑内容生态的权力结构。传统SEO时代,企业争夺的是搜索结果页的排名位置;而生成式引擎优化(GEO)时代,企业争夺的是大模型回答中的“引用资格”。这一…

阅读更多 →
别再问C#学习资料少,正确的入坑姿势和实战避坑指南 2026/9/10 0:01:44

别再问C#学习资料少,正确的入坑姿势和实战避坑指南

后台收到一条让人哭笑不得的私信,原话是“c#学习资料好少啊”。我当时就想,兄弟,你不是缺资料,你是缺一个正确的入坑姿势。C#从2000年诞生到现在,二十多年了,从Windows桌面到Unity游戏、从服务器后端到物联…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞