前段时间在写一个玩Minecraft游戏的Agent项目maicraft-next ,它是原python项目maicraft的typescript重构。原项目中各种组件的依赖已经非常复杂了,于是重构的时候就打算引入类似Java Spring一样的IoC的实现。
为了学习相关的概念和实现,我并没有引入第三方的现成的框架,而是和Cursor 加Claude Sonnet 4.5协作写了一套轻量化的IoC容器。本文记录下这个实现,方便后续回看。
1. 概述
在后端开发中,传统模式下组件需自己通过new创建依赖对象,导致代码紧耦合。为解决这一问题,有了IoC(控制反转)设计思想——把“创建、管理依赖”的控制权从组件转移到第三方;
而依赖注入(DI) 是实现IoC最主流的方式,即第三方主动创建依赖对象,通过构造器、Setter等方式“注入”给组件;我们常说的IoC容器(如Spring的ApplicationContext),则是承载IoC思想、执行DI操作的落地工具。
由于项目本身比较小,不打算弄得太复杂导致过度设计,就没用反射机制和类似java注解的机制了,而是在一个初始化文件中手动管理依赖。
先展示一下最后的效果:
1.1服务消费端
需要用到某个组件的时候,无需关心依赖关系,只需要声明自己要哪个组件就能从容器中获取到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class Agent { constructor( private bot: Bot, private executor: ActionExecutor, private llmManager: LLMManager, private memory: MemoryManager, ) { } }
const agent = await container.resolveAsync<Agent>(ServiceKeys.Agent); await agent.start();
|
1.2服务配置端
在bootstrap.ts这个文件注册所有的组件,这里传入的回调函数是在初始化的时候调用来构建该组件的工厂函数,在此处声明依赖关系并且注入(这也是简化的部分,如果要进一步设计,可以用类似反射的机制来避免每个组件都要手动解析依赖和手动注入),因为不是注册的时候立刻初始化所以用回调。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| export function configureServices(container: Container): void { container .registerSingleton(ServiceKeys.Agent, async c => { const bot = c.resolve<Bot>(ServiceKeys.Bot); const executor = c.resolve(ServiceKeys.ActionExecutor); const llmManager = await c.resolveAsync(ServiceKeys.LLMManager); const memory = await c.resolveAsync(ServiceKeys.MemoryManager);
return new Agent(bot, executor, llmManager, memory); }) .withInitializer(ServiceKeys.Agent, async agent => { await agent.initialize(); }) .withDisposer(ServiceKeys.Agent, async agent => { await agent.stop(); }); }
|
1.3应用启动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class MaicraftNext { async initialize(): Promise<void> { const container = new Container();
container.registerInstance(ServiceKeys.Config, this.config); container.registerInstance(ServiceKeys.Bot, this.bot);
configureServices(container);
const agent = await container.resolveAsync<Agent>(ServiceKeys.Agent); await agent.start(); } }
|
2. 核心组件设计
2.1 服务标识符
使用 Symbol 确保类型安全和唯一性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| export const ServiceKeys = { Config: Symbol('Config'), Bot: Symbol('Bot'), Agent: Symbol('Agent'), LLMManager: Symbol('LLMManager'), MemoryManager: Symbol('MemoryManager'), } as const;
export interface ServiceTypeMap { [ServiceKeys.Agent]: Agent; [ServiceKeys.Bot]: Bot; [ServiceKeys.LLMManager]: LLMManager; }
|
为什么选择 Symbol 而非字符串?
主要就下面两个原因,其他性能优化啥的这个项目并不看重,不过既然Claude设计了就留着。
1. 唯一性保证
1 2 3 4 5 6 7 8 9
| const botKey1 = Symbol('Bot'); const botKey2 = Symbol('Bot'); console.log(botKey1 === botKey2);
const botKey1 = 'Bot'; const botKey2 = 'Bot'; console.log(botKey1 === botKey2);
|
2. 防止键冲突
这个特性其实项目足够大才能体现出用处,本项目比较小,一个对象记录就可以,不用分多个。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const userServiceKeys = { Cache: 'Cache', Logger: 'Logger', };
const productServiceKeys = { Cache: 'Cache', Logger: 'Logger', };
const userServiceKeys = { Cache: Symbol('Cache'), Logger: Symbol('Logger'), };
const productServiceKeys = { Cache: Symbol('Cache'), Logger: Symbol('Logger'), };
|
2.2 服务生命周期
1 2 3 4 5
| export enum Lifetime { Singleton = 'singleton', Transient = 'transient', Scoped = 'scoped', }
|
2.3 服务描述符
1 2 3 4 5 6 7 8
| interface ServiceDescriptor<T = any> { key: ServiceKey; factory: Factory<T>; lifetime: Lifetime; instance?: T; initializer?: (instance: T) => Promise<void> | void; disposer?: (instance: T) => Promise<void> | void; }
|
3. 容器核心实现
3.1 Container 类结构
1 2 3 4 5 6 7 8 9 10
| export class Container { private services = new Map<ServiceKey, ServiceDescriptor>(); private resolving = new Set<ServiceKey>();
constructor(logger?: Logger) { this.logger = logger || getLogger('Container'); } }
|
3.2 服务注册
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| registerSingleton<T>(key: ServiceKey, factory: Factory<T>): this { return this.register(key, factory, Lifetime.Singleton); }
registerTransient<T>(key: ServiceKey, factory: Factory<T>): this { return this.register(key, factory, Lifetime.Transient); }
registerInstance<T>(key: ServiceKey, instance: T): this { this.services.set(key, { key, factory: () => instance, lifetime: Lifetime.Singleton, instance, }); return this; } register<T>(key: ServiceKey, factory: Factory<T>, lifetime: Lifetime = Lifetime.Singleton): this { if (this.services.has(key)) { this.logger.warn(`服务 ${String(key)} 已存在,将被覆盖`); }
this.services.set(key, { key, factory, lifetime, });
this.logger.debug(`注册服务: ${String(key)} (${lifetime})`); return this; }
|
3.3 循环依赖检测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| resolve<T>(key: ServiceKey): T { const descriptor = this.services.get(key);
if (this.resolving.has(key)) { const chain = Array.from(this.resolving) .map(k => String(k)) .join(' -> '); throw new Error(`检测到循环依赖: ${chain} -> ${String(key)}`); }
try { this.resolving.add(key); } finally { this.resolving.delete(key); } }
|
由于我对命名有些强迫症,所以顺便问了问为什么要用resolve()而不是get(),在我眼里IoC容器就是个复杂版的Map,而且Spring的命名好像也是getBean()。
回答是:从语义上来说,get通常返回已经存在的元素且不会创建新的元素,而resolve则需要经过复杂的解析依赖过程,且可能创建新的元素;
4. 异步支持
4.1 异步工厂函数
和同步解析相比,核心差别只是async和await关键字,分开两个方便调用方await。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| export type Factory<T = any> = (container: Container) => T | Promise<T>;
async resolveAsync<T>(key: ServiceKey): Promise<T> { const descriptor = this.services.get(key);
if (descriptor.lifetime === Lifetime.Singleton && descriptor.instance) { return descriptor.instance; }
try { this.resolving.add(key);
const instance = await descriptor.factory(this);
if (descriptor.lifetime === Lifetime.Singleton) { descriptor.instance = instance; }
if (descriptor.initializer && descriptor.lifetime === Lifetime.Singleton) { await descriptor.initializer(instance); }
return instance; } finally { this.resolving.delete(key); } }
|
4.2 生命周期管理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| withInitializer<T>(key: ServiceKey, initializer: (instance: T) => Promise<void> | void): this { const descriptor = this.services.get(key); if (!descriptor) { throw new Error(`服务 ${String(key)} 未注册`); } descriptor.initializer = initializer; return this; }
withDisposer<T>(key: ServiceKey, disposer: (instance: T) => Promise<void> | void): this { const descriptor = this.services.get(key); descriptor.disposer = disposer; return this; }
async dispose(): Promise<void> { const disposers: Array<Promise<void>> = [];
for (const descriptor of this.services.values()) { if (descriptor.lifetime === Lifetime.Singleton && descriptor.instance && descriptor.disposer) { disposers.push(descriptor.disposer(descriptor.instance)); } }
await Promise.all(disposers); this.services.clear(); }
|
5. 实际应用案例
5.1 复杂依赖链示例
1 2 3 4 5 6 7 8 9 10 11
| container.registerSingleton(ServiceKeys.Agent, async c => { const bot = c.resolve<Bot>(ServiceKeys.Bot); const executor = c.resolve(ServiceKeys.ActionExecutor); const llmManager = await c.resolveAsync(ServiceKeys.LLMManager); const memory = await c.resolveAsync(ServiceKeys.MemoryManager); const planningManager = await c.resolveAsync(ServiceKeys.GoalPlanningManager); const config = c.resolve<AppConfig>(ServiceKeys.Config);
return new Agent(bot, executor, llmManager, config, memory, planningManager); });
|
5.2 条件性依赖注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| container.registerSingleton(ServiceKeys.MemoryManager, async c => { const memory = new MemoryManager(); const config = c.resolve<AppConfig>(ServiceKeys.Config);
if (config.maibot.enabled) { try { const maibotClient = await c.resolveAsync(ServiceKeys.MaiBotClient); memory.setMaiBotClient(maibotClient); } catch (error) { logger.warn('MaiBot 客户端初始化失败,使用无 MaiBot 模式'); } }
return memory; });
|
5.3 延迟初始化
1 2 3 4 5 6 7 8 9 10 11 12 13
| container .registerSingleton(ServiceKeys.ActionExecutor, c => { const executor = new ActionExecutor(contextManager, logger);
registerActions(executor, logger); return executor; }) .withInitializer(ServiceKeys.ActionExecutor, executor => { contextManager.updateExecutor(executor); });
|
6. 测试支持
6.1 测试容器隔离
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const testContainer = new Container();
testContainer.registerInstance(ServiceKeys.Bot, mockBot); testContainer.registerInstance(ServiceKeys.LLMManager, mockLLMManager);
testContainer.registerSingleton(ServiceKeys.MemoryManager, c => { return new MemoryManager(); });
const agent = await testContainer.resolveAsync<Agent>(ServiceKeys.Agent);
|
6.2 集成测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| describe('Agent Integration Tests', () => { let container: Container; let agent: Agent;
beforeEach(async () => { container = new Container();
setupTestContainer(container);
agent = await container.resolveAsync<Agent>(ServiceKeys.Agent); });
afterEach(async () => { await container.dispose(); }); });
|