JavaRush /Курсы /Spring Core /Главные anti-patterns Spring Core

Главные anti-patterns Spring Core

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

1. Anti-pattern’ы в Spring Core

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

Важно правильно прочувствовать, зачем этот разговор вообще нужен. В конце курса у вас уже есть рабочая mental model контейнера и рабочий ContextFlow, который демонстрирует хорошие практики. Если сейчас не проговорить «как оно ломается», вы очень быстро встретите ситуацию из реального мира: проект работает, релизы выходят, но любой небольшой рефакторинг вызывает эффект домино — и в какой-то момент команда начинает бояться трогать код. Это не драматизация: это буквально стандартная судьба проекта, где Spring превратился в магию.

Чтобы было проще ориентироваться, давайте зафиксируем небольшую «карту местности» anti-pattern’ов в виде таблицы. Это не список «всё запретить», а набор сигналов: если вы видите симптом, вы знаете, куда смотреть.

Anti-pattern Как выглядит в коде Почему плохо Здоровая альтернатива
Annotation soup класс оброс аннотациями, но непонятна его роль аннотации начинают скрывать дизайн вместо того, чтобы его выражать разделить роли: service отдельно, config отдельно, listeners отдельно
Hidden dependencies зависимости спрятаны в полях/статике/`getBean()` невозможно понять контракт класса и сложно тестировать constructor injection + явный wiring
Service locator / static context бизнес-код ищет bean сам это DI наоборот: IoC ломается контейнер выбирает зависимости на старте, а не во время сценария
Mutable singleton singleton-bean хранит изменяемое состояние баги «из прошлого», flaky tests, нелогичное поведение stateless services + краткоживущие объекты/сессии
Overused SpEL конфигурация превращается в ребус в строках читать и дебажить сложно, ошибки late-runtime properties + обычный Java-код конфигурации
Event soup основной сценарий спрятан в цепочке listeners поток выполнения становится неявным events только для side effects, orchestration — прямым кодом
Framework leakage домен и application слой «говорят на Spring» бизнес-логика становится зависимой от контейнера Spring-специфику держим в config/support

2. Annotation soup

Annotation soup обычно начинается невинно: «ну я добавлю ещё одну аннотацию, чтобы заработало». Потом ещё одну. Потом ещё. И однажды вы открываете класс — а он выглядит так, будто его наряжали всей командой на корпоративе: каждый повесил по игрушке. Проблема не в количестве аннотаций как таковом, а в том, что роль класса перестаёт быть ясной.

Самый неприятный эффект здесь в том, что аннотации начинают подменять архитектуру. Вместо того чтобы честно разделить обязанности (use-case сервис, конфигурация, инфраструктура, listener, аспект), мы делаем одного «универсального солдата» и пытаемся им закрыть все дырки. В ContextFlow мы как раз строили структуру domain / application / infrastructure / support / config, чтобы этого не случилось.

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

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;

@Service // класс одновременно объявлен как service (use-case слой?)
@Configuration // ...и как конфигурация (wiring слой?) — роли смешаны
@Profile("demo") // ...и ещё привязан к профилю, что делает поведение неочевидным
public class OrderPlacementService {
    // Пустой класс тут намеренно: проблема именно в смешении ролей аннотациями.
}

Проблема даже не в том, что Spring «не любит» такую комбинацию. Проблема в вас через две недели: вы будете открывать класс и думать, почему use-case сервис вдруг стал конфигурацией и почему он живёт только в профиле demo. В итоге вместо объяснимой схемы получится «ну оно так исторически сложилось».

Здоровая версия выглядит скучнее, но скука здесь — комплимент. Сервис остаётся сервисом, а профили и wiring живут в конфигурации:

import org.springframework.stereotype.Service;

@Service // Одна роль: use-case / application service
public class OrderPlacementService {
    // use-case orchestration, без трюков
}

А profile-specific решения — там, где им место: в config.profiles (или в @Bean-методах с @Profile). Такой подход делает проект читабельным: вы понимаете, что меняется при переключении профиля, и где именно это описано. И если очень хочется «повесить ещё одну аннотацию» — это хороший внутренний сигнал остановиться и спросить: «а не смешал ли я роли?».

3. Hidden dependencies

Скрытые зависимости — это как скрытая проводка в стене: пока вы не пытаетесь просверлить дырку, всё выглядит отлично. Но как только начинается рефакторинг, тестирование или просто перенос логики в другой модуль — вы вдруг обнаруживаете, что класс не может жить без контейнера, а его реальная форма «контракта» спрятана где-то внутри. Spring может подставить зависимости почти как угодно, но ваша задача — не прятать их от человека.

