JavaRush /Курсы /Spring Core /Читаемый wiring в ContextF...

Читаемый wiring в ContextFlow

Spring Core
8 уровень , 4 лекция
Открыта

1. Идея читаемого wiring

Когда начинаешь работать со Spring, легко попасть в ловушку: кажется, что контейнер — это умная коробка, которая сама разрулит любую неоднозначность, а значит можно не думать о читаемости wiring. Но через пару недель (или через пару одногруппников, которые попробуют понять ваш код) выясняется, что «умная коробка» не заменяет архитектурные договорённости: где у нас default? где намеренный выбор? где реально нужен весь набор реализаций?

В ContextFlow это особенно хорошо видно на примере NotificationSender и DiscountPolicy. Пока у нас была одна реализация интерфейса, конструктор сервиса выглядел невинно: «мне нужен NotificationSender». Но как только появляются EmailNotificationSender, SmsNotificationSender, ConsoleNotificationSender, контейнер перестаёт угадывать — и правильно делает. И вот тут начинается взрослая часть: не просто «починить ошибку NoUniqueBeanDefinitionException», а сделать так, чтобы чтение конструктора говорило правду о намерениях класса.

Полезная мысль на сегодня звучит так: wiring — это тоже часть дизайна. Он должен читаться так же легко, как хороший метод calculateTotal(). Если wiring превращается в «магический набор аннотаций, который работает, но непонятно почему» — вы снова на пути к ручному new, только теперь с заклинаниями.

2. Модели зависимости: default, explicit, реестр

В Spring, когда у вас несколько реализаций интерфейса, на самом деле есть всего три здравых «формы запроса» к контейнеру. И эти формы можно буквально увидеть по сигнатуре конструктора. Это очень удобно: открываете класс — и уже по конструктору понимаете, какой контракт зависимости у этого класса, без чтения тела методов и без гадания «а что там контейнер выбрал?».

Давайте зафиксируем эти три модели в табличке, потому что мозгу новичка так проще (а мозгу преподавателя — спокойнее):

Что хочет класс Как выглядит зависимость Чем решаем в Spring Что это означает по смыслу
«Мне нужен один обычный вариант» NotificationSender sender @Primary / @Fallback (или сделать кандидата уникальным) Класс доверяет контейнеру выбрать «стандарт»
«Мне нужен именно вот этот вариант» @Qualifier("smsNotificationSender") NotificationSender sender @Qualifier в точке внедрения Выбор важен и должен быть виден в коде
«Мне нужен весь набор, я сам выберу» Map<String, NotificationSender> senders Collection injection (List/Set/Map) Класс — маршрутизатор/каталог/агрегатор стратегий

И теперь главное: эти модели можно смешивать в одном приложении (и нужно!). У вас вполне может быть один сервис, который пользуется «дефолтным» DiscountPolicy, другой сервис, которому нужен конкретный NoDiscountPolicy для расчёта «чистой цены», и третий, который хранит List<DiscountPolicy> как каталог доступных политик.

Чтобы это не превратилось в кашу, полезно держать в голове простое правило: если выбор повторяется в десяти местах, это уже не выбор — это default. Если выбор важен только в одном месте — делайте его явным через @Qualifier. Если сервис реально работает с несколькими реализациями, не притворяйтесь, что вам «нужен один bean»: просите коллекцию и называйте вещи своими именами.

Ниже мы соберём эту идею в одну «читаемую схему» на примерах ContextFlow.

3. Default-choice: @Primary и @Fallback

Когда классу честно нужен один обычный вариант, постоянные qualifier’ы только шумят. В ContextFlow default-choice закрывает две разные ситуации: у NotificationSender есть привычный канал по умолчанию, а у DiscountPolicy есть запасной NoDiscountPolicy, который не должен мешать более осмысленной политике.

import org.springframework.context.annotation.Fallback;
import org.springframework.stereotype.Component;

@Component("noDiscountPolicy")
@Fallback // Запасной вариант: используется только если нет других DiscountPolicy
public class NoDiscountPolicy implements DiscountPolicy {

    // Базовая стратегия: скидки нет, сумма не меняется
    public int apply(int total) { return total; }
}

На стороне NotificationSender та же идея выглядит как один @Primary-канал по умолчанию: сервисам не приходится повторять один и тот же выбор снова и снова.

