Dependency Injection in JavaScript
Dependency Injection (DI) is a fundamental pattern for managing dependencies. This article covers injection techniques, IoC containers, and practical implementations.
Introduction
Dependency Injection provides:
- Loose coupling
- Testability
- Flexibility
- Reusability
- Maintainability
Understanding DI helps you:
- Write testable code
- Manage dependencies
- Build flexible systems
- Reduce coupling
- Improve code quality
Constructor Injection
Basic Constructor Injection
// โ
Good: Constructor injection
class UserRepository {
save(user) {
console.log('Saving user:', user);
}
findById(id) {
console.log('Finding user:', id);
}
}
class UserService {
constructor(repository) {
this.repository = repository;
}
createUser(user) {
return this.repository.save(user);
}
getUser(id) {
return this.repository.findById(id);
}
}
// Usage
const repository = new UserRepository();
const service = new UserService(repository);
service.createUser({ name: 'John' });
service.getUser(1);
Multiple Dependencies
// โ
Good: Multiple dependencies
class EmailService {
send(to, subject, body) {
console.log(`Sending email to ${to}: ${subject}`);
}
}
class LoggerService {
log(message) {
console.log(`[LOG] ${message}`);
}
}
class UserService {
constructor(repository, emailService, logger) {
this.repository = repository;
this.emailService = emailService;
this.logger = logger;
}
createUser(user) {
this.logger.log(`Creating user: ${user.name}`);
const result = this.repository.save(user);
this.emailService.send(user.email, 'Welcome', 'Welcome to our service');
return result;
}
}
// Usage
const repository = new UserRepository();
const emailService = new EmailService();
const logger = new LoggerService();
const service = new UserService(repository, emailService, logger);
service.createUser({ name: 'John', email: '[email protected]' });
Property Injection
// โ
Good: Property injection
class Service {
setRepository(repository) {
this.repository = repository;
return this;
}
setLogger(logger) {
this.logger = logger;
return this;
}
execute() {
this.logger.log('Executing service');
return this.repository.getData();
}
}
// Usage
const service = new Service();
service
.setRepository(new Repository())
.setLogger(new Logger());
service.execute();
Method Injection
// โ
Good: Method injection
class DataProcessor {
process(data, validator, transformer) {
const validated = validator.validate(data);
const transformed = transformer.transform(validated);
return transformed;
}
}
class Validator {
validate(data) {
console.log('Validating data');
return data;
}
}
class Transformer {
transform(data) {
console.log('Transforming data');
return data;
}
}
// Usage
const processor = new DataProcessor();
const result = processor.process(
{ name: 'John' },
new Validator(),
new Transformer()
);
Inversion of Control (IoC) Container
Simple IoC Container
// โ
Good: Simple IoC container
class Container {
constructor() {
this.services = {};
this.singletons = {};
}
register(name, definition, options = {}) {
this.services[name] = {
definition,
singleton: options.singleton || false
};
}
resolve(name) {
const service = this.services[name];
if (!service) {
throw new Error(`Service ${name} not found`);
}
if (service.singleton) {
if (!this.singletons[name]) {
this.singletons[name] = this.createInstance(service.definition);
}
return this.singletons[name];
}
return this.createInstance(service.definition);
}
createInstance(definition) {
if (typeof definition === 'function') {
return new definition();
}
return definition;
}
}
// Usage
const container = new Container();
class Repository {
getData() {
return 'data';
}
}
class Service {
constructor(repository) {
this.repository = repository;
}
}
container.register('repository', Repository, { singleton: true });
container.register('service', () => {
const repository = container.resolve('repository');
return new Service(repository);
});
const service = container.resolve('service');
console.log(service.repository.getData()); // data
Advanced IoC Container
// โ
Good: Advanced IoC container with dependency resolution
class AdvancedContainer {
constructor() {
this.services = new Map();
this.singletons = new Map();
}
register(name, definition, options = {}) {
this.services.set(name, {
definition,
singleton: options.singleton || false,
dependencies: options.dependencies || []
});
}
resolve(name) {
const service = this.services.get(name);
if (!service) {
throw new Error(`Service ${name} not found`);
}
if (service.singleton && this.singletons.has(name)) {
return this.singletons.get(name);
}
const instance = this.createInstance(service);
if (service.singleton) {
this.singletons.set(name, instance);
}
return instance;
}
createInstance(service) {
const dependencies = service.dependencies.map(dep => this.resolve(dep));
if (typeof service.definition === 'function') {
return new service.definition(...dependencies);
}
return service.definition;
}
}
// Usage
const container = new AdvancedContainer();
class Logger {
log(message) {
console.log(`[LOG] ${message}`);
}
}
class Database {
query(sql) {
console.log(`Executing: ${sql}`);
}
}
class UserRepository {
constructor(database, logger) {
this.database = database;
this.logger = logger;
}
save(user) {
this.logger.log(`Saving user: ${user.name}`);
this.database.query(`INSERT INTO users VALUES (...)`);
}
}
class UserService {
constructor(repository, logger) {
this.repository = repository;
this.logger = logger;
}
createUser(user) {
this.logger.log(`Creating user: ${user.name}`);
this.repository.save(user);
}
}
container.register('logger', Logger, { singleton: true });
container.register('database', Database, { singleton: true });
container.register('userRepository', UserRepository, {
dependencies: ['database', 'logger']
});
container.register('userService', UserService, {
dependencies: ['userRepository', 'logger']
});
const userService = container.resolve('userService');
userService.createUser({ name: 'John' });
Practical DI Patterns
Service Locator Pattern
// โ
Good: Service locator
class ServiceLocator {
constructor() {
this.services = {};
}
register(name, service) {
this.services[name] = service;
}
get(name) {
return this.services[name];
}
}
// Usage
const locator = new ServiceLocator();
locator.register('logger', new Logger());
locator.register('database', new Database());
const logger = locator.get('logger');
const database = locator.get('database');
Factory with DI
// โ
Good: Factory with dependency injection
class ServiceFactory {
constructor(container) {
this.container = container;
}
createUserService() {
const repository = this.container.resolve('userRepository');
const logger = this.container.resolve('logger');
return new UserService(repository, logger);
}
createProductService() {
const repository = this.container.resolve('productRepository');
const logger = this.container.resolve('logger');
return new ProductService(repository, logger);
}
}
// Usage
const factory = new ServiceFactory(container);
const userService = factory.createUserService();
const productService = factory.createProductService();
Module Pattern with DI
// โ
Good: Module pattern with DI
const UserModule = (() => {
let repository;
let service;
return {
init(container) {
repository = container.resolve('userRepository');
service = new UserService(repository);
},
getService() {
return service;
}
};
})();
// Usage
UserModule.init(container);
const userService = UserModule.getService();
Testing with DI
Mocking Dependencies
// โ
Good: Easy testing with DI
class UserService {
constructor(repository) {
this.repository = repository;
}
getUser(id) {
return this.repository.findById(id);
}
}
// Mock repository for testing
class MockRepository {
findById(id) {
return { id, name: 'Mock User' };
}
}
// Test
const mockRepository = new MockRepository();
const service = new UserService(mockRepository);
const user = service.getUser(1);
console.log(user); // { id: 1, name: 'Mock User' }
Spy Objects
// โ
Good: Spy objects for testing
class SpyRepository {
constructor() {
this.calls = [];
}
findById(id) {
this.calls.push({ method: 'findById', args: [id] });
return { id, name: 'User' };
}
getCalls() {
return this.calls;
}
}
// Test
const spyRepository = new SpyRepository();
const service = new UserService(spyRepository);
service.getUser(1);
service.getUser(2);
console.log(spyRepository.getCalls());
// [
// { method: 'findById', args: [1] },
// { method: 'findById', args: [2] }
// ]
Best Practices
-
Inject dependencies through constructor:
// โ Good class Service { constructor(dependency) { this.dependency = dependency; } } // โ Bad class Service { constructor() { this.dependency = new Dependency(); } } -
Use interfaces/contracts:
// โ Good class Service { constructor(repository) { // repository must have save() and findById() this.repository = repository; } } // โ Bad class Service { constructor(obj) { this.obj = obj; } } -
Keep container configuration centralized:
// โ Good // config/container.js container.register('service', Service, { dependencies: ['repository', 'logger'] }); // โ Bad // Scattered throughout codebase const service = new Service(repo, logger);
Common Mistakes
-
Service Locator anti-pattern:
// โ Bad - hidden dependencies class Service { constructor() { this.repository = ServiceLocator.get('repository'); } } // โ Good - explicit dependencies class Service { constructor(repository) { this.repository = repository; } } -
Circular dependencies:
// โ Bad - circular dependency class A { constructor(b) { this.b = b; } } class B { constructor(a) { this.a = a; } } // โ Good - break the cycle class A { constructor(b) { this.b = b; } } class B { setA(a) { this.a = a; } } -
Over-engineering:
// โ Bad - too complex const container = new AdvancedContainer(); // ... 50 lines of configuration // โ Good - simple when possible const service = new Service(repository, logger);
Summary
Dependency Injection improves code quality. Key takeaways:
- Constructor injection is most common
- Property injection for optional dependencies
- Method injection for specific operations
- IoC containers manage complex dependencies
- Improves testability
- Reduces coupling
- Enables flexibility
- Facilitates maintenance
Related Resources
- Dependency Injection - MDN
- Inversion of Control - Wikipedia
- IoC Containers - Martin Fowler
- SOLID Principles
- Design Patterns
Next Steps
- Learn about Architectural Patterns
- Explore Module Patterns
- Study SOLID Principles
- Practice dependency injection
- Build IoC containers
Comments