Самая классическая форма hidden dependencies — field injection. Мы уже обсуждали её раньше, а сейчас просто фиксируем как анти-паттерн, который очень легко «подцепить», потому что он короткий. Короткий — да. Хороший — нет.

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

@Service
public class NotificationDispatchService {

    @Autowired
    private NotificationSender sender; // Зависимость спрятана в поле: снаружи не видно контракта класса.
    // В тестах такой объект сложно создать "по-человечески" без Spring/рефлексии.
}

Почему это плохо ощущается на практике? Потому что по классу не видно, что ему нужно для работы. Вы не можете создать объект «как обычный Java-объект» в тесте без рефлексии, а IDE не помогает вам понять, какие зависимости обязательны. Плюс появляется соблазн добавлять новые зависимости «по тихому» — просто ещё одно поле, и готово.

Нормальный вариант для ContextFlow — constructor injection. Он делает зависимости частью публичного контракта класса:

import org.springframework.stereotype.Service;

@Service
public class NotificationDispatchService {

    private final NotificationSender sender; // Явно фиксируем обязательную зависимость.

    public NotificationDispatchService(NotificationSender sender) {
        // Конструктор показывает контракт: без sender этот сервис не работает.
        this.sender = sender;
    }
}

Да, тут больше строк. Но это те строки, которые экономят вам часы в будущем. И что приятно: Spring в таком стиле работает максимально предсказуемо. Если зависимости не хватает — вы падаете на старте (fail-fast), а не где-то в середине сценария, когда пользователь уже успел получить пол-результата.

4. Service locator и static context

Если constructor injection — это «класс получает готовые зависимости», то service locator — это «класс сам бегает и ищет, что ему нужно». То есть DI переворачивается назад, а IoC превращается в I-am-in-control (и да, это звучит как название токсичного трека). В Spring это обычно проявляется двумя способами: прямой applicationContext.getBean(...) в бизнес-коде или статический holder контекста, чтобы можно было дергать контейнер откуда угодно.

Вот типичный «не делайте так» пример из бизнес-класса:

public class OrderPlacementService {

    public void place(String message) {
        // Бизнес-код сам лезет в контейнер: зависимость не видна в конструкторе.
        NotificationSender sender =
                StaticContextHolder.getBean(NotificationSender.class);

        // Тестировать и читать такой код больно: нужен Spring-контекст даже для простого сценария.
        sender.send(message);
    }
}

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

Чтобы было совсем понятно, как возникает «статический доступ», вот минимальный пример holder-а (его легко написать за минуту, поэтому он так часто появляется в реальных проектах — и так же часто становится проблемой):

import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class StaticContextHolder implements ApplicationContextAware {
    // Глобальное состояние: один раз записали — и оно живёт "везде" и "всегда".
    private static ApplicationContext context;

    public void setApplicationContext(ApplicationContext c) {
        // Контекст устанавливается при инициализации Spring.
        // В тестах/нескольких контекстах это легко приводит к перезаписи и неожиданностям.
        context = c;
    }

    public static <T> T getBean(Class<T> type) {
        // Фактически это Service Locator: любой код может запросить зависимость "по месту".
        return context.getBean(type);
    }
}

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

Правильное лекарство обычно простое и скучное: вернуться к DI и сделать зависимость явной. Если нужно выбирать реализацию динамически, это тоже решается DI-инструментами (например, @Qualifier, @Primary, Map<String, T> injection), а не поиском по контейнеру во время выполнения.

5. Mutable singleton

Mutable singleton-bean — это когда сервис тихо запоминает что-то между вызовами. Иногда это делается «ради удобства», иногда — «ради кэша», а иногда просто потому, что в классе появился new ArrayList<>(), и никто не задал вопрос: «а сколько живёт этот объект?». В Spring по умолчанию большинство beans — singleton scope, то есть живут столько же, сколько живёт ApplicationContext. А это обычно весь запуск приложения.

Вот учебный пример буфера, который выглядит невинно, но легко превращается в ловушку:

import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service // Важно помнить: по умолчанию это singleton на весь ApplicationContext.
public class ReportBuffer {
    // Это изменяемое состояние, которое будет жить между вызовами и тестами.
    private final List<String> lines = new ArrayList<>();

    public void add(String line) {
        // Каждое добавление меняет состояние singleton-bean.
        lines.add(line);
    }

    public List<String> snapshot() {
        // Копию отдаём безопасно, но источник проблемы остаётся: состояние копится.
        return List.copyOf(lines);
    }
}