А конструктор сервиса при этом остаётся честно простым:

import org.springframework.stereotype.Service;

@Service
public class OrderPricingService {
    private final DiscountPolicy discountPolicy;

    public OrderPricingService(DiscountPolicy discountPolicy) {
        // Класс просит один DiscountPolicy и доверяет контейнеру обычный выбор
        this.discountPolicy = discountPolicy;
    }
}

Такой конструктор читается нормально только если у типа есть ясная договорённость: один @Primary, один обычный кандидат поверх fallback-резерва или вообще единственная реализация. Здесь default-choice нужен не ради “магии”, а чтобы не тащить повторяющийся выбор в каждый сервис.

4. Explicit choice: @Qualifier

Как только default перестаёт отражать смысл класса, выбор должен стать явным. Здесь @Qualifier уже не про “починить контейнер”, а про то, чтобы конструктор сам говорил правду.

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

@Service
public class EmergencyNotificationService {
    private final NotificationSender sender;

    public EmergencyNotificationService(
            @Qualifier("smsNotificationSender") NotificationSender sender) {
        // Этот сервис по смыслу работает именно с SMS-каналом
        this.sender = sender;
    }
}

Такой код читается без догадок: сервис не просто “что-то отправляет”, а работает с конкретным каналом. Если же выбор относится не к смыслу сервиса, а к wiring-решению, тот же @Qualifier можно поставить на параметр @Bean-метода и оставить сам класс нейтральным. Главное различие простое: default скрывает рутину, qualifier показывает намеренный выбор.

5. Реестр стратегий: Map<String, T>

До этого NotificationDispatchService был удобен как учебный one-sender пример: на нём легко было увидеть, где контейнер спотыкается о несколько кандидатов. Как только выбор канала становится реальной задачей приложения, такой сервис уже лучше читать как маленький маршрутизатор.

Map<String, NotificationSender> здесь полезен именно как реестр стратегий. В ранней механической демонстрации можно было вычислять имя bean-а по конвенции. Для читаемого wiring лучше держать выбор канала явным в одном месте, а не надеяться, что строковая формула угадывается сама.

import java.util.Map;
import org.springframework.stereotype.Service;

@Service
public class NotificationDispatchService {
    private final Map<String, NotificationSender> senders;

    public NotificationDispatchService(Map<String, NotificationSender> senders) {
        // Spring сам соберёт Map: ключ = имя bean-а, значение = реализация интерфейса
        this.senders = senders;
    }

    public void send(NotificationChannel channel, String message) {
        // Выбор канала выражен явно и живёт в одном месте
        String beanName = switch (channel) {
            case EMAIL -> "emailNotificationSender";
            case SMS -> "smsNotificationSender";
            case CONSOLE -> "consoleNotificationSender";
        };

        NotificationSender sender = senders.get(beanName);
        if (sender == null) {
            throw new IllegalArgumentException("Unknown channel: " + channel);
        }

        sender.send(message);
    }
}

В таком виде NotificationDispatchService и становится нормальным рабочим вариантом для ContextFlow: остальные сервисы знают только про канал и сообщение, а детали выбора sender-а живут в одном месте.

6. Схема ContextFlow: default + explicit + map

Теперь картина не расползается: один и тот же проект использует все три режима wiring, но каждый — на своём месте.

  • OrderPricingService просит один DiscountPolicy и получает «основной» вариант, а NoDiscountPolicy остаётся fallback.
  • EmergencyNotificationService просит конкретный NotificationSender через @Qualifier, потому что смысл сервиса — SMS.
  • NotificationDispatchService просит все NotificationSender, потому что он маршрутизатор.

Если изобразить это как граф, получится очень понятная картинка:

