JavaRush /Курсы /Spring Core /Первый проект

Первый проект

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

1. Нужен честный baseline

Если после разговора про Spring Core сразу перейти к контейнеру, легко получить ощущение, что фреймворк решает какую-то абстрактную, чуть ли не академическую проблему. Чтобы этого не произошло, нужен честный baseline: маленькое приложение без Spring, где руками видно, кто создаёт объекты, кто что получает в конструктор и где вообще живёт код запуска 🛕.

Это важный методический момент 🗿. Spring лучше объяснять не через фразу «контейнер делает удобно», а через контраст. Пока не увидишь обычный Java-код, который приходится собирать вручную, слова вроде wiring, object graph и ApplicationContext звучат слишком абстрактно. А как только видишь plain Java версию, становится понятно, почему тема сборки приложения заслуживает отдельного инженерного разговора 🔧.

Поэтому у нас появляется сквозной проект курса — ContextFlow: Order Processing, Notifications & Audit. Не как «большой продукт», а как учебный полигон. Мы не строим интернет-магазин на сто сущностей, не уходим в web, не подключаем базу и не притворяемся, что в первый день нам нужен мир уровня production. Нам нужен сценарий, в котором естественно есть несколько связанных объектов и несколько побочных эффектов. Этого уже достаточно, чтобы показать, откуда растёт боль 💣.

2. Первый кусочек будущего проекта

Сегодняшний срез ContextFlow нарочно небольшой. Есть сценарий «создать заказ». Чтобы он сработал, нужно хотя бы сохранить заказ, отправить уведомление и записать аудит. Уже на этом масштабе появляются разные роли: кто-то отвечает за хранение, кто-то — за уведомление, кто-то — за фиксацию действия. А значит, появляются зависимости и вместе с ними — вопрос сборки.

Такой домен хорош по двум причинам. Во-первых, он понятный: заказ, уведомление и аудит не требуют отдельной лекции по предметной области. Во-вторых, он естественно растёт. Стоит захотеть отмену заказа, отчёт или другой канал уведомлений — и граф объектов начнёт усложняться без всякой искусственной драматургии. То есть сама тема курса будет проявляться через естественный рост приложения, а не через специально выдуманные сложности.

Сценарий можно представить очень просто:

sequenceDiagram
    participant R as ScenarioRunner
    participant S as OrderPlacementService
    participant St as OrderStore
    participant N as NotificationSender
    participant A as AuditWriter

    R->>S: placeOrder(command)
    S->>St: save(order)
    S->>N: send("order created")
    S->>A: write("order created")
    S-->>R: orderId

Здесь пока нет ничего про Spring 🪄, и это именно то, что нам нужно. Есть обычный use-case сервис, несколько зависимых объектов и точка запуска, которая должна всё это собрать. Если дальше держать в голове именно этот срез, все следующие разговоры про контейнер будут восприниматься уже не как абстракция, а как развитие конкретной ситуации.

3. Plain Java версия проекта

Давайте посмотрим на самый обычный старт приложения. Есть main, есть несколько зависимостей, есть сервис сценария и есть ScenarioRunner, который этот сценарий запускает. Пока всё собирается вручную, безо всякой магии и без Spring.

public class ContextFlowApp {
    public static void main(String[] args) {
        // Вручную создаём инфраструктурные зависимости (выбор конкретных реализаций).
        OrderStore store = new InMemoryOrderStore(); // Хранилище заказов: пока in-memory
        NotificationSender sender = new ConsoleNotificationSender(); // Канал уведомлений: пока консоль
        AuditWriter audit = new ConsoleAuditWriter(); // Аудит: тоже пишем в консоль

        // Вручную собираем use-case сервис: явно прокидываем зависимости через конструктор.
        OrderPlacementService placement = new OrderPlacementService(store, sender, audit);

        // Вручную создаём точку запуска сценария (bootstrap): кто-то должен «стартануть» use-case.
        ScenarioRunner runner = new ScenarioRunner(placement);
        runner.run(); // Запускаем сценарий (например, «создать заказ»)
    }
}

У такого кода есть важное достоинство: он очень честный. Ничего не скрыто. Сразу видно, что OrderPlacementService не может работать без store, sender и audit. Для учебного текста это особенно полезно, потому что студент видит реальную форму зависимости, а не достраивает её по описанию.

Сам сервис тоже выглядит как обычный Java-класс. Не особенный, не завязанный на фреймворк и не «помеченный магией». Он просто использует то, что ему передали.