Если такой буфер внедрить в ReportingService и вызывать генерацию отчёта несколько раз за один запуск, вы можете получить «призраков прошлого»: второй отчёт содержит строки из первого. В тестах это проявляется ещё веселее: тест A что-то добавил, тест B внезапно увидел эти данные и упал, а вы потом час ищете «почему тесты flaky». Это то самое чувство, когда код «иногда работает», а вы начинаете подозревать фазу Луны.

Здоровый подход для ContextFlow обычно один из двух. Либо состояние живёт в method-local структурах, то есть создаётся на каждый вызов:

import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class ReportingService {

    public List<String> buildReportLines() {
        // Состояние создаётся на каждый вызов: между вызовами ничего не "залипает".
        List<String> lines = new ArrayList<>();
        lines.add("HEADER");
        return List.copyOf(lines);
    }
}

Либо, если вам нужен объект-сессия (мы это уже проходили в теме scopes), делаете короткоживущую сессию и получаете её через provider, а не храните состояние в singleton-сервисе:

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;

@Service
public class ReportingService {

    // Provider позволяет каждый раз получать новый экземпляр "сессии" генерации.
    private final ObjectProvider<ReportGenerationSession> sessions;

    public ReportingService(ObjectProvider<ReportGenerationSession> sessions) {
        this.sessions = sessions;
    }

    public void generateDailyReport() {
        // Важная мысль: состояние лежит внутри ReportGenerationSession, а не в singleton-сервисе.
        sessions.getObject().run();
    }
}

Смысл один: singleton-сервисы в application.service по умолчанию должны быть stateless, иначе проект становится непредсказуемым. И да, даже в non-web приложении. «Но у нас же один поток!» — звучит убедительно ровно до тех пор, пока вы не начнёте запускать несколько сценариев подряд или параллельно гонять тесты.

6. Overused SpEL

SpEL (Spring Expression Language) — полезная штука в небольших дозах. Как перец чили: иногда делает блюдо лучше, но если вы высыпали туда половину банки, вы уже не едите суп, вы проходите испытание. Anti-pattern начинается там, где SpEL используется как «язык программирования внутри строк», и важные решения становятся нечитаемыми и плохо тестируемыми.

Вот пример, который выглядит «умно», но на практике быстро превращается в головоломку:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class ReportOutputSettings {

    // Логика вычисления пути спрятана в строке:
    // смешаны system properties, placeholders и конкатенация.
    @Value("#{systemProperties['user.home'] + '/' + '${contextflow.app-name}' + '/reports'}")
    private String outputDir;
}

Почему это плохо? Во-первых, вы смешали сразу несколько источников: system properties и placeholders. Во-вторых, вы спрятали логику вычисления пути в строку, которую IDE почти не рефакторит и не проверяет. В-третьих, ошибки проявятся поздно: строка может быть некорректной, ключ может отсутствовать, user.home может быть неожиданным — и вы узнаете об этом в рантайме.

Для ContextFlow здоровая альтернатива обычно выглядит так: пусть properties хранят значение, а преобразование и нормализация делается в обычном Java-коде конфигурации. Например, вы храните contextflow.report.output-dir как строку, а в конфиге превращаете в Path:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.nio.file.Path;

@Configuration
public class ReportingPathsConfig {

    @Bean
    Path reportOutputDir(@Value("${contextflow.report.output-dir}") String dir) {
        // Вся "логика" теперь в обычном коде:
        // легко дебажить, тестировать и рефакторить.
        return Path.of(dir);
    }
}

Это читается, дебажится, тестируется и нормально рефакторится. И главное: вы не создаёте «магическую строку», смысл которой понятен только автору (и то первые два дня). SpEL остаётся инструментом «точечно помочь», а не способом спрятать половину конфигурационной логики в аннотации.

7. Event soup

Application events — классная штука, но у них есть одна суперспособность: ими очень легко злоупотребить. Event soup возникает, когда вы начинаете делать событиями всё подряд, и в какой-то момент основной сценарий приложения перестаёт быть читаемым без обхода десяти listeners. Это выглядит как «гибкая архитектура», а ощущается как «мне страшно нажимать Run без отладчика».

Чтобы прочувствовать разницу, полезно увидеть две схемы. Сначала — здоровая модель для ContextFlow, где use-case сервис делает основную работу, а события — только для side effects:

flowchart TD
    A[OrderPlacementService.placeOrder] --> B[OrderStore.save]
    A --> C[OrderPricingService.calculate]
    A --> D[publish OrderCreatedEvent]
    D --> E[Audit listener]
    D --> F[Notification listener]
    D --> G[Statistics listener]

