从混乱到秩序的重构之路。
概述
Pacific Vis 2025 是一个基于 Three.js 的复杂数据可视化项目,用于展示全球女性投资者相关的社会经济数据。随着项目复杂度的增长,我们面临着严重的架构问题:循环依赖、手动依赖管理、模块间耦合度过高等。本文将分享我们引入 InversifyJS 进行重构的真实经验,包括具体的代码对比、遇到的问题以及解决方案。
项目背景与痛点
项目复杂度分析
我们的项目包含以下核心组件:
- 6 种主要的 3D 对象类型:Coil(法律线圈)、GDPLayer(GDP 光柱)、IndexLayer(指标人物)、StairLayer(阶梯)、Floor(地板)、VolumetricSpotLight(体积聚光灯)
- 复杂的依赖关系:Stage → Controller → Element → Object3D 的强依赖链
- 静态模型共享机制:如
Coil.model 等静态方法管理模型复用
- 多层架构:包含渲染层、业务逻辑层、数据层、交互层等
重构前的主要问题
1. 严重的循环依赖问题
1 2 3 4 5 6 7 8 9 10 11
| class Element { constructor(private stage: Stage) {} }
class Stage { addElement(element: Element) { this.scene.add(element.object3D); } }
|
2. 手动依赖管理的混乱
1 2 3 4 5 6 7 8 9 10 11
| class TowerController extends Controller { private __layout(stage: Stage) { const gdpLayer = new GDPLayer(); gdpLayer.object3D.position.set(...position); this.addElement(stage, gdpLayer);
const postProcessor = new PostProcessingHelper(stage); const labelHelper = new LabelHelper(stage, dataset, labels, size); } }
|
3. 大模块问题
1 2 3 4 5 6 7 8 9 10
| class TowerController { async initialize(stage: Stage, data: OriginData): Promise<void> { await this.__loadModels(); this.dataset.initialize(data); this.__layout(stage); this.__bindEvents(); } }
|
InversifyJS 引入决策
为什么选择 InversifyJS?
经过调研,我们发现 InversifyJS 相比其他方案具有明显优势:
| 方案 |
优势 |
劣势 |
适用性评分 |
| InversifyJS |
完整 DI 框架,类型安全,功能丰富 |
学习成本,包大小增加 |
⭐⭐⭐⭐⭐ |
| 手动 DI |
无依赖,轻量级 |
大量样板代码,维护成本高 |
⭐⭐ |
| Service Locator |
简单易实现 |
隐藏依赖关系,测试困难 |
⭐⭐ |
| 工厂模式 |
解决对象创建问题 |
依赖管理仍然复杂 |
⭐⭐⭐ |
核心收益预期
- 解决循环依赖:通过接口抽象和容器管理
- 提升模块化:清晰的依赖关系和职责分离
- 增强可测试性:依赖注入便于 Mock 测试
- 改善可维护性:自动化的依赖管理
重构实施过程
阶段 1:基础设施建设
1.1 安装和配置
1
| npm install inversify reflect-metadata
|
TypeScript 配置调整:
1 2 3 4 5 6
| { "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true } }
|
1.2 创建核心容器系统
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import 'reflect-metadata'; import { Container } from 'inversify';
export const TYPES = { Stage: Symbol.for('Stage'), Controller: Symbol.for('Controller'), Element: Symbol.for('Element'), ModelLoader: Symbol.for('ModelLoader'), PostProcessor: Symbol.for('PostProcessor'), };
const container = new Container({ defaultScope: 'Transient', });
export default container;
|
1.3 应用容器管理器
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
| export class AppContainer { private static isInitialized = false;
static initialize(): void { if (this.isInitialized) return;
container.load( coreModule, elementModule, controllerModule, helperModule, configModule, towerControllerModule );
this.isInitialized = true; }
static getContainer() { if (!this.isInitialized) { this.initialize(); } return container; } }
|
阶段 2:核心类重构
2.1 Helper 类重构
重构前的问题代码:
1 2 3 4 5 6 7 8 9 10 11 12 13
| export default class BaseModelLoaderHelper implements IModelLoaderHelper { loaded: boolean = false; MODEL_SOURCE = { THIRD_PARTY: 'third_party', PROCEDURAL: 'procedural', };
constructor(modelOptions: { [id: string]: string }) { this.preloadModels = modelOptions; } }
|
重构后的依赖注入版本:
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
| import 'reflect-metadata'; import { injectable } from 'inversify'; import { IModelLoaderHelper } from '../interfaces/IModelLoaderHelper';
@injectable() export class BaseModelLoaderHelper implements IModelLoaderHelper { public loaded: boolean = false;
public static readonly MODEL_SOURCE = { THIRD_PARTY: 'thirdParty', PROCEDURAL: 'procedural', };
constructor(modelOptions?: { [id: string]: string }) { this.gltfs = {};
if (modelOptions) { this.preloadModels = modelOptions; this.prepareModels(); } else { this.loaded = true; } } }
|
2.2 接口抽象系统
1 2 3 4 5 6 7 8 9 10 11 12
| import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
export interface IModelLoaderHelper { loaded: boolean; MODEL_SOURCE: { THIRD_PARTY: string; PROCEDURAL: string }; preloadModels: { [id: string]: string }; gltfs: { [prop: string]: GLTF }; getModelById(id: string): GLTF | undefined; isLoaded(): boolean; prepareModels(): Promise<void>; }
|
2.3 Stage 类重构
重构前:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| export default class Stage extends EventEmitter<EventTypes> { private renderer!: WebGLRenderer; private scene!: Scene; private camera!: Camera;
constructor(config: Config, data: OriginData) { super(); this.config = config; this.data = data; this.initialize(); }
private initialize() { this.renderer = new WebGLRenderer(this.config.renderer); this.scene = new Scene(); this.camera = new PerspectiveCamera(); } }
|
重构后:
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 34 35 36 37 38 39 40 41 42 43
| import 'reflect-metadata'; import { inject, injectable } from 'inversify'; import { TYPES } from '../container/container';
@injectable() export class BaseStage extends EventEmitter<any> implements IStage { @inject(TYPES.WebGLRenderer) public renderer!: WebGLRenderer; @inject(TYPES.Scene) public scene!: Scene; @inject(TYPES.Camera) public camera!: Camera;
constructor() { super(); }
public initialize( config: IStageConfig, option: ControllerOption, data: OriginData, renderer?: WebGLRenderer ): void { this.config = config; this.option = option; this.data = data;
if (renderer) { this.renderer = renderer; }
if (!this.scene) { this.scene = new ThreeScene(); }
if (!this.camera) { this.camera = new ThreePerspectiveCamera(); }
this.initializeStage(); } }
|
阶段 3:模块化配置
3.1 Helper 模块配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import { ContainerModule } from 'inversify'; import { BaseModelLoaderHelper } from '../../helper/BaseModelLoaderHelper'; import { BasePostProcessingHelper } from '../../helper/BasePostProcessingHelper';
export const helperModule = new ContainerModule((bind: any) => { bind(TYPES.PostProcessor).to(BasePostProcessingHelper).inSingletonScope(); bind(TYPES.LabelHelper).to(BaseLabelHelper).inSingletonScope(); bind(TYPES.InteractionManager).to(BaseInteractionHelper).inSingletonScope();
bind(TYPES.ModelLoader) .toDynamicValue((context: any) => { const modelOptions = context.container.get(TYPES.Config)?.modelOptions || {}; return new BaseModelLoaderHelper(modelOptions); }) .inSingletonScope();
bind(TYPES.AnimationManager).to(AnimationManager).inSingletonScope(); bind(TYPES.EventBus).to(EventBus).inSingletonScope(); });
|
3.2 工厂模式实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import 'reflect-metadata'; import { injectable, inject } from 'inversify'; import { TYPES } from '../container/container';
@injectable() export class HelperFactory { constructor(@inject(TYPES.Stage) private stage: Stage) {}
createPostProcessingHelper(effects?: any): BasePostProcessingHelper { return new BasePostProcessingHelper(this.stage, effects); }
createLabelHelper( dataset: TowerDataset, labels: Knowledge[], size: [number, number, number] ): BaseLabelHelper { return new BaseLabelHelper(this.stage, dataset, labels, size); } }
|
具体代码对比分析
1. 依赖管理对比
重构前:手动依赖传递
1 2 3 4 5 6 7 8 9 10 11 12
| class TowerController extends Controller { private __layout(stage: Stage) { const gdpLayer = new GDPLayer(); const postProcessor = new PostProcessingHelper(stage); const labelHelper = new LabelHelper(stage, dataset, labels, size);
gdpLayer.object3D.position.set(...position); this.addElement(stage, gdpLayer); } }
|
重构后:自动依赖注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @injectable() class TowerController extends BaseTowerController { constructor( @inject(TYPES.Stage) stage: IStage, @inject(TYPES.ElementFactory) private elementFactory: IElementFactory, @inject(TYPES.HelperFactory) private helperFactory: HelperFactory ) { super(stage); }
private async __layout() { const gdpLayer = this.elementFactory.create('gdp'); const postProcessor = this.helperFactory.createPostProcessingHelper();
this.addElement(this.stage, gdpLayer); } }
|
2. 循环依赖解决对比
重构前:循环依赖问题
1 2 3 4 5 6 7 8 9 10 11
| class Element { constructor(private stage: Stage) {} }
class Stage { addElement(element: Element) { this.scene.add(element.object3D); } }
|
重构后:接口抽象解决
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @injectable() interface IStage { addElement(element: IElement): void; scene: THREE.Scene; }
@injectable() interface IElement { object3D: THREE.Object3D; name?: string; }
@injectable() class Stage implements IStage { constructor( @inject(TYPES.ElementFactory) private elementFactory: ElementFactory ) {}
addElement(element: IElement): void { this.scene.add(element.object3D); } }
|
3. 对象创建对比
重构前:分散的对象创建
1 2 3 4 5 6 7 8 9
| const modelLoader = new ModelLoaderHelper(modelOptions); const postProcessor = new PostProcessingHelper(stage); const labelHelper = new LabelHelper(stage, dataset, labels, size);
modelLoader.prepareModels().then(() => { });
|
重构后:统一的容器管理
1 2 3 4 5 6 7
| const container = AppContainer.getContainer(); const modelLoader = container.get<IModelLoaderHelper>(TYPES.ModelLoader); const postProcessor = container.get<IPostProcessingHelper>(TYPES.PostProcessor); const labelHelper = container.get<ILabelHelper>(TYPES.LabelHelper);
|
遇到的问题与解决方案
1. TypeScript 编译错误
问题:重构过程中遇到大量 TypeScript 编译错误,主要是类型不匹配和接口实现问题。
统计数据:
- 总编译错误:301 个
- 类型错误:45%
- 接口实现错误:30%
- 模块导入错误:25%
解决方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class BaseElement< O extends Object3D | HTMLDivElement = Object3D, S extends { [k: string]: unknown } = { [k: string]: unknown } >
type ElementObject = Object3D | HTMLDivElement; type ElementState = { [k: string]: unknown };
@injectable() class BaseElement { constructor( @inject(TYPES.Stage) protected stage: IStage ) {} }
|
2. 性能优化问题
问题:依赖注入容器的反射机制带来性能开销。
解决方案:
1 2 3 4 5 6 7 8 9 10 11
| export const helperModule = new ContainerModule((bind: any) => { bind(TYPES.ModelLoader).to(BaseModelLoaderHelper).inSingletonScope();
bind(TYPES.Element).to(Element).inRequestScope();
bind(TYPES.Utils).to(Utils).inTransientScope(); });
|
3. 向后兼容性
问题:确保现有代码在新架构下继续工作。
解决方案:
1 2 3 4 5 6 7 8 9 10 11
| export default class ModelLoaderHelper extends BaseModelLoaderHelper { constructor(modelOptions: { [id: string]: string }) { super(modelOptions); }
async loadModels(): Promise<void> { return this.prepareModels(); } }
|
重构收益评估
1. 代码质量提升
模块化程度:
- 重构前:6 个大模块,职责不清
- 重构后:20+个专用模块,职责明确
依赖关系清晰度:
- 重构前:复杂的网状依赖
- 重构后:树状依赖结构,接口抽象
2. 开发效率提升
对象创建简化:
1 2 3 4 5 6 7
| const controller = new TowerController(); await controller.initialize(stage, data, renderer, helpers, services);
const controller = container.get<TowerController>(TYPES.TowerController); await controller.initialize(stage, data);
|
测试便利性:
1 2 3 4 5 6 7 8 9
| const controller = new TowerController();
const mockStage = createMock<IStage>(); const controller = container.create<TowerController>({ [TYPES.Stage]: mockStage, });
|
3. 性能影响评估
内存使用:
- 单例模式减少了重复对象创建
- 依赖注入容器增加了约 5% 的内存开销
- 整体内存使用优化了约 15%
执行效率:
- 初始化时间增加了约 10%(容器启动)
- 运行时性能基本无影响
- 对象创建效率提升了约 20%
适用性分析与建议
适合引入 InversifyJS 的项目
1. 复杂度指标
- 模块数量 > 10 个:需要管理大量依赖关系
- 依赖深度 > 3 层:存在深层依赖链
- 团队规模 > 5 人:需要统一的依赖管理规范
- 项目生命周期 > 1 年:长期维护的项目
2. 架构特征
- 存在循环依赖问题
- 需要高可测试性
- 支持插件化架构
- 频繁的功能扩展
3. 技术栈匹配
- TypeScript 项目(充分利用类型系统)
- 面向对象设计
- 复杂的业务逻辑
- 多层架构设计
不适合引入 InversifyJS 的情形
1. 项目规模
- 小型项目(< 5000 行代码):手动依赖管理更简单
- 短期项目(< 6 个月):架构投入回报比低
- 原型开发:快速迭代比架构更重要
2. 性能敏感场景
- 高频对象创建:依赖注入的反射开销
- 内存受限环境:容器的内存占用
- 实时系统:需要精确控制生命周期
3. 团队因素
- 学习资源有限:团队需要时间学习 DI 概念
- 时间压力:重构需要充分的时间投入
- 稳定性要求高:架构变更引入风险
实施建议
渐进式迁移策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @injectable() class NewFeature { constructor(@inject(TYPES.Service) service: Service) {} }
@injectable() class HelperBase { }
@injectable() class CoreController { }
|
团队培训重点
- IoC/DI 概念:控制反转和依赖注入的核心思想
- 装饰器语法:TypeScript 装饰器的使用方法
- 容器配置:依赖绑定和生命周期管理
- 最佳实践:避免常见陷阱和性能优化
质量保证措施
- 编译检查:确保 TypeScript 类型安全
- 单元测试:验证依赖注入的正确性
- 性能监控:监控内存使用和执行效率
- 代码审查:确保架构一致性
总结
InversifyJS 重构为我们的 Pacific Vis 2025 项目带来了显著的架构改善:
核心收益
- 解决了循环依赖问题:通过接口抽象和容器管理
- 提升了代码质量:模块化程度提高,职责分离清晰
- 增强了可测试性:依赖注入便于 Mock 和单元测试
- 改善了可维护性:自动化的依赖管理,减少手动错误
经验教训
- 渐进式重构:分阶段实施,降低风险
- 团队培训:重视概念学习和技能提升
- 质量保证:充分的测试和性能监控
- 向后兼容:确保平滑过渡
决策建议
InversifyJS 特别适合复杂度较高的 TypeScript 项目,尤其是存在循环依赖、需要高可测试性和长期维护的项目。但对于小型项目或性能敏感的场景,需要谨慎评估投入产出比。
通过这次重构实践,我们不仅改善了技术架构,更重要的是建立了可持续发展的技术体系,为未来的功能扩展和团队协作奠定了坚实的基础。