← Tilbage til patterns

Dependency Injection Pattern

Injicerer dependencies udefra i stedet for at lade klasser oprette deres egne dependencies, hvilket fremmer loose coupling og testbarhed.

StructuralBegynder-venlig

Om Patterned

Dependency Injection (DI) er et fundamental design pattern og en form for Inversion of Control (IoC) der blev formaliseret i begyndelsen af 2000'erne, selvom konceptet har været brugt længere. Patterned fungerer ved at dependencies passes til en klasse udefra (typisk via constructor, setter methods eller interfaces) i stedet for at klassen selv instantierer sine dependencies. Dette er en cornerstone i moderne objekt-orienteret programmering og clean architecture, da det fremmer loose coupling, gør kode lettere at teste, og følger SOLID principles specielt Dependency Inversion Principle. I moderne PHP frameworks som Symfony, Laravel og Laminas er DI containers en integral del der automatisk resolver og injicerer dependencies baseret på type hints. Dependency Injection gør det muligt at skifte implementations, mock dependencies i tests, og konfigurere applikationer mere fleksibelt.

Key Points

  • Dependencies injiceres udefra, ikke oprettet internt
  • Tre typer: Constructor Injection, Setter Injection, Interface Injection
  • Fremmer loose coupling mellem komponenter
  • Følger Dependency Inversion Principle
  • Gør testing lettere ved at kunne inject mocks
  • DI Containers automatiserer dependency resolution
  • Dependencies er eksplicitte og synlige
  • Runtime fleksibilitet til at skifte implementations
  • Reducerer tight coupling til concrete classes
  • Constructor injection er preferred for required dependencies
  • Setter injection bruges for optional dependencies
  • PHP frameworks tilbyder autowiring capabilities

Kode Eksempel

<?php

declare(strict_types=1);

namespace App\Order;

use App\Payment\PaymentGatewayInterface;
use App\Notification\NotificationServiceInterface;
use App\Inventory\InventoryServiceInterface;
use Psr\Log\LoggerInterface;

/**
 * Payment Gateway Interface
 */
interface PaymentGatewayInterface
{
    public function charge(float $amount, string $currency): string;
}

/**
 * Notification Service Interface
 */
interface NotificationServiceInterface
{
    public function sendOrderConfirmation(string $email, string $orderId): void;
}

/**
 * Inventory Service Interface
 */
interface InventoryServiceInterface
{
    public function reserveItems(array $items): bool;
}

/**
 * Concrete Payment Gateway
 */
final readonly class StripePaymentGateway implements PaymentGatewayInterface
{
    public function __construct(
        private string $apiKey
    ) {}
    
    public function charge(float $amount, string $currency): string
    {
        // Stripe API call simulation
        echo "Charging {$amount} {$currency} via Stripe\n";
        return 'ch_' . bin2hex(random_bytes(12));
    }
}

/**
 * Concrete Notification Service
 */
final readonly class EmailNotificationService implements NotificationServiceInterface
{
    public function __construct(
        private string $fromEmail
    ) {}
    
    public function sendOrderConfirmation(string $email, string $orderId): void
    {
        echo "Sending order confirmation to {$email} for order {$orderId}\n";
        echo "From: {$this->fromEmail}\n";
    }
}

/**
 * Concrete Inventory Service
 */
final class InventoryService implements InventoryServiceInterface
{
    public function reserveItems(array $items): bool
    {
        echo "Reserving " . count($items) . " items in inventory\n";
        return true;
    }
}

/**
 * Simple Logger Implementation
 */
final class Logger implements LoggerInterface
{
    public function log($level, $message, array $context = []): void
    {
        echo "[{$level}] {$message}\n";
    }
    
    public function emergency($message, array $context = []): void { $this->log('EMERGENCY', $message, $context); }
    public function alert($message, array $context = []): void { $this->log('ALERT', $message, $context); }
    public function critical($message, array $context = []): void { $this->log('CRITICAL', $message, $context); }
    public function error($message, array $context = []): void { $this->log('ERROR', $message, $context); }
    public function warning($message, array $context = []): void { $this->log('WARNING', $message, $context); }
    public function notice($message, array $context = []): void { $this->log('NOTICE', $message, $context); }
    public function info($message, array $context = []): void { $this->log('INFO', $message, $context); }
    public function debug($message, array $context = []): void { $this->log('DEBUG', $message, $context); }
}

/**
 * OrderProcessor med Constructor Injection
 * Alle required dependencies injiceres via constructor
 */
final class OrderProcessor
{
    private ?LoggerInterface $logger = null;
    