А теперь — event soup, где use-case сервис превращается в «почтовый ящик», а логика расползается по listeners:

flowchart TD
    A[OrderPlacementService.placeOrder] --> D[publish CreateOrderCommand event]
    D --> B["Listener #1: save order"]
    D --> C["Listener #2: calculate price"]
    D --> E["Listener #3: audit"]
    D --> F["Listener #4: notify"]

На бумаге «всё модульно». В жизни — вы потеряли точку, где сценарий действительно контролируется. Особенно неприятно становится, когда кто-то добавляет ещё один listener, меняет @Order, добавляет condition, и всё начинает зависеть от порядка и условий.

Вот карикатурный, но очень узнаваемый код, который ведёт в event soup:

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

@Service
public class OrderPlacementService {

    private final ApplicationEventPublisher publisher;

    public OrderPlacementService(ApplicationEventPublisher publisher) {
        // Зависимость на publisher сама по себе нормальная.
        this.publisher = publisher;
    }

    public void place(CreateOrderCommand cmd) {
        // Плохой сигнал: публикуем "команду" как событие и ждём, что listeners сделают основную работу.
        publisher.publishEvent(cmd); // "пусть кто-нибудь сделает заказ"
    }
}

Сервис больше не «place order». Он «попросить вселенную сделать order». А кто именно сделает, как именно, и что будет, если один listener упадёт — всё это перестаёт быть очевидным.

Здоровая версия остаётся простой: сервис выполняет обязательные шаги (id, store, pricing, статус), а потом публикует событие как сигнал «заказ создан» для побочных эффектов:

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

@Service
public class OrderPlacementService {

    private final ApplicationEventPublisher publisher;

    public OrderPlacementService(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void place(String orderId) {
        // Обязательная orchestration здесь (save, price, status...)
        // Событие — уведомление о факте: заказ создан.
        publisher.publishEvent(new OrderCreatedEvent(orderId));
    }
}

Правило, которое хорошо работает в голове: event — это “уведомление о факте”, а не “просьба выполнить основную работу”. Это удерживает архитектуру от превращения в кашу.

8. Framework leakage

Framework leakage — это когда бизнес-код и доменная часть начинают зависеть от Spring-специфики сильнее, чем от собственных контрактов. Обычно это проявляется не сразу, а как серия «маленьких удобств»: тут Environment прочитаю, тут ApplicationContext подержу, тут Aware реализую «для диагностики», а потом внезапно выясняется, что без контейнера вы вообще не можете понять, как работает use-case.

Классический маркер leakage — Aware в application/service слое. Например, сервис внезапно начинает реализовывать EnvironmentAware:

import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;

@Service
public class NotificationDispatchService implements EnvironmentAware {

    // Spring-инфраструктура "протекает" в бизнес-сервис: появляется зависимость на Environment.
    private Environment env;

