Advanced Topics
This document covers advanced usage patterns and techniques for Vercube's dependency injection system. These topics are useful for complex applications and specific use cases.
Implementation Details
No Reflection Usage
Vercube's dependency injection system is designed to work without using reflection. Instead, it uses a combination of:
- Decorator Metadata: The
@Inject
and other decorators store dependency information directly on the class prototype - Container Registry: The container maintains a registry of services and their implementations
- Explicit Resolution: Dependencies are resolved explicitly when services are instantiated
This approach offers several advantages:
- Better performance (no reflection overhead)
- Works in environments where reflection is limited or disabled
- More predictable behavior
- Easier to debug
How It Works
When you use the @Inject
decorator, it registers the dependency information directly on the class:
class UserService {
@Inject(Logger)
private logger!: Logger;
}
The decorator adds metadata to the class that the container can access without reflection:
// Simplified internal implementation
function Inject(key: ServiceKey) {
return function(target: any, propertyKey: string) {
// Store dependency information directly on the class
if (!target.constructor.__dependencies) {
target.constructor.__dependencies = [];
}
target.constructor.__dependencies.push({
key,
propertyKey,
optional: false
});
};
}
When resolving a service, the container uses this metadata to inject dependencies:
// Simplified internal implementation
resolve<T>(target: Constructor<T>): T {
const instance = new target();
const dependencies = target.__dependencies || [];
for (const dep of dependencies) {
const service = this.get(dep.key);
instance[dep.propertyKey] = service;
}
return instance;
}
Service Lifecycle Hooks
You can implement lifecycle hooks to manage service resources:
class DatabaseService {
private connection?: Connection;
@Init()
async initialize() {
this.connection = await this.connect();
}
@Destroy()
async cleanup() {
if (this.connection) {
await this.connection.close();
}
}
}
Dynamic Service Registration
You can register services dynamically based on configuration:
function registerServices(container: Container, config: Config) {
// Register logger based on environment
if (config.env === 'production') {
container.bind(Logger, ProductionLogger);
} else {
container.bind(Logger, ConsoleLogger);
}
// Register database based on type
switch (config.dbType) {
case 'mysql':
container.bind(Database, MySQLDatabase);
break;
case 'postgres':
container.bind(Database, PostgresDatabase);
break;
default:
container.bind(Database, SQLiteDatabase);
}
}
Conditional Injection
You can use conditional injection based on runtime conditions:
class NotificationService {
@InjectOptional(EmailService)
private emailService?: EmailService;
@InjectOptional(SMSService)
private smsService?: SMSService;
async notify(user: User, message: string) {
if (user.preferences.email && this.emailService) {
await this.emailService.send(user.email, message);
}
if (user.preferences.sms && this.smsService) {
await this.smsService.send(user.phone, message);
}
}
}
Service Composition
You can compose services to create more complex functionality:
class CompositeLogger implements ILogger {
@Inject(FileLogger)
private fileLogger: FileLogger;
@Inject(ConsoleLogger)
private consoleLogger: ConsoleLogger;
info(message: string): void {
this.fileLogger.info(message);
this.consoleLogger.info(message);
}
error(message: string): void {
this.fileLogger.error(message);
this.consoleLogger.error(message);
}
}
Testing Patterns
Mocking Services
// Create a test container
const testContainer = new Container();
// Register mock services
testContainer.bindMock(Logger, {
info: jest.fn(),
error: jest.fn()
});
testContainer.bindMock(UserRepository, {
findById: jest.fn().mockResolvedValue({ id: '123', name: 'Test User' })
});
// Resolve service with mocks
const userService = testContainer.resolve(UserService);
Performance Optimization
Lazy Loading
class LazyService {
@Inject(() => HeavyService)
private heavyService!: HeavyService;
}
Service Caching
class CachedService {
private cache = new Map<string, any>();
@Inject(ExpensiveService)
private expensiveService!: ExpensiveService;
async getData(key: string) {
if (this.cache.has(key)) {
return this.cache.get(key);
}
const data = await this.expensiveService.fetch(key);
this.cache.set(key, data);
return data;
}
}
Best Practices
Use Child Containers for Modularity
- Separate concerns
- Better resource management
- Easier testing
Implement Lifecycle Hooks
- Proper resource cleanup
- Better error handling
- Improved reliability
Use Interceptors for Cross-Cutting Concerns
- Logging
- Performance monitoring
- Error handling
Optimize Service Resolution
- Use lazy loading
- Implement caching
- Minimize dependencies
Write Testable Code
- Use interfaces
- Implement dependency injection
- Create mock services
See Also
- Container - The core container class and its methods
- Decorators - Available decorators for dependency injection
- Types - Type definitions used in the DI system