В чем разница между Dependency Injection (внедрение зависимостей) и Service Locator (локатор служб)?

Ответ

Оба подхода решают проблему создания зависимостей, но делают это принципиально по-разному, что влияет на тестируемость и прозрачность кода.

Dependency Injection (DI, Внедрение зависимостей): Зависимости явно передаются классу извне (через конструктор, метод или свойство). Класс ничего не знает о том, как создаются его зависимости.

// Зависимости передаются явно через конструктор.
public class UserService {
    private final UserRepository userRepository;
    private final EmailService emailService;

    // Зависимости ВНЕДРЯЮТСЯ. Код ясно показывает, что нужно для работы сервиса.
    public UserService(UserRepository userRepository, EmailService emailService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
    }

    public void registerUser(String email) {
        User user = new User(email);
        userRepository.save(user);
        emailService.sendWelcomeEmail(user);
    }
}
// Тестирование простое: передаем моки-заглушки.
UserRepository mockRepo = mock(UserRepository.class);
EmailService mockEmail = mock(EmailService.class);
UserService service = new UserService(mockRepo, mockEmail);

Service Locator (SL, Локатор служб): Класс сам запрашивает свои зависимости из некоего глобального контейнера или реестра.

public class UserService {
    // Зависимости скрыты. Непонятно, что нужно для работы класса, не заглядывая в метод.
    public void registerUser(String email) {
        // Класс активно обращается к глобальному локатору.
        UserRepository userRepository = ServiceLocator.getService(UserRepository.class);
        EmailService emailService = ServiceLocator.getService(EmailService.class);

        User user = new User(email);
        userRepository.save(user);
        emailService.sendWelcomeEmail(user);
    }
}
// Тестирование сложнее: нужно настроить глобальный ServiceLocator, чтобы он возвращал моки.

Ключевая проблема Service Locator:

  1. Скрытые зависимости. Невозможно понять зависимости класса, глядя только на его публичный интерфейс (сигнатуру конструктора/методов). Нужно анализировать весь код.
  2. Усложнение тестирования. Тест зависит от глобального состояния (локатора), который нужно правильно настроить и очистить.
  3. Нарушение принципа инверсии зависимостей (DIP). Класс зависит от конкретной реализации локатора.

Вывод: DI является предпочтительным паттерном, так как он делает зависимости явными, код — более понятным, тестируемым и соответствующим принципам SOLID. Service Locator считается антипаттерном в контексте управления зависимостями, хотя может быть полезен в некоторых специфических сценариях (например, для разрешения зависимостей внутри фабрик).

Ответ 18+ 🔞

А, слушай, вот тебе наглядная разница, как два подхода к одной задаче, а результат — просто небо и земля. Один — прозрачный, как слеза младенца, другой — тёмный лес, где зависимости прячутся, как маньяк в кустах.

Dependency Injection (DI, или Внедрение зависимостей, ёпта): Тут всё честно, по-пацански. Классу прямо в морду, извини, в конструктор, суют всё, что ему нужно для жизни. Он не парится, откуда это взялось, он просто пользуется. Прозрачность — овердохуища.

// Смотри, как всё на виду. Читаешь конструктор — и сразу ясно, без кодана, что этому сервису нужно.
public class UserService {
    private final UserRepository userRepo;
    private final EmailService emailService;

    // Всё принесли, положили на стол. Никаких секретов.
    public UserService(UserRepository userRepo, EmailService emailService) {
        this.userRepo = userRepo;
        this.emailService = emailService;
    }

    public void registerUser(String email) {
        User user = new User(email);
        userRepo.save(user);
        emailService.sendWelcomeEmail(user);
    }
}
// Тестирование? Да похуй! Суёшь ему заглушки (моки) и тестируешь, доверия ебать ноль к окружению не нужно.
UserRepository mockRepo = mock(UserRepository.class);
EmailService mockEmail = mock(EmailService.class);
UserService service = new UserService(mockRepo, mockEmail); // Всё под контролем.

Service Locator (SL, он же Локатор служб, он же хитрая жопа): А вот это уже пиздопроебибна. Класс сам, как голодный кот, шарится по глобальному холодильнику и ищет, чего бы сожрать. Зависимости спрятаны, как вор в тумане.

public class UserService {
    // Смотришь на класс — нихуя не понятно. Что ему надо? Хуй его знает.
    public void registerUser(String email) {
        // Ага, вот он, подвох! Он лезет в какой-то глобальный ящик и выуживает оттуда сервисы.
        UserRepository userRepo = ServiceLocator.getService(UserRepository.class);
        EmailService emailService = ServiceLocator.getService(EmailService.class);

        User user = new User(email);
        userRepo.save(user);
        emailService.sendWelcomeEmail(user);
    }
}
// Тестировать это? Да это же ёперный театр! Надо этот глобальный ServiceLocator настроить, чтобы он в тестах отдавал моки, а потом не забыть всё откатить. Один сплошной геморрой.

В чём, собственно, засада с Service Locator:

  1. Зависимости — хуй с горы. Не посмотришь в код — нихуя не поймёшь, от чего класс пляшет. С DI — открыл конструктор и всё ясно.
  2. Тесты превращаются в ад. Теперь твой тест зависит от этой глобальной штуки, её состояния. Настроил не так — получил неожиданный пиздец. С DI ты просто передал мок и спишь спокойно.
  3. Принципы SOLID плачут кровавыми слезами. Класс начинает зависеть от конкретной глобальной сущности (локатора), а это уже не круто.

Итог, чувак: DI — это как честный разговор: всё на столе, всё ясно, всё тестируется без танцев с бубном. А Service Locator — это как общение с чиновником: непонятно, куда идти, что подписывать, и в итоге тебя ещё и обманут. В 99% случаев бери DI и не парься. SL — это для каких-то совсем экзотических случаев, когда других вариантов реально нет, а так — лучше обойти эту мудню стороной.