    public void setEnvironment(Environment environment) {
        // Теперь сервис потенциально может принимать runtime-решения на основе окружения.
        // Это делает поведение менее предсказуемым и усложняет тестирование.
        this.env = environment;
    }
}

Само по себе Spring это проглотит. Но архитектурно вы сделали так, что сервису нужен «контекст окружения», а значит он начинает принимать runtime-решения внутри бизнес-методов: какой канал уведомлений выбрать, куда писать, как форматировать… В итоге вы возвращаетесь к той же проблеме, от которой спасались профилями и конфигурацией: вариативность «внутри кода» вместо вариативности «в сборке контейнера».

В ContextFlow мы специально фиксировали дисциплину: бизнес-сервисы не реализуют Aware, а если нужен diagnostic bean — он живёт отдельно в support.diagnostics. И если сервису нужна настройка, она приходит как обычная зависимость или типизированное значение, а не как «пульт управления контейнером».

Очень похожий leakage — попытка лечить self-invocation проблему AOP «трюками» вместо рефакторинга, например через доступ к прокси изнутри. Мы это уже обсуждали в AOP-части: технически можно, но как привычка это превращается в «магический Spring-стиль», который сложно сопровождать. Сильный базовый принцип курса сохраняется: сначала исправляем дизайн, потом применяем механики Spring, а не наоборот.

9. Диагностика anti-pattern’ов

Когда вы видите проект впервые (или свой же код через месяц), вам нужно быстро понять: тут Spring используется как инженерный инструмент или как набор заклинаний. Хорошая новость: anti-pattern’ы обычно оставляют заметные следы. Плохая новость: мозг быстро привыкает к «ну оно же работает». Поэтому полезно иметь несколько простых вопросов, которые вы задаёте коду.

Первый вопрос звучит почти по-детски: «Если я открою класс, я пойму, что ему нужно для работы?» Если зависимости не видны в конструкторе, если внутри getBean(), если где-то статический holder — вы на территории hidden dependencies. Второй вопрос: «Могу ли я прочитать основной сценарий создания заказа в одном месте?» Если нет, и вы вынуждены прыгать по listeners, аспектам и post-processors, чтобы понять, что реально происходит, — пахнет event soup или чрезмерной “магией”.

Третий вопрос: «Этот класс — про бизнес или про Spring?» Если это business, но он реализует Aware, дергает Environment, читает ресурсы напрямую, хранит ссылки на контекст — это leakage. Четвёртый вопрос: «Где лежит вариативность?» Если она описана в конфигурации (profiles, разные beans) — хорошо. Если вариативность в if (profile == ...) внутри сервиса — вы снова сделали manual wiring, просто в более дорогой обёртке.

Чтобы закрепить, вот короткая табличка-диагност:

Сигнал в коде Вероятный anti-pattern Что обычно сделать
@Autowired на поле hidden dependencies перевести на constructor injection
ApplicationContext.getBean() в сервисе service locator передать зависимость через DI, использовать @Qualifier/Map
статический context static context access удалить holder, перестроить wiring
List/Map в singleton-сервисе как состояние mutable singleton перенести состояние в method-local или session/prototype
сложный @Value("#{...}") overused SpEL вынести в properties + Java config
события «делают всё» event soup вернуть orchestration в сервис, events оставить для side effects
Aware в application/domain framework leakage изолировать в support.*, сервисам дать обычные зависимости

Если вы держите эту таблицу в голове, code review становится проще: вы не спорите «нравится/не нравится», вы обсуждаете наблюдаемые признаки и последствия. Это особенно полезно для junior-уровня: вместо вкусовщины появляется понятная инженерная логика.

10. Типичные ошибки при работе со Spring Core

В конце курса легко попасть в ловушку: увидеть anti-pattern, кивнуть «ага, понял», и через неделю повторить его в своём проекте, потому что «так быстрее». Поэтому полезно зафиксировать несколько типичных ошибок именно в формате поведения, а не терминов. Речь не про то, что «так делать нельзя», а про то, что так делать почти всегда заканчивается одинаково: код становится непрозрачным, а исправления — дорогими.

Ошибка №1: воспринимать anti-pattern как “редкий случай”, который бывает только у плохих разработчиков.
На практике anti-pattern’ы появляются у нормальных людей под давлением времени: «срочно надо демо», «не успеваем», «потом поправим». Проблема в том, что “потом” часто не наступает, а код уже стал частью системы. Лечится это привычкой задавать себе два вопроса: видно ли зависимости и читается ли основной сценарий без прыжков.

Ошибка №2: лечить архитектурный запах ещё одной аннотацией.
Если класс непонятен, добавление @Lazy, @DependsOn, @Order, condition и прочих украшений редко делает его понятнее. Обычно это сигнал, что роль класса размыта или границы ответственности поплыли. Починка часто скучная: разделить класс на два, вынести wiring в конфигурацию, убрать “универсального солдата”.

Ошибка №3: “разрешить себе” service locator, потому что “так иногда можно”.
Да, есть случаи, когда ObjectProvider уместен в инфраструктуре. Но как только вы разрешили getBean() в бизнес-коде, вы получаете утечку стиля на весь проект. Через месяц ApplicationContext начнёт появляться везде “для удобства”, а DI превратится в декорацию. Лучше держать границу жёстко: business code зависимости не ищет, он их получает.

Ошибка №4: хранить состояние в singleton-bean, потому что “мы же просто аккуратно почистим список”.
Это одна из самых частых причин странных багов и flaky tests. “Аккуратно почистим” обычно превращается в “забыли почистить в одном из ранних return”. В ContextFlow мы сознательно показывали: state живёт либо в краткоживущих объектах (session/prototype), либо в локальных переменных метода. Singleton-сервисы — stateless, как швейцарские банки (по крайней мере в теории).

Ошибка №5: превращать events в главный способ писать бизнес-логику.
События нужны, чтобы увести side effects и уменьшить связность. Но если вы сделали event-ами “создание заказа” как процесс, а не как факт, вы потеряли orchestration и сделали систему неявной. Внутри одного приложения это особенно обидно: вы буквально могли написать прямой код, но решили сделать себе квест.

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