    public function __construct(
        private readonly PaymentGatewayInterface $paymentGateway,
        private readonly NotificationServiceInterface $notificationService,
        private readonly InventoryServiceInterface $inventoryService
    ) {}
    
    /**
     * Setter Injection for optional dependencies
     */
    public function setLogger(LoggerInterface $logger): void
    {
        $this->logger = $logger;
    }
    
    public function processOrder(
        string $orderId,
        string $customerEmail,
        array $items,
        float $total
    ): bool {
        $this->logger?->info("Processing order {$orderId}");
        
        try {
            // Reserve inventory
            if (!$this->inventoryService->reserveItems($items)) {
                throw new \RuntimeException('Items not available');
            }
            
            // Process payment
            $transactionId = $this->paymentGateway->charge($total, 'DKK');
            $this->logger?->info("Payment successful: {$transactionId}");
            
            // Send confirmation
            $this->notificationService->sendOrderConfirmation(
                $customerEmail,
                $orderId
            );
            
            $this->logger?->info("Order {$orderId} completed successfully");
            
            return true;
        } catch (\Exception $e) {
            $this->logger?->error("Order {$orderId} failed: {$e->getMessage()}");
            return false;
        }
    }
}

/**
 * Simple DI Container
 */
final class Container
{
    private array $services = [];
    
    public function set(string $id, callable $factory): void
    {
        $this->services[$id] = $factory;
    }
    
    public function get(string $id): mixed
    {
        if (!isset($this->services[$id])) {
            throw new \RuntimeException("Service {$id} not found");
        }
        
        return $this->services[$id]($this);
    }
}

// Setup DI Container
$container = new Container();

// Register services
$container->set(PaymentGatewayInterface::class, fn() => 
    new StripePaymentGateway('sk_test_xxx')
);

$container->set(NotificationServiceInterface::class, fn() => 
    new EmailNotificationService('noreply@shop.dk')
);

$container->set(InventoryServiceInterface::class, fn() => 
    new InventoryService()
);

$container->set(LoggerInterface::class, fn() => 
    new Logger()
);

$container->set(OrderProcessor::class, fn($c) => {
    $processor = new OrderProcessor(
        $c->get(PaymentGatewayInterface::class),
        $c->get(NotificationServiceInterface::class),
        $c->get(InventoryServiceInterface::class)
    );
    
    // Inject optional logger
    $processor->setLogger($c->get(LoggerInterface::class));
    
    return $processor;
});

// Brug af Dependency Injection
echo "=== Order Processing with Dependency Injection ===\n\n";

$orderProcessor = $container->get(OrderProcessor::class);

$success = $orderProcessor->processOrder(
    orderId: 'ORD-12345',
    customerEmail: 'customer@example.com',
    items: ['product-1', 'product-2'],
    total: 599.00
);

echo "\nOrder processing " . ($success ? 'succeeded' : 'failed') . "\n";

Fordele

  • +Loose coupling - klasser afhænger af interfaces, ikke concrete classes
  • +Testbarhed - let at inject mocks i unit tests
  • +Flexibility - skift implementations uden at ændre consuming code
  • +Explicit dependencies - klart hvad en klasse har brug for
  • +Single Responsibility - klasser fokuserer på deres opgave, ikke dependency management
  • +Fremmer SOLID principles specielt Dependency Inversion

Ulemper

  • Øget kompleksitet for simple applikationer
  • DI containers kan være magic og svære at debugge
  • Flere interfaces og abstractions skal vedligeholdes
  • Learning curve for nye udviklere

Hvornår bruges det?

  • Service classes - inject repositories, APIs, external services
  • Testing - inject mocks og test doubles i stedet for real implementations
  • Configuration - inject forskellige implementations baseret på environment
  • Plugin systems - inject forskellige strategies eller behaviors
  • Framework integration - autowiring i Symfony, Laravel containers
  • Middleware pipelines - inject handlers dynamisk

Best Practices

  • Prefer constructor injection for required dependencies
  • Brug setter injection kun for optional dependencies
  • Inject interfaces, ikke concrete classes
  • Brug readonly properties i PHP 8+ for injected dependencies
  • Udnyt framework DI containers (Symfony, Laravel) i stedet for at bygge egen
  • Document dependencies tydeligt med type hints
  • Undgå Service Locator anti-pattern
  • Brug autowiring hvor muligt for at reducere configuration

Quick Info

Kategori
Structural
Sværhedsgrad
Begynder-venlig
Relaterede Patterns
  • Service Locator - alternativ approach (ofte betragtet som anti-pattern)
  • Factory Pattern - factories kan bruges til at oprette objekter med dependencies
  • Strategy Pattern - DI bruges ofte til at inject forskellige strategies