flowchart TD
    OPS[OrderPricingService] --> DP[DiscountPolicy]
    DP --> LDP[LoyalCustomerDiscountPolicy]
    DP --> NDP["NoDiscountPolicy
@Fallback"] ENS[EmergencyNotificationService] --> SMS["SmsNotificationSender
@Qualifier"] NDS[NotificationDispatchService] --> MAP["Map<String, NotificationSender>"] MAP --> CNS["ConsoleNotificationSender
@Primary"] MAP --> EMS[EmailNotificationSender] MAP --> SMS

Теперь покажем маленький кусок use-case сервиса, который этим пользуется. Пусть OrderPlacementService не выбирает реализацию sender’а напрямую — он просто вызывает dispatch по каналу.

import org.springframework.stereotype.Service;

@Service
public class OrderPlacementService {
    private final NotificationDispatchService notificationDispatchService;

    public OrderPlacementService(NotificationDispatchService notificationDispatchService) {
        // Сервис не знает, какие реализации sender-ов есть внутри — он делегирует выбор маршрутизатору
        this.notificationDispatchService = notificationDispatchService;
    }
}

И где-то внутри сценария:

notificationDispatchService.send(NotificationChannel.CONSOLE, "Order created!")
CONSOLE: Order created!

С точки зрения читателя кода это выглядит очень честно. OrderPlacementService не знает про @Primary, не знает про имена bean-ов, не содержит new SmsNotificationSender(). Он просто использует сервис по его назначению. А вся «хитрость» выбора аккуратно живёт там, где ей и место: в wiring/маршрутизации.

Если вы поймаете себя на мысли, что хотите сделать так:

// Анти-паттерн: ручной выбор реализации в бизнес-коде вместо того, чтобы отдать это контейнеру/маршрутизатору
if (channel == NotificationChannel.SMS) {
    new SmsNotificationSender().send(message);
} else {
    new EmailNotificationSender().send(message);
}

то можно спокойно выдохнуть и сказать: «Ага, мы снова пытаемся откатиться в Дни 1–2». И вернуться к контейнеру.

7. Типичные ошибки в wiring

Ошибка №1: “Пофиксил неоднозначность через new внутри сервиса — и ладно”.
Это выглядит как быстрый ремонт, но на деле вы выносите решение из контейнера обратно в бизнес-код и создаёте скрытые зависимости. Через неделю вы захотите заменить SMS-отправителя на другой, и окажется, что нужно искать по проекту все new SmsNotificationSender(). Это тот же manual wiring, только в профиль.

Ошибка №2: “Давайте внедрим List<T>, а default возьмём как list.get(0)”.
Такой код иногда «случайно работает», пока порядок кандидатов не поменялся, пока не добавили новую реализацию, пока не обновили зависимости. Но это плохой контракт: вы делаете вид, что List — это «один bean», а не набор. Если нужен default, он должен быть объявлен через @Primary, а не через «кто первым прибежал, тот и прав».

Ошибка №3: “Я всё контролирую, поэтому @Qualifier поставлю везде”.
Обычно так начинается “qualifier-spam”: половина конструкторов превращается в повторяющиеся строки "consoleNotificationSender", "consoleNotificationSender", "consoleNotificationSender". Это не контроль, это шум. Если один и тот же выбор повторяется массово, это сигнал сделать default (@Primary) или вынести выбор в один специализированный сервис (например, маршрутизатор).

Ошибка №4: “Bean name — случайность, назову как-нибудь потом”.
Как только вы начинаете использовать Map<String, T>, имя bean-а перестаёт быть декоративной деталью. Оно становится ключом в вашей «таблице стратегий». Если вы называете бины абы как (или оставляете непонятные имена после рефакторинга), вы сами превращаете маршрутизацию в ребус. Имена вроде smsNotificationSender и loyalCustomerDiscountPolicy — это не «много букв», это документация.

Ошибка №5: “Маршрутизатор везде”: внедряем Map<String, NotificationSender> в каждый сервис.
Иногда после знакомства с Map хочется сделать так, чтобы каждый сервис сам выбирал реализацию по ключу. Это снова размазывает выбор по приложению. Гораздо чище, когда Map живёт в одном месте — в NotificationDispatchService — а остальные сервисы просто вызывают его, не зная деталей контейнерной кухни.

1
Задача
Spring Core, 8 уровень, 4 лекция
Недоступна
Default и explicit choice в одном приложении
Default и explicit choice в одном приложении
1
Задача
Spring Core, 8 уровень, 4 лекция
Недоступна
Default policy и реестр политик
Default policy и реестр политик
1
Опрос
Внедрение зависимостей, 8 уровень, 4 лекция
Недоступен
Внедрение зависимостей
Bean-ы и квалификаторы
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