В чём разница между паттерном Service Locator и принципом IoC (Dependency Injection)?

«В чём разница между паттерном Service Locator и принципом IoC (Dependency Injection)?» — вопрос из категории Архитектура, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Service Locator и Inversion of Control (IoC) через Dependency Injection (DI) — это два подхода к управлению зависимостями, но с противоположными последствиями для качества кода.

Dependency Injection (Внедрение зависимостей)

  • Принцип: Зависимости класса явно объявляются (обычно через параметры конструктора) и предоставляются извне контейнером IoC.
  • Преимущества: Прозрачность зависимостей, простота модульного тестирования (можно передать mock/stub), соблюдение принципа инверсии зависимостей (DIP).
// Зависимости ЯВНЫЕ и обязательные
public class OrderProcessor
{
    private readonly IPaymentGateway _paymentGateway;
    private readonly IEmailService _emailService;

    // Зависимости инжектируются. Класс не знает, как их создать.
    public OrderProcessor(IPaymentGateway paymentGateway, IEmailService emailService)
    {
        _paymentGateway = paymentGateway ?? throw new ArgumentNullException(nameof(paymentGateway));
        _emailService = emailService ?? throw new ArgumentNullException(nameof(emailService));
    }

    public async Task ProcessOrder(Order order)
    {
        await _paymentGateway.ChargeAsync(order.Total);
        await _emailService.SendReceiptAsync(order);
    }
}

// Тестирование тривиально
var mockGateway = new Mock<IPaymentGateway>();
var mockEmail = new Mock<IEmailService>();
var processor = new OrderProcessor(mockGateway.Object, mockEmail.Object);
// ... assertions

Service Locator (Локатор служб) — антипаттерн

  • Принцип: Класс скрыто запрашивает свои зависимости из глобального или статического контейнера.
  • Недостатки: Скрытые зависимости, усложнённое тестирование (требуется настройка локатора перед каждым тестом), нарушение DIP, код становится жёстко связанным с конкретной реализацией локатора.
// Зависимости СКРЫТЫ и необязательные (на этапе компиляции)
public class OrderProcessor
{
    public OrderProcessor() // Конструктор пуст. Какие зависимости? Неясно.
    { }

    public async Task ProcessOrder(Order order)
    {
        // Магическое извлечение зависимостей. Класс зависит от глобального состояния.
        var paymentGateway = ServiceLocator.Current.GetService<IPaymentGateway>();
        var emailService = ServiceLocator.Current.GetService<IEmailService>();

        // Что, если локатор не сконфигурирован или сервис не зарегистрирован?
        // Runtime-ошибка, а не compile-time ошибка.
        await paymentGateway.ChargeAsync(order.Total);
        await emailService.SendReceiptAsync(order);
    }
}

// Тестирование становится кошмаром
[TestInitialize]
public void Setup()
{
    // Необходимо настроить глобальный статический локатор для каждого теста
    var mockGateway = new Mock<IPaymentGateway>();
    ServiceLocator.SetLocatorProvider(() => new MockServiceLocator(mockGateway.Object));
}
// Это хрупко и не масштабируется.

Итог

  • IoC/DI — это современный best practice, ведущий к чистой, тестируемой и поддерживаемой архитектуре.
  • Service Locator считается антипаттерном, так как маскирует истинные зависимости класса, делая систему менее предсказуемой и более сложной в сопровождении. Избегайте его в новом коде.