7 Dependency Injection
Dependency Injection (DI) is a design pattern that implements Inversion of Control (IoC) by removing hard-coded dependencies and making it possible to change them at runtime or compile time. This pattern helps create loosely coupled, testable, and maintainable code.
7.1 Core Concepts
┌─────────────────────────────────────────────────┐
│ Without DI │
│ ┌──────────┐ │
│ │ ClassA │──creates──> ┌──────────┐ │
│ │ │ │ ClassB │ │
│ └──────────┘ └──────────┘ │
│ Tight Coupling (Bad) │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ With DI │
│ ┌──────────┐ ┌──────────┐ │
│ │ ClassA │<──injects── │ ClassB │ │
│ │ │ │ │ │
│ └──────────┘ └──────────┘ │
│ ▲ ▲ │
│ │ │ │
│ └─────────┬───────────────┘ │
│ ┌──────▼──────┐ │
│ │ Injector │ │
│ └──────────────┘ │
│ Loose Coupling (Good) │
└─────────────────────────────────────────────────┘
7.2 Types of Dependency Injection
- Constructor Injection - Dependencies provided through constructor
- Property/Setter Injection - Dependencies set via properties or setters
- Method Injection - Dependencies passed as method parameters
- Interface Injection - Dependency provides injector method (less common)
7.3 Constructor Injection
The most common and recommended form of dependency injection.
// Define interfaces for abstraction
interface ILogger {
log(message: string): void;
}
interface IEmailService {
sendEmail(to: string, subject: string, body: string): void;
}
// Concrete implementations
class ConsoleLogger implements ILogger {
log(message: string): void {
console.log(`[LOG]: ${message}`);
}
}
class FileLogger implements ILogger {
constructor(private filename: string) {}
log(message: string): void {
// In real app, write to file
console.log(`[FILE ${this.filename}]: ${message}`);
}
}
class EmailService implements IEmailService {
constructor(private logger: ILogger) {}
sendEmail(to: string, subject: string, body: string): void {
this.logger.log(`Sending email to ${to}`);
// Send email logic here
this.logger.log(`Email sent successfully`);
}
}
// Service that depends on logger and email service
class UserService {
// Dependencies injected through constructor
constructor(
private logger: ILogger,
private emailService: IEmailService
) {}
createUser(email: string, name: string): void {
this.logger.log(`Creating user: ${name}`);
// Business logic
const userId = Math.random().toString(36);
this.emailService.sendEmail(
,
email"Welcome!",
`Hello ${name}, your ID is ${userId}`
;
)
this.logger.log(`User created with ID: ${userId}`);
}
}
// Usage - Manual injection
const logger = new ConsoleLogger();
const emailService = new EmailService(logger);
const userService = new UserService(logger, emailService);
.createUser("user@example.com", "Alice");
userService
// Easy to swap implementations
const fileLogger = new FileLogger("app.log");
const emailServiceWithFileLogger = new EmailService(fileLogger);
const userServiceWithFileLogger = new UserService(
,
fileLogger
emailServiceWithFileLogger; )
// Define interfaces for abstraction
public interface ILogger
{
void Log(string message);
}
public interface IEmailService
{
void SendEmail(string to, string subject, string body);
}
// Concrete implementations
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
.WriteLine($"[LOG]: {message}");
Console}
}
public class FileLogger : ILogger
{
private readonly string filename;
public FileLogger(string filename)
{
this.filename = filename;
}
public void Log(string message)
{
// In real app, write to file
.AppendAllText(filename, $"[{DateTime.Now}]: {message}\n");
File}
}
public class EmailService : IEmailService
{
private readonly ILogger logger;
public EmailService(ILogger logger)
{
this.logger = logger;
}
public void SendEmail(string to, string subject, string body)
{
.Log($"Sending email to {to}");
logger// Send email logic here
.Log("Email sent successfully");
logger}
}
// Service that depends on logger and email service
public class UserService
{
private readonly ILogger logger;
private readonly IEmailService emailService;
// Dependencies injected through constructor
public UserService(ILogger logger, IEmailService emailService)
{
this.logger = logger;
this.emailService = emailService;
}
public void CreateUser(string email, string name)
{
.Log($"Creating user: {name}");
logger
// Business logic
var userId = Guid.NewGuid().ToString();
.SendEmail(
emailService,
email"Welcome!",
"Hello {name}, your ID is {userId}"
$);
.Log($"User created with ID: {userId}");
logger}
}
// Usage - Manual injection
class Program
{
static void Main()
{
var logger = new ConsoleLogger();
var emailService = new EmailService(logger);
var userService = new UserService(logger, emailService);
.CreateUser("user@example.com", "Alice");
userService
// Easy to swap implementations
var fileLogger = new FileLogger("app.log");
var emailServiceWithFileLogger = new EmailService(fileLogger);
var userServiceWithFileLogger = new UserService(
,
fileLogger
emailServiceWithFileLogger);
}
}
from abc import ABC, abstractmethod
from typing import Protocol
import uuid
from datetime import datetime
# Define protocols/interfaces for abstraction
class Logger(Protocol):
def log(self, message: str) -> None: ...
class EmailService(Protocol):
def send_email(self, to: str, subject: str, body: str) -> None: ...
# Concrete implementations
class ConsoleLogger:
def log(self, message: str) -> None:
print(f"[LOG]: {message}")
class FileLogger:
def __init__(self, filename: str):
self.filename = filename
def log(self, message: str) -> None:
with open(self.filename, 'a') as f:
f"[{datetime.now()}]: {message}\n")
f.write(
class EmailServiceImpl:
def __init__(self, logger: Logger):
self.logger = logger
def send_email(self, to: str, subject: str, body: str) -> None:
self.logger.log(f"Sending email to {to}")
# Send email logic here
self.logger.log("Email sent successfully")
# Service that depends on logger and email service
class UserService:
# Dependencies injected through constructor
def __init__(self, logger: Logger, email_service: EmailService):
self.logger = logger
self.email_service = email_service
def create_user(self, email: str, name: str) -> None:
self.logger.log(f"Creating user: {name}")
# Business logic
= str(uuid.uuid4())
user_id
self.email_service.send_email(
email,"Welcome!",
f"Hello {name}, your ID is {user_id}"
)
self.logger.log(f"User created with ID: {user_id}")
# Usage - Manual injection
= ConsoleLogger()
logger = EmailServiceImpl(logger)
email_service = UserService(logger, email_service)
user_service
"user@example.com", "Alice")
user_service.create_user(
# Easy to swap implementations
= FileLogger("app.log")
file_logger = EmailServiceImpl(file_logger)
email_service_with_file = UserService(file_logger, email_service_with_file) user_service_with_file
7.4 Property/Setter Injection
Dependencies are injected through properties or setter methods after object creation.
interface IDatabase {
connect(): void;
query(sql: string): any[];
}
class PostgresDatabase implements IDatabase {
connect(): void {
console.log("Connected to PostgreSQL");
}
query(sql: string): any[] {
console.log(`Executing: ${sql}`);
return [];
}
}
class Repository {
// Property injection - dependency set after construction
private _database?: IDatabase;
set database(db: IDatabase) {
this._database = db;
this._database.connect();
}
get database(): IDatabase {
if (!this._database) {
throw new Error("Database not injected");
}return this._database;
}
findAll(): any[] {
return this.database.query("SELECT * FROM users");
}
}
// Usage
const repo = new Repository();
.database = new PostgresDatabase(); // Inject dependency via setter
repoconst users = repo.findAll();
// Alternative: Method chaining pattern
class FluentRepository {
private _database?: IDatabase;
private _cache?: Map<string, any>;
withDatabase(db: IDatabase): this {
this._database = db;
return this;
}
withCache(cache: Map<string, any>): this {
this._cache = cache;
return this;
}
build(): void {
if (!this._database) {
throw new Error("Database is required");
}this._database.connect();
}
}
// Fluent usage
const fluentRepo = new FluentRepository()
.withDatabase(new PostgresDatabase())
.withCache(new Map());
.build(); fluentRepo
public interface IDatabase
{
void Connect();
<object> Query(string sql);
List}
public class PostgresDatabase : IDatabase
{
public void Connect()
{
.WriteLine("Connected to PostgreSQL");
Console}
public List<object> Query(string sql)
{
.WriteLine($"Executing: {sql}");
Consolereturn new List<object>();
}
}
public class Repository
{
private IDatabase? _database;
// Property injection with auto-property
public IDatabase Database
{
=> _database ?? throw new InvalidOperationException("Database not injected");
get
set{
= value;
_database .Connect();
_database}
}
// Optional dependency with null checking
public ILogger? Logger { get; set; }
public List<object> FindAll()
{
?.Log("Finding all users");
Loggerreturn Database.Query("SELECT * FROM users");
}
}
// Usage
class Program
{
static void Main()
{
var repo = new Repository();
.Database = new PostgresDatabase(); // Inject via property
repo.Logger = new ConsoleLogger(); // Optional dependency
repo
var users = repo.FindAll();
}
}
// Alternative: Fluent builder pattern
public class RepositoryBuilder
{
private readonly Repository _repository = new();
public RepositoryBuilder WithDatabase(IDatabase database)
{
.Database = database;
_repositoryreturn this;
}
public RepositoryBuilder WithLogger(ILogger logger)
{
.Logger = logger;
_repositoryreturn this;
}
public Repository Build()
{
if (_repository.Database == null)
throw new InvalidOperationException("Database is required");
return _repository;
}
}
// Fluent usage
var repository = new RepositoryBuilder()
.WithDatabase(new PostgresDatabase())
.WithLogger(new ConsoleLogger())
.Build();
from typing import Optional, List, Any
class Database(Protocol):
def connect(self) -> None: ...
def query(self, sql: str) -> List[Any]: ...
class PostgresDatabase:
def connect(self) -> None:
print("Connected to PostgreSQL")
def query(self, sql: str) -> List[Any]:
print(f"Executing: {sql}")
return []
class Repository:
def __init__(self):
self._database: Optional[Database] = None
self._logger: Optional[Logger] = None
# Property injection using @property
@property
def database(self) -> Database:
if not self._database:
raise ValueError("Database not injected")
return self._database
@database.setter
def database(self, db: Database) -> None:
self._database = db
self._database.connect()
# Optional dependency
@property
def logger(self) -> Optional[Logger]:
return self._logger
@logger.setter
def logger(self, logger: Logger) -> None:
self._logger = logger
def find_all(self) -> List[Any]:
if self.logger:
self.logger.log("Finding all users")
return self.database.query("SELECT * FROM users")
# Usage
= Repository()
repo = PostgresDatabase() # Inject via setter
repo.database = ConsoleLogger() # Optional dependency
repo.logger = repo.find_all()
users
# Alternative: Builder pattern with method chaining
class FluentRepository:
def __init__(self):
self._database: Optional[Database] = None
self._cache: Optional[dict] = None
def with_database(self, db: Database) -> 'FluentRepository':
self._database = db
return self
def with_cache(self, cache: dict) -> 'FluentRepository':
self._cache = cache
return self
def build(self) -> None:
if not self._database:
raise ValueError("Database is required")
self._database.connect()
# Fluent usage
= (FluentRepository()
fluent_repo
.with_database(PostgresDatabase())
.with_cache({})) fluent_repo.build()
7.5 Method Injection
Dependencies are passed as method parameters when needed.
interface IValidator {
validate(data: any): boolean;
}
class EmailValidator implements IValidator {
validate(data: any): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(data);
}
}
class PhoneValidator implements IValidator {
validate(data: any): boolean {
const phoneRegex = /^\d{10}$/;
return phoneRegex.test(data);
}
}
class DataProcessor {
// Method injection - dependency passed when needed
processData(data: any, validator: IValidator): void {
if (!validator.validate(data)) {
throw new Error("Invalid data");
}
console.log("Processing:", data);
// Process the data
}
// Multiple dependencies via method injection
processWithLogging(
: any,
data: IValidator,
validator: ILogger
logger: void {
).log(`Processing data: ${data}`);
logger
try {
this.processData(data, validator);
.log("Processing completed");
loggercatch (error) {
} .log(`Error: ${error}`);
loggerthrow error;
}
}
}
// Usage
const processor = new DataProcessor();
// Inject different validators based on context
.processData("user@example.com", new EmailValidator());
processor.processData("1234567890", new PhoneValidator());
processor
// Inject multiple dependencies
.processWithLogging(
processor"test@test.com",
new EmailValidator(),
new ConsoleLogger()
; )
public interface IValidator
{
bool Validate(object data);
}
public class EmailValidator : IValidator
{
public bool Validate(object data)
{
if (data is not string email) return false;
return Regex.IsMatch(email, @"^[^\s@]+@[^\s@]+\.[^\s@]+$");
}
}
public class PhoneValidator : IValidator
{
public bool Validate(object data)
{
if (data is not string phone) return false;
return Regex.IsMatch(phone, @"^\d{10}$");
}
}
public class DataProcessor
{
// Method injection - dependency passed when needed
public void ProcessData(object data, IValidator validator)
{
if (!validator.Validate(data))
{
throw new ArgumentException("Invalid data");
}
.WriteLine($"Processing: {data}");
Console// Process the data
}
// Multiple dependencies via method injection
public void ProcessWithLogging(
object data,
,
IValidator validator)
ILogger logger{
.Log($"Processing data: {data}");
logger
try
{
ProcessData(data, validator);
.Log("Processing completed");
logger}
catch (Exception ex)
{
.Log($"Error: {ex.Message}");
loggerthrow;
}
}
// Generic method injection
public T Transform<T>(
object input,
<object, T> transformer,
Func<string>? logger = null)
Action{
?.Invoke($"Transforming: {input}");
loggervar result = transformer(input);
?.Invoke($"Result: {result}");
loggerreturn result;
}
}
// Usage
class Program
{
static void Main()
{
var processor = new DataProcessor();
// Inject different validators based on context
.ProcessData("user@example.com", new EmailValidator());
processor.ProcessData("1234567890", new PhoneValidator());
processor
// Inject multiple dependencies
.ProcessWithLogging(
processor"test@test.com",
new EmailValidator(),
new ConsoleLogger()
);
// Generic method injection with lambda
var result = processor.Transform(
"123",
=> int.Parse(input.ToString()),
input => Console.WriteLine($"[LOG] {message}")
message );
}
}
import re
from typing import Any, Callable, Optional
class Validator(Protocol):
def validate(self, data: Any) -> bool: ...
class EmailValidator:
def validate(self, data: Any) -> bool:
if not isinstance(data, str):
return False
return bool(re.match(r'^[^\s@]+@[^\s@]+\.[^\s@]+$', data))
class PhoneValidator:
def validate(self, data: Any) -> bool:
if not isinstance(data, str):
return False
return bool(re.match(r'^\d{10}$', data))
class DataProcessor:
# Method injection - dependency passed when needed
def process_data(self, data: Any, validator: Validator) -> None:
if not validator.validate(data):
raise ValueError("Invalid data")
print(f"Processing: {data}")
# Process the data
# Multiple dependencies via method injection
def process_with_logging(
self,
data: Any,
validator: Validator,
logger: Logger-> None:
) f"Processing data: {data}")
logger.log(
try:
self.process_data(data, validator)
"Processing completed")
logger.log(except Exception as e:
f"Error: {e}")
logger.log(raise
# Using callable as dependency
def transform(
self,
data: Any,
transformer: Callable[[Any], Any],str], None]] = None
logger: Optional[Callable[[-> Any:
) if logger:
f"Transforming: {data}")
logger(
= transformer(data)
result
if logger:
f"Result: {result}")
logger(
return result
# Usage
= DataProcessor()
processor
# Inject different validators based on context
"user@example.com", EmailValidator())
processor.process_data("1234567890", PhoneValidator())
processor.process_data(
# Inject multiple dependencies
processor.process_with_logging("test@test.com",
EmailValidator(),
ConsoleLogger()
)
# Using lambda/callable injection
= processor.transform(
result "123",
=lambda x: int(x),
transformer=lambda msg: print(f"[LOG] {msg}")
logger )
7.6 DI Containers and Frameworks
Modern frameworks provide DI containers to automatically manage dependency injection.
// Using InversifyJS - Popular DI container for TypeScript
import "reflect-metadata";
import { Container, injectable, inject } from "inversify";
// Define service identifiers
const TYPES = {
: Symbol.for("Logger"),
Logger: Symbol.for("Database"),
Database: Symbol.for("EmailService"),
EmailService: Symbol.for("UserService")
UserService;
}
// Mark classes as injectable
injectable()
@class ConsoleLogger implements ILogger {
log(message: string): void {
console.log(`[Console]: ${message}`);
}
}
injectable()
@class DatabaseService implements IDatabase {
connect(): void {
console.log("Database connected");
}
query(sql: string): any[] {
return [];
}
}
injectable()
@class EmailService implements IEmailService {
constructor(
inject(TYPES.Logger) private logger: ILogger
@
) {}
sendEmail(to: string, subject: string, body: string): void {
this.logger.log(`Sending email to ${to}`);
}
}
injectable()
@class UserService {
constructor(
inject(TYPES.Logger) private logger: ILogger,
@inject(TYPES.Database) private database: IDatabase,
@inject(TYPES.EmailService) private emailService: IEmailService
@
) {}
createUser(email: string, name: string): void {
this.database.connect();
this.logger.log(`Creating user: ${name}`);
this.emailService.sendEmail(email, "Welcome", "Hello!");
}
}
// Container configuration
const container = new Container();
// Bind interfaces to implementations
.bind<ILogger>(TYPES.Logger).to(ConsoleLogger).inSingletonScope();
container.bind<IDatabase>(TYPES.Database).to(DatabaseService);
container.bind<IEmailService>(TYPES.EmailService).to(EmailService);
container.bind<UserService>(TYPES.UserService).to(UserService);
container
// Usage
const userService = container.get<UserService>(TYPES.UserService);
.createUser("user@example.com", "Alice");
userService
// Alternative: Simple DIY container
class SimpleDIContainer {
private services = new Map<string, any>();
private singletons = new Map<string, any>();
register<T>(key: string, factory: () => T, singleton = false): void {
if (singleton) {
this.singletons.set(key, null);
}this.services.set(key, factory);
}
resolve<T>(key: string): T {
if (this.singletons.has(key)) {
if (!this.singletons.get(key)) {
const factory = this.services.get(key);
this.singletons.set(key, factory());
}return this.singletons.get(key);
}
const factory = this.services.get(key);
if (!factory) {
throw new Error(`Service ${key} not registered`);
}return factory();
}
}
// Usage of simple container
const simpleContainer = new SimpleDIContainer();
.register("logger", () => new ConsoleLogger(), true);
simpleContainer.register("database", () => new DatabaseService());
simpleContainer.register("emailService",
simpleContainer=> new EmailService(simpleContainer.resolve("logger"))
() ;
)
const logger = simpleContainer.resolve<ILogger>("logger");
using Microsoft.Extensions.DependencyInjection;
// Using .NET Core's built-in DI container
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Register services with different lifetimes
.AddSingleton<ILogger, ConsoleLogger>();
services.AddScoped<IDatabase, PostgresDatabase>();
services.AddTransient<IEmailService, EmailService>();
services.AddTransient<UserService>();
services
// Register with factory
.AddSingleton<IConfiguration>(provider =>
services{
var config = new Configuration();
.Load("appsettings.json");
configreturn config;
});
// Register multiple implementations
.AddSingleton<IValidator, EmailValidator>();
services.AddSingleton<IValidator, PhoneValidator>();
services
// Conditional registration
.AddScoped<IDatabase>(provider =>
services{
var config = provider.GetRequiredService<IConfiguration>();
return config.GetValue("UsePostgres")
? new PostgresDatabase()
: new MySqlDatabase();
});
}
}
// Service with dependencies automatically injected
public class UserController
{
private readonly UserService userService;
private readonly ILogger logger;
// Constructor injection handled by DI container
public UserController(UserService userService, ILogger logger)
{
this.userService = userService;
this.logger = logger;
}
public void CreateUser(string email, string name)
{
.Log("Controller: Creating user");
logger.CreateUser(email, name);
userService}
}
// Program entry point
class Program
{
static void Main()
{
// Build service container
var services = new ServiceCollection();
var startup = new Startup();
.ConfigureServices(services);
startup
var serviceProvider = services.BuildServiceProvider();
// Resolve services
var controller = serviceProvider.GetRequiredService<UserController>();
.CreateUser("user@example.com", "Alice");
controller
// Using scope for scoped services
using (var scope = serviceProvider.CreateScope())
{
var database = scope.ServiceProvider.GetRequiredService<IDatabase>();
.Connect();
database}
}
}
// Custom simple DI container implementation
public class SimpleDIContainer
{
private readonly Dictionary<Type, Func<object>> services = new();
private readonly Dictionary<Type, object> singletons = new();
public void Register<TInterface, TImplementation>(bool singleton = false)
: TInterface, new()
where TImplementation {
[typeof(TInterface)] = () => Activator.CreateInstance<TImplementation>();
services
if (singleton)
{
[typeof(TInterface)] = null;
singletons}
}
public void Register<T>(Func<T> factory, bool singleton = false)
{
[typeof(T)] = () => factory();
services
if (singleton)
{
[typeof(T)] = null;
singletons}
}
public T Resolve<T>()
{
var type = typeof(T);
if (singletons.ContainsKey(type))
{
if (singletons[type] == null)
{
[type] = services[type]();
singletons}
return (T)singletons[type];
}
if (services.ContainsKey(type))
{
return (T)services[type]();
}
throw new InvalidOperationException($"Service {type.Name} not registered");
}
}
# Using dependency-injector library
from dependency_injector import containers, providers
# Define container with providers
class Container(containers.DeclarativeContainer):
# Configuration
= providers.Configuration()
config
# Singleton - same instance always
= providers.Singleton(
logger
ConsoleLogger
)
# Factory - new instance each time
= providers.Factory(
database
PostgresDatabase,=config.db.connection_string
connection_string
)
# Singleton with dependency
= providers.Singleton(
email_service
EmailServiceImpl,=logger
logger
)
# Service with multiple dependencies
= providers.Factory(
user_service
UserService,=logger,
logger=email_service
email_service
)
# Usage
= Container()
container "postgresql://localhost/db")
container.config.db.connection_string.from_value(
= container.user_service()
user_service "user@example.com", "Alice")
user_service.create_user(
# Simple DIY container implementation
class SimpleDIContainer:
def __init__(self):
self._services = {}
self._singletons = {}
def register(self, key: str, factory: Callable, singleton: bool = False):
"""Register a service with the container"""
self._services[key] = factory
if singleton:
self._singletons[key] = None
def resolve(self, key: str) -> Any:
"""Resolve a service from the container"""
if key in self._singletons:
if self._singletons[key] is None:
self._singletons[key] = self._services[key](self)
return self._singletons[key]
if key not in self._services:
raise KeyError(f"Service '{key}' not registered")
return self._services[key](self)
def __getitem__(self, key: str) -> Any:
"""Allow dictionary-like access"""
return self.resolve(key)
# Usage of simple container
= SimpleDIContainer()
container
# Register services
"logger", lambda c: ConsoleLogger(), singleton=True)
container.register("database", lambda c: PostgresDatabase())
container.register("email_service",
container.register(lambda c: EmailServiceImpl(c["logger"]),
=True
singleton
)"user_service",
container.register(lambda c: UserService(c["logger"], c["email_service"])
)
# Resolve and use
= container["user_service"]
user_service "test@test.com", "Bob")
user_service.create_user(
# Using Python's built-in typing and dataclasses for DI
from dataclasses import dataclass
from typing import Protocol
@dataclass
class AppConfig:
"""Configuration container"""
str = "localhost"
db_host: int = 5432
db_port: str = "INFO"
log_level:
@dataclass
class ServiceRegistry:
"""Manual service registry using dataclass"""
config: AppConfig
logger: Logger
database: Database
email_service: EmailService
@classmethod
def create_default(cls) -> 'ServiceRegistry':
"""Factory method to create with default services"""
= AppConfig()
config = ConsoleLogger()
logger = PostgresDatabase()
database = EmailServiceImpl(logger)
email_service
return cls(
=config,
config=logger,
logger=database,
database=email_service
email_service
)
# Usage
= ServiceRegistry.create_default()
registry = UserService(registry.logger, registry.email_service) user_service
7.7 Advanced Patterns
7.7.1 Service Locator Pattern
// Service Locator - Anti-pattern but sometimes useful
class ServiceLocator {
private static services = new Map<string, any>();
static register(key: string, service: any): void {
this.services.set(key, service);
}
static get<T>(key: string): T {
const service = this.services.get(key);
if (!service) {
throw new Error(`Service ${key} not found`);
}return service;
}
}
// Usage - less explicit than constructor injection
class ProductService {
private logger = ServiceLocator.get<ILogger>("logger");
private database = ServiceLocator.get<IDatabase>("database");
createProduct(name: string): void {
this.logger.log(`Creating product: ${name}`);
this.database.query("INSERT INTO products...");
} }
// Service Locator Pattern
public class ServiceLocator
{
private static readonly Dictionary<Type, object> services = new();
public static void Register<T>(T service)
{
[typeof(T)] = service;
services}
public static T Get<T>()
{
if (services.TryGetValue(typeof(T), out var service))
{
return (T)service;
}
throw new InvalidOperationException($"Service {typeof(T).Name} not found");
}
}
// Usage
public class ProductService
{
private readonly ILogger logger;
private readonly IDatabase database;
public ProductService()
{
// Service locator hides dependencies
= ServiceLocator.Get<ILogger>();
logger = ServiceLocator.Get<IDatabase>();
database }
public void CreateProduct(string name)
{
.Log($"Creating product: {name}");
logger.Query("INSERT INTO products...");
database}
}
# Service Locator Pattern
class ServiceLocator:
= {}
_services
@classmethod
def register(cls, key: str, service: Any) -> None:
= service
cls._services[key]
@classmethod
def get(cls, key: str) -> Any:
if key not in cls._services:
raise KeyError(f"Service '{key}' not found")
return cls._services[key]
# Usage - less explicit dependencies
class ProductService:
def __init__(self):
# Dependencies hidden in constructor
self.logger = ServiceLocator.get("logger")
self.database = ServiceLocator.get("database")
def create_product(self, name: str) -> None:
self.logger.log(f"Creating product: {name}")
self.database.query("INSERT INTO products...")
7.7.2 Factory Pattern with DI
// Factory with dependency injection
interface IServiceFactory {
createUserService(): UserService;
createProductService(): ProductService;
}
class ServiceFactory implements IServiceFactory {
constructor(
private logger: ILogger,
private database: IDatabase,
private emailService: IEmailService
) {}
createUserService(): UserService {
return new UserService(this.logger, this.emailService);
}
createProductService(): ProductService {
return new ProductService(this.logger, this.database);
}
}
// Usage
const factory = new ServiceFactory(
new ConsoleLogger(),
new DatabaseService(),
new EmailService(new ConsoleLogger())
;
)
const userService = factory.createUserService();
const productService = factory.createProductService();
// Factory with dependency injection
public interface IServiceFactory
{
CreateUserService();
UserService CreateProductService();
ProductService }
public class ServiceFactory : IServiceFactory
{
private readonly ILogger logger;
private readonly IDatabase database;
private readonly IEmailService emailService;
public ServiceFactory(
,
ILogger logger,
IDatabase database)
IEmailService emailService{
this.logger = logger;
this.database = database;
this.emailService = emailService;
}
public UserService CreateUserService()
{
return new UserService(logger, emailService);
}
public ProductService CreateProductService()
{
return new ProductService(logger, database);
}
}
# Factory with dependency injection
class ServiceFactory:
def __init__(
self,
logger: Logger,
database: Database,
email_service: EmailService
):self.logger = logger
self.database = database
self.email_service = email_service
def create_user_service(self) -> UserService:
return UserService(self.logger, self.email_service)
def create_product_service(self) -> 'ProductService':
return ProductService(self.logger, self.database)
# Usage
= ServiceFactory(
factory
ConsoleLogger(),
PostgresDatabase(),
EmailServiceImpl(ConsoleLogger())
)
= factory.create_user_service()
user_service = factory.create_product_service() product_service
7.8 Testing with Dependency Injection
DI makes testing easier by allowing mock/stub injection:
// Mock implementations for testing
class MockLogger implements ILogger {
public messages: string[] = [];
log(message: string): void {
this.messages.push(message);
}
}
class MockEmailService implements IEmailService {
public sentEmails: Array<{to: string, subject: string}> = [];
sendEmail(to: string, subject: string, body: string): void {
this.sentEmails.push({to, subject});
}
}
// Unit test with mocks
describe("UserService", () => {
it("should create user and send email", () => {
// Arrange
const mockLogger = new MockLogger();
const mockEmailService = new MockEmailService();
const userService = new UserService(mockLogger, mockEmailService);
// Act
.createUser("test@test.com", "Test User");
userService
// Assert
expect(mockLogger.messages).toContain("Creating user: Test User");
expect(mockEmailService.sentEmails).toHaveLength(1);
expect(mockEmailService.sentEmails[0].to).toBe("test@test.com");
;
}); })
// Using Moq framework for mocking
[TestClass]
public class UserServiceTests
{
[TestMethod]
public void CreateUser_ShouldLogAndSendEmail()
{
// Arrange
var mockLogger = new Mock<ILogger>();
var mockEmailService = new Mock<IEmailService>();
var userService = new UserService(
.Object,
mockLogger.Object
mockEmailService);
// Act
.CreateUser("test@test.com", "Test User");
userService
// Assert
.Verify(
mockLogger=> x.Log(It.Is<string>(s => s.Contains("Test User"))),
x .Once
Times);
.Verify(
mockEmailService=> x.SendEmail("test@test.com", It.IsAny<string>(), It.IsAny<string>()),
x .Once
Times);
}
}
// Manual mock implementation
public class MockLogger : ILogger
{
public List<string> Messages { get; } = new List<string>();
public void Log(string message)
{
.Add(message);
Messages}
}
import unittest
from unittest.mock import Mock, MagicMock
class TestUserService(unittest.TestCase):
def test_create_user_logs_and_sends_email(self):
# Arrange
= Mock(spec=Logger)
mock_logger = Mock(spec=EmailService)
mock_email_service
= UserService(mock_logger, mock_email_service)
user_service
# Act
"test@test.com", "Test User")
user_service.create_user(
# Assert
mock_logger.log.assert_called()= mock_logger.log.call_args_list
calls self.assertTrue(
any("Test User" in str(call) for call in calls)
)
mock_email_service.send_email.assert_called_once()= mock_email_service.send_email.call_args[0]
args self.assertEqual(args[0], "test@test.com")
# Manual mock implementation
class MockLogger:
def __init__(self):
self.messages = []
def log(self, message: str) -> None:
self.messages.append(message)
class MockEmailService:
def __init__(self):
self.sent_emails = []
def send_email(self, to: str, subject: str, body: str) -> None:
self.sent_emails.append({"to": to, "subject": subject})
# Test with manual mocks
def test_user_service_with_manual_mocks():
# Arrange
= MockLogger()
mock_logger = MockEmailService()
mock_email_service = UserService(mock_logger, mock_email_service)
user_service
# Act
"test@test.com", "Test User")
user_service.create_user(
# Assert
assert any("Test User" in msg for msg in mock_logger.messages)
assert len(mock_email_service.sent_emails) == 1
assert mock_email_service.sent_emails[0]["to"] == "test@test.com"
7.9 Best Practices
7.9.1 1. Prefer Constructor Injection
- Most explicit about dependencies
- Ensures required dependencies are provided
- Makes testing easier
7.9.2 2. Program to Interfaces
- Depend on abstractions, not concrete implementations
- Makes code more flexible and testable
7.9.3 3. Avoid Service Locator
- Hides dependencies
- Makes testing harder
- Prefer explicit dependency injection
7.9.4 4. Use DI Containers Wisely
- Don’t let the container leak into business logic
- Configure container at application root
- Keep registration logic centralized
7.9.5 5. Keep Constructors Simple
- Only assign dependencies
- Avoid complex logic in constructors
- Use factory methods for complex initialization
7.9.6 6. Manage Scope Appropriately
- Singleton: Shared state, thread-safe services
- Scoped: Per-request or per-operation
- Transient: Stateless, lightweight services
7.10 Summary
Dependency Injection is a powerful pattern that:
- Reduces coupling between classes
- Improves testability through easy mocking
- Increases flexibility by allowing runtime configuration
- Makes dependencies explicit and clear
- Supports SOLID principles, especially Dependency Inversion
Choose the injection method based on your needs:
- Constructor Injection: For required dependencies
- Property Injection: For optional dependencies
- Method Injection: For context-specific dependencies
- DI Container: For large applications with complex dependency graphs