public class OrderPlacementService {
    // Явные зависимости сервиса: без них use-case не может выполняться.
    private final OrderStore store; // отвечает за сохранение заказа
    private final NotificationSender sender; // отвечает за отправку уведомления
    private final AuditWriter audit; // отвечает за запись аудита

    public OrderPlacementService(
            OrderStore store,
            NotificationSender sender,
            AuditWriter audit
    ) {
        // Сохраняем зависимости — дальше сервис будет использовать их в бизнес-методах.
        this.store = store;
        this.sender = sender;
        this.audit = audit;
    }

    public String placeOrder(CreateOrderCommand command) {
        // Генерируем идентификатор заказа (упрощённо, просто для примера).
        String orderId = "ORD-" + System.currentTimeMillis();

        // Сохраняем доменный объект в хранилище.
        store.save(new Order(orderId, command.customerName()));

        // Побочный эффект №1: отправляем уведомление.
        sender.send("Order created: " + orderId);

        // Побочный эффект №2: пишем в аудит.
        audit.write("Created order: " + orderId);

        // Возвращаем идентификатор — результат сценария.
        return orderId;
    }
}

Пока это читается спокойно 🤯. Даже приятно. Класс выглядит нормально, зависимости явные, код запуска короткий. На таком масштабе проекта ручная сборка не кажется проблемой — и в этом честность первого уровня. Spring не нужно объявлять спасением человечества из-за одного конструктора. Но форма будущего роста уже видна: у одного сервиса три зависимости, и их кто-то должен выбрать, создать и связать 🔗.

4. Граф объектов на человеческом языке

Когда говорят «граф объектов», новичок легко слышит в этом что-то математически страшное 😅. На деле всё куда бытовее. Это просто карта того, какие объекты существуют в приложении и кто на кого опирается. В нашем примере граф пока небольшой, но он уже есть: ContextFlowApp создаёт OrderPlacementService, а тот зависит от OrderStore, NotificationSender и AuditWriter.

flowchart TD
    App[ContextFlowApp.main] --> Runner[ScenarioRunner]
    App --> Placement[OrderPlacementService]
    Placement --> Store[OrderStore]
    Placement --> Notify[NotificationSender]
    Placement --> Audit[AuditWriter]

Пока граф маленький, им легко управлять глазами 👀. Но именно здесь рождается важный вопрос: кто отвечает за правила этой сборки? Кто решил, что NotificationSender будет консольным? Кто решил, что AuditWriter тоже консольный? Кто держит в голове, какой объект общий, а какой можно спокойно создавать отдельно? Эти решения уже существуют, просто пока их немного.

Ещё одно полезное слово здесь — bootstrap. Это код старта приложения, то место, где оно собирает себя перед выполнением первого сценария. В простом Java-мире bootstrap очень часто живёт прямо в main. И это нормально, пока приложение маленькое. Но как только в bootstrap начинают стекаться детали из разных уголков системы, он перестаёт быть «несколькими строчками запуска» и становится отдельной зоной заботы.

5. Код старта растёт быстрее, чем кажется

Самая коварная часть ручного связывания объектов (wiring) не в количестве new. Коварство в другом: код запуска начинает знать слишком много о внутреннем устройстве приложения. Допустим, вы добавили второй сценарий — отмену заказа. Теперь нужен ещё один сервис, и ему тоже требуются зависимости.

// Новый use-case сервис: отмена заказа (использует уже созданные зависимости).
OrderCancellationService cancellation = new OrderCancellationService(store, audit); // важно: тот же store/audit

// Ещё один сценарий: отчёты (ему нужен только store).
ReportingService reporting = new ReportingService(store); // снова важно: тот же store, чтобы видеть те же данные

// Точка запуска теперь знает про несколько сценариев и должна всё это связать.
ScenarioRunner runner = new ScenarioRunner(placement, cancellation, reporting);

В этот момент main уже знает, какие зависимости нужны placement, какие нужны cancellation, какие объекты должны быть общими, а какие — нет. И вот здесь начинается интересная инженерная развилка 🤔. На уровне одной новой строчки кажется, что ничего страшного не произошло. Но на уровне архитектуры код запуска постепенно превращается в место, где сосредоточено слишком много знания о приложении.

Есть ещё более тонкий момент 👀. Некоторые объекты недостаточно просто создать — их нужно создать правильно относительно друг друга. Например, если OrderPlacementService сохраняет заказ в одном InMemoryOrderStore, а OrderCancellationService ищет его уже в другом, приложение будет работать, но неправильно. То есть ручной wiring рождает не только длинный bootstrap, но и новый класс ошибок: ошибок сборки графа, которые не всегда проявляются явным исключением 🧨.

Из-за этого рост ручного wiring обманчив. Снаружи кажется, что вы просто добавляете ещё один сервис. Но внутри появляются новые правила согласования: какие объекты должны быть общими, где находится точка выбора реализации, какой объект должен жить весь runtime приложения, а какой можно создавать на каждый вызов. И именно эта невидимая часть потом растёт быстрее, чем сами бизнес-методы.

6. Ручное связывание не преступление, но у него есть потолок

На этом месте важно не сделать неверный вывод: «значит, ручная сборка всегда плохая» 🤬. Нет. Для маленькой утилиты, короткого скрипта или очень простого приложения ручной wiring может быть совершенно адекватным выбором. И честный курс по Spring должен это проговорить. Фреймворк не нужен там, где проблема ещё не возникла.

Но у ручного wiring есть вполне наблюдаемый потолок. Он упирается не в количество строк, а в количество решений о сборке, которые нужно удерживать в голове. Пока зависимость одна и сценарий один, всё терпимо. Когда сценариев несколько, реализаций становится больше одной, а поведение приложения начинает зависеть от конфигурации или режима запуска, сборка становится отдельной задачей.

Это можно увидеть в очень компактном контрасте:

Пока всё ещё спокойно Уже начинается инженерное напряжение
один use-case сервис несколько сценариев с общими объектами
одна реализация зависимости несколько реализаций одной роли
main знает пару new bootstrap знает внутреннюю структуру половины приложения
баги обычно видны сразу часть проблем — это ошибки самого wiring

Вот где появляется взрослая мысль 😱. Оказывается, у приложения есть не только бизнес-логика, но и логика собственной сборки. И пока эту логику разработчик держит вручную, она живёт либо в main, либо в самодельных фабриках, либо размазывается по коду. Spring появляется не там, где лениво писать new, а там, где сборка приложения уже стала отдельной инженерной задачей 🔧.

7. Нам нужен ContextFlow

Сквозной проект нужен не ради красивого названия. Он нужен, чтобы все последующие темы опирались на один и тот же предметный контекст. Если сегодня объяснять manual wiring на заказах, завтра lifecycle — на библиотеке книг, а послезавтра events — на банковском примере, студент тратит силы не на механику Spring, а на постоянную смену декораций.

ContextFlow хорош тем, что позволяет расти естественно. Сегодня нам хватило OrderPlacementService, OrderStore, NotificationSender, AuditWriter и ScenarioRunner. Чуть дальше проект спокойно выдержит несколько реализаций уведомлений, конфигурацию режимов, ресурсы с шаблонами, application events, post-processors и даже proxy-based поведение. При этом домен останется достаточно простым, чтобы не заслонить собой тему курса.

Именно поэтому сегодня мы не выгружаем весь каталог сущностей, сервисов и пакетов. Для первого дня это был бы не плюс, а перегруз. Пока достаточно увидеть одну живую бизнес-цепочку и заметить, как быстро вокруг неё возникает вопрос сборки. Следующий естественный шаг уже не про сам домен, а про качество этого plain Java кода: может ли он работать и при этом быть архитектурно неудобным? Ответ, к сожалению, да 😐. И именно здесь начинаются сильное связывание, скрытые зависимости и цена изменений.

8. Типичные ошибки 👺

Ошибка №1: объявить manual wiring злом сразу и навсегда.
Такой вывод слишком грубый. На крошечном коде ручная сборка может быть нормальной. Важно не запретить new, а понять, где число решений о сборке становится самостоятельной проблемой.

Ошибка №2: измерять рост сложности только количеством строк.
Иногда main всё ещё короткий, но уже знает слишком много о структуре приложения. Проблема ручной сборки чаще проявляется как рост связей и правил согласования, а не просто как длинный метод.

Ошибка №3: слишком рано раздувать домен.
Если на первом дне втащить платежи, доставку, пользователей, статусы, скидки и интеграции, тема курса растворится в предметной области. Нам нужен не «реальный бизнес целиком», а правильный срез, на котором хорошо видно проблему сборки.

Ошибка №4: думать, что боль начинается только тогда, когда приложение упало.
Одна из самых неприятных особенностей ручной сборки в том, что часть ошибок не валит программу сразу. Она просто делает её трудной для изменения, чтения и локальной проверки. И это часто опаснее явного падения 👍.

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