Основы ООП в Java
Глава 7. Принципы проектирования и хорошего кода
DRY, KISS, YAGNI
DRY: Не повторяйся
DRY (Don't Repeat Yourself — Не повторяйся) — это принцип, который требует избегать дублирования кода, логики или данных в программе. Если один и тот же фрагмент повторяется в нескольких местах, его нужно вынести в общий элемент, чтобы изменения в одном месте автоматически отражались везде.
Почему важно: Дублирование приводит к ошибкам — если нужно исправить логику, вы рискуете забыть обновить все копии. Это усложняет поддержку кода и увеличивает его объем.
Как применять в Java: Выносите общую логику в методы, классы или утилиты. Используйте наследование, композицию или библиотеки.
Пример нарушения DRY:
Два метода в классе Calculator, которые дублируют расчет площади:
Исправление: Выносите общую часть в метод:
Нюансы:
Не путайте с WET (We Enjoy Typing — Мы любим печатать) — саркастический термин для дублирующего кода.
В ООП: Используйте абстрактные классы или интерфейсы для общих шаблонов.
В Java: Константы в enum или static final для общих значений.
Ловушка: Избыточная абстракция — не выносите, если код используется редко (связано с YAGNI).
KISS: Делай просто
KISS (Keep It Simple, Stupid — Делай просто, глупец) — принцип, который призывает к простоте в дизайне кода, алгоритмах и архитектуре. Вместо сложных конструкций выбирайте простые решения, которые легко понять и поддерживать.
Почему важно: Сложный код трудно отлаживать, тестировать и расширять. Простота снижает ошибки и ускоряет разработку.
Как применять в Java: Избегайте ненужных абстракций, используйте встроенные средства языка, пишите короткие методы.
Пример нарушения KISS:
Сложный расчет с лишними классами:
Нюансы:
"Глупец" в аббревиатуре — напоминание, что простота побеждает "умные" хаки.
В ООП: Предпочитайте композицию над глубоким наследованием.
В Java: Используйте Stream API для простых операций, но не для всего.
Ловушка: Простота не значит примитивность — балансируйте с читаемостью.
#Java #для_новичков #beginner #OOP #DRY #KISS #YAGNI
Глава 7. Принципы проектирования и хорошего кода
DRY, KISS, YAGNI
DRY: Не повторяйся
DRY (Don't Repeat Yourself — Не повторяйся) — это принцип, который требует избегать дублирования кода, логики или данных в программе. Если один и тот же фрагмент повторяется в нескольких местах, его нужно вынести в общий элемент, чтобы изменения в одном месте автоматически отражались везде.
Почему важно: Дублирование приводит к ошибкам — если нужно исправить логику, вы рискуете забыть обновить все копии. Это усложняет поддержку кода и увеличивает его объем.
Как применять в Java: Выносите общую логику в методы, классы или утилиты. Используйте наследование, композицию или библиотеки.
Пример нарушения DRY:
Два метода в классе Calculator, которые дублируют расчет площади:
public class Calculator {
// Нарушение: Дублирование
public double circleArea(double radius) {
return 3.14159 * radius * radius;
}
public double circleVolume(double radius, double height) {
return 3.14159 * radius * radius * height; // Тот же расчет площади
}
}
Исправление: Выносите общую часть в метод:
public class Calculator {
// Правильно: Общий метод
private double calculateCircleBase(double radius) {
final double PI = 3.14159;
return PI * radius * radius;
}
public double circleArea(double radius) {
return calculateCircleBase(radius);
}
public double circleVolume(double radius, double height) {
return calculateCircleBase(radius) * height;
}
}
Нюансы:
Не путайте с WET (We Enjoy Typing — Мы любим печатать) — саркастический термин для дублирующего кода.
В ООП: Используйте абстрактные классы или интерфейсы для общих шаблонов.
В Java: Константы в enum или static final для общих значений.
Ловушка: Избыточная абстракция — не выносите, если код используется редко (связано с YAGNI).
KISS: Делай просто
KISS (Keep It Simple, Stupid — Делай просто, глупец) — принцип, который призывает к простоте в дизайне кода, алгоритмах и архитектуре. Вместо сложных конструкций выбирайте простые решения, которые легко понять и поддерживать.
Почему важно: Сложный код трудно отлаживать, тестировать и расширять. Простота снижает ошибки и ускоряет разработку.
Как применять в Java: Избегайте ненужных абстракций, используйте встроенные средства языка, пишите короткие методы.
Пример нарушения KISS:
Сложный расчет с лишними классами:
// Нарушение: Избыточная сложность
public class ComplexCalculator {
public double calculate(double a, double b) {
AdvancedMathUtils utils = new AdvancedMathUtils();
return utils.performAdvancedOps(new OperationContext(a, b)).getResult();
}
}
// Куча лишних классов...
Исправление: Простой метод:
javapublic class SimpleCalculator {
public double calculate(double a, double b) {
return Math.sqrt(a * a + b * b); // Прямо и ясно
}
}
Нюансы:
"Глупец" в аббревиатуре — напоминание, что простота побеждает "умные" хаки.
В ООП: Предпочитайте композицию над глубоким наследованием.
В Java: Используйте Stream API для простых операций, но не для всего.
Ловушка: Простота не значит примитивность — балансируйте с читаемостью.
#Java #для_новичков #beginner #OOP #DRY #KISS #YAGNI
👍3
YAGNI: Тебе это не понадобится
YAGNI (You Ain't Gonna Need It — Тебе это не понадобится) — принцип, который предупреждает против добавления функциональности заранее, на основе предположений о будущем. Реализуйте только то, что нужно сейчас, чтобы избежать переусложнения.
Почему важно: Предвидение будущего часто ошибочно, и лишний код увеличивает сложность, баги и время на поддержку.
Как применять в Java: Добавляйте код по мере необходимости, используйте рефакторинг для расширений.
Пример нарушения YAGNI:
Класс с "будущими" методами:
Нюансы:
Связано с принципом "You Are Not Gonna Need It" — фокус на MVP (минимально жизнеспособном продукте).
В ООП: Не создавайте абстрактные классы "на будущее" — реализуйте, когда нужно.
В Java: Избегайте over-engineering в дизайне (например, Factory для простых объектов).
Ловушка: YAGNI не значит игнорировать планирование — используйте TDD (тест-драйвен разработку) для роста.
Полезные советы для новичков
DRY: Ищите дубли — рефакторьте в методы или утилиты.
KISS: Читайте код через день — если не понятно, упростите.
YAGNI: Задавайте: "Нужно ли это прямо сейчас?"
В Java: Используйте IDE для рефакторинга (Extract Method для DRY).
Ресурсы: Книга "Чистый код" Роберта Мартина — классика по этим принципам.
#Java #для_новичков #beginner #OOP #DRY #KISS #YAGNI
YAGNI (You Ain't Gonna Need It — Тебе это не понадобится) — принцип, который предупреждает против добавления функциональности заранее, на основе предположений о будущем. Реализуйте только то, что нужно сейчас, чтобы избежать переусложнения.
Почему важно: Предвидение будущего часто ошибочно, и лишний код увеличивает сложность, баги и время на поддержку.
Как применять в Java: Добавляйте код по мере необходимости, используйте рефакторинг для расширений.
Пример нарушения YAGNI:
Класс с "будущими" методами:
// Нарушение: Лишние методы "на всякий случай"
public class User {
private String name;
public User(String name) {
this.name = name;
}
public void sendEmail() { /* Пока пусто */ }
public void sendSMS() { /* Пока пусто */ }
public void integrateWithAPI() { /* Пока пусто */ }
}
Исправление: Только необходимое:
javapublic class User {
private String name;
public User(String name) {
this.name = name;
}
// Только базовая функциональность
public String getName() {
return name;
}
}
Нюансы:
Связано с принципом "You Are Not Gonna Need It" — фокус на MVP (минимально жизнеспособном продукте).
В ООП: Не создавайте абстрактные классы "на будущее" — реализуйте, когда нужно.
В Java: Избегайте over-engineering в дизайне (например, Factory для простых объектов).
Ловушка: YAGNI не значит игнорировать планирование — используйте TDD (тест-драйвен разработку) для роста.
Полезные советы для новичков
DRY: Ищите дубли — рефакторьте в методы или утилиты.
KISS: Читайте код через день — если не понятно, упростите.
YAGNI: Задавайте: "Нужно ли это прямо сейчас?"
В Java: Используйте IDE для рефакторинга (Extract Method для DRY).
Ресурсы: Книга "Чистый код" Роберта Мартина — классика по этим принципам.
#Java #для_новичков #beginner #OOP #DRY #KISS #YAGNI
👍4🔥1
Реактивное программирование
Концепции реактивного программирования: Push vs Pull — кто управляет данными
Сегодня разберём модели Push (отправку данных) и Pull (получение данных), почему одна из них идеальна для реактивного стиля, и как это решает проблемы из первого поста: от тяжёлых потоков до callback-ада.
Представьте: у вас есть конвейер на фабрике. В pull-модели рабочий сам подходит к предыдущему этапу и "берет" деталь, когда ему нужно. В push-модели предыдущий этап "отправляет" деталь дальше, как только она готова, не спрашивая.
Какая модель лучше? Зависит от сценария. В программировании это то же самое: pull подходит для предсказуемых, контролируемых потоков, а push — для динамичных, событийных, где данные приходят непредсказуемо (сеть, пользователи, сенсоры).
Реактивное программирование строится на push — и вот почему это революция.
Pull-модель: вы контролируете темп, но рискуете блокировками
В традиционном императивном программировании (когда код выполняется шаг за шагом, как рецепт) данные обычно "получаются" по требованию. Это pull-модель: потребитель (ваш код) сам запрашивает данные, когда готов их обработать.
Пример — чтение из итератора в Java:
Здесь вы контролируете процесс: hasNext() проверяет наличие, next() получает элемент. Это удобно для локальных, синхронных данных — вы знаете, сколько элементов, и ничего не ждёте. Но под капотом это синхронно: если данные в потоке (файл, сеть), next() может заблокироваться, как в старых потоках. Поток висит в ожидании, ресурсы тратятся впустую.
В асинхронных сценариях pull эволюционировал в Future или CompletableFuture: вы "запрашиваете" результат через get() или цепочки, но контроль остаётся у вас.
Проблема: если источник данных медленный (БД, API), ваш pull-запрос блокирует или создаёт callback-ад. Под нагрузкой — тысячи pull-запросов — система не масштабируется, потому что каждый требует ресурса (потока). Это как толпа в очереди: каждый тянет за своим, но если касса одна, все стоят.
Ещё минус: pull не справляется с "лишними" данными. Если источник генерирует 1 млн событий в секунду, а вы получаете по одному — либо перегрузка буфера, либо вы не успеваете. Нет встроенного механизма, чтобы сказать "замедлись".
#Java #middle #Reactor #Push #Flux
Концепции реактивного программирования: Push vs Pull — кто управляет данными
Сегодня разберём модели Push (отправку данных) и Pull (получение данных), почему одна из них идеальна для реактивного стиля, и как это решает проблемы из первого поста: от тяжёлых потоков до callback-ада.
Представьте: у вас есть конвейер на фабрике. В pull-модели рабочий сам подходит к предыдущему этапу и "берет" деталь, когда ему нужно. В push-модели предыдущий этап "отправляет" деталь дальше, как только она готова, не спрашивая.
Какая модель лучше? Зависит от сценария. В программировании это то же самое: pull подходит для предсказуемых, контролируемых потоков, а push — для динамичных, событийных, где данные приходят непредсказуемо (сеть, пользователи, сенсоры).
Реактивное программирование строится на push — и вот почему это революция.
Pull-модель: вы контролируете темп, но рискуете блокировками
В традиционном императивном программировании (когда код выполняется шаг за шагом, как рецепт) данные обычно "получаются" по требованию. Это pull-модель: потребитель (ваш код) сам запрашивает данные, когда готов их обработать.
Пример — чтение из итератора в Java:
List<String> fruits = Arrays.asList("яблоко", "банан", "вишня");
Iterator<String> iterator = fruits.iterator();
while (iterator.hasNext()) {
String fruit = iterator.next(); // "Вытягиваем" следующий элемент
System.out.println(fruit.toUpperCase());
}
Здесь вы контролируете процесс: hasNext() проверяет наличие, next() получает элемент. Это удобно для локальных, синхронных данных — вы знаете, сколько элементов, и ничего не ждёте. Но под капотом это синхронно: если данные в потоке (файл, сеть), next() может заблокироваться, как в старых потоках. Поток висит в ожидании, ресурсы тратятся впустую.
В асинхронных сценариях pull эволюционировал в Future или CompletableFuture: вы "запрашиваете" результат через get() или цепочки, но контроль остаётся у вас.
Проблема: если источник данных медленный (БД, API), ваш pull-запрос блокирует или создаёт callback-ад. Под нагрузкой — тысячи pull-запросов — система не масштабируется, потому что каждый требует ресурса (потока). Это как толпа в очереди: каждый тянет за своим, но если касса одна, все стоят.
Ещё минус: pull не справляется с "лишними" данными. Если источник генерирует 1 млн событий в секунду, а вы получаете по одному — либо перегрузка буфера, либо вы не успеваете. Нет встроенного механизма, чтобы сказать "замедлись".
#Java #middle #Reactor #Push #Flux
👍3
Push-модель: данные приходят сами, реактивно и эффективно
Теперь изменим: в push-модели источник "отправляет" данные потребителю, как только они готовы, без запросов. Потребитель пассивен — он подписывается и реагирует.
Это основа реактивного программирования: события push'атся асинхронно, без блокировок. Контроль переходит к источнику, но с обратным давлением — подписчик может сказать "хватит на время".
В Reactive Streams это реализовано через Publisher и Subscriber: издатель толкает onNext(элемент), подписчик реагирует сразу.
Пример с Flux в Project Reactor (push-стиль):
Здесь Flux — издатель, который отправляет элементы по мере готовности. subscribe() — подписка, и элементы приходят автоматически: "ЯБЛОКО", "БАНАН"... Нет next() — нет pull.
Если источник асинхронный, например, чтение из сети:
Данные push'атся по мере поступления от сервера — без блокировок. Reactor использует неблокирующий IO (на базе Netty), так что поток не висит: один event-loop-цикл (цикл обработки событий) обслуживает тысячи подписок.
Почему push лучше для реактивности?
Во-первых, эффективность: нет лишних проверок hasNext().
Во-вторых, естественность для событий: клик мыши, сообщение в чате — это push по природе, они приходят сами.
В-третьих, масштабируемость: тысячи подписчиков на один издатель — ок, потому что push идёт через события, а не потоки на каждого.
Гибридные сценарии: когда mix работает
На практике модели смешиваются. В Reactor Flux может имитировать pull через операторы вроде buffer() или take(), но основа — push.
Пример: pull из локального списка, но push в сеть:
Здесь локальный pull (fromIterable) переходит в push (flatMap для API).
Это гибкость: используйте pull для контроля, push для асинхронности. Но важно избегать блокировок: Reactor проверяет и предупреждает, если в лямбде блокирующий код (onBlock()).
Ещё пример из реальной жизни: стриминг видео в Netflix. Pull — когда пользователь сам получает фреймы, но под нагрузкой лагает. Push — сервер отдает фреймы по мере готовности, с буферизацией. Реактивные библиотеки (как RxJava) позволяют строить такие конвейеры.
Почему Push — ключ к новому подходу в реактивном программировании
Возвращаясь к проблемам: потоки тяжёлые, потому что pull требует ожидания; Future блокирует на get(), потому что это pull в асинхронной обёртке; CompletableFuture даёт цепочки, но push-подход в нём слаб (колбэки — это мини-push, но без полного контроля).
Реактивный push меняет всё: данные текут как река, вы реагируете без ожидания, ресурсы на минимуме. Системы становятся resilient (устойчивыми): если один поток сломается, другие продолжают. Под нагрузкой — горизонтальное масштабирование без боли.
#Java #middle #Reactor #Push #Flux
Теперь изменим: в push-модели источник "отправляет" данные потребителю, как только они готовы, без запросов. Потребитель пассивен — он подписывается и реагирует.
Это основа реактивного программирования: события push'атся асинхронно, без блокировок. Контроль переходит к источнику, но с обратным давлением — подписчик может сказать "хватит на время".
В Reactive Streams это реализовано через Publisher и Subscriber: издатель толкает onNext(элемент), подписчик реагирует сразу.
Пример с Flux в Project Reactor (push-стиль):
Flux<String> pushFlux = Flux.fromIterable(Arrays.asList("яблоко", "банан", "вишня"))
.map(String::toUpperCase); // Преобразование в потоке
pushFlux.subscribe(
fruit -> System.out.println("Получено из push: " + fruit), // Реакция на толкание
Throwable::printStackTrace, // Обработка ошибок
() -> System.out.println("Push завершён")
);
Здесь Flux — издатель, который отправляет элементы по мере готовности. subscribe() — подписка, и элементы приходят автоматически: "ЯБЛОКО", "БАНАН"... Нет next() — нет pull.
Если источник асинхронный, например, чтение из сети:
WebClient.create()
.get()
.uri("https://api.example.com/fruits")
.retrieve()
.bodyToFlux(String.class) // Flux толкает строки из ответа
.subscribe(fruit -> System.out.println("Push из API: " + fruit));
Данные push'атся по мере поступления от сервера — без блокировок. Reactor использует неблокирующий IO (на базе Netty), так что поток не висит: один event-loop-цикл (цикл обработки событий) обслуживает тысячи подписок.
Почему push лучше для реактивности?
Во-первых, эффективность: нет лишних проверок hasNext().
Во-вторых, естественность для событий: клик мыши, сообщение в чате — это push по природе, они приходят сами.
В-третьих, масштабируемость: тысячи подписчиков на один издатель — ок, потому что push идёт через события, а не потоки на каждого.
Гибридные сценарии: когда mix работает
На практике модели смешиваются. В Reactor Flux может имитировать pull через операторы вроде buffer() или take(), но основа — push.
Пример: pull из локального списка, но push в сеть:
Flux.fromIterable(fruits)
.map(String::toUpperCase)
.flatMap(fruit -> sendToApi(fruit)) // flatMap толкает в асинхронный API
.subscribe(result -> System.out.println("Ответ: " + result));
Здесь локальный pull (fromIterable) переходит в push (flatMap для API).
Это гибкость: используйте pull для контроля, push для асинхронности. Но важно избегать блокировок: Reactor проверяет и предупреждает, если в лямбде блокирующий код (onBlock()).
Ещё пример из реальной жизни: стриминг видео в Netflix. Pull — когда пользователь сам получает фреймы, но под нагрузкой лагает. Push — сервер отдает фреймы по мере готовности, с буферизацией. Реактивные библиотеки (как RxJava) позволяют строить такие конвейеры.
Почему Push — ключ к новому подходу в реактивном программировании
Возвращаясь к проблемам: потоки тяжёлые, потому что pull требует ожидания; Future блокирует на get(), потому что это pull в асинхронной обёртке; CompletableFuture даёт цепочки, но push-подход в нём слаб (колбэки — это мини-push, но без полного контроля).
Реактивный push меняет всё: данные текут как река, вы реагируете без ожидания, ресурсы на минимуме. Системы становятся resilient (устойчивыми): если один поток сломается, другие продолжают. Под нагрузкой — горизонтальное масштабирование без боли.
#Java #middle #Reactor #Push #Flux
👍3
Реактивное программирование
Концепции реактивного программирования: Reactive Streams API — Publisher и Subscriber
Сегодня разберём Reactive Streams API — это спецификация, которая лежит в сердце реактивного программирования на Java. Она не просто набор интерфейсов, а рамка для построения асинхронных потоков данных с контролем над ними. Представьте это как правила дорожного движения для потока событий: без них — хаос и пробки, с ними — плавный поток.
Reactive Streams API решает ключевые проблемы из первого поста: вместо тяжёлых потоков и callback-ада, мы получаем унифицированный способ обмена данными между компонентами. Это не библиотека, а интерфейсы, которые реализуют фреймворки вроде Project Reactor или RxJava. Они обеспечивают совместимость: один компонент от Reactor может работать с другим от Akka Streams.
Что такое Reactive Streams API
Спецификация Reactive Streams появилась в 2015 году как ответ на хаос асинхронности. До неё каждый фреймворк изобретал велосипед: свои способы обработки потоков, ошибок и давления.
API стандартизирует это: четыре интерфейса (Publisher, Subscriber, Subscription, Processor), которые описывают, как данные текут асинхронно. Главная идея — неблокирующая коммуникация: ничего не ждём синхронно, всё через события.
Это подводит нас ближе к реактивному мышлению: вместо "запроси и жди" (как в Future.get()), мы "подпишись и реагируй". Под нагрузкой это экономит ресурсы: один поток (event-loop) может обслуживать тысячи подписок, без создания новых на каждую задачу. В итоге, системы становятся более отзывчивыми и масштабируемыми — идеально для микросервисов или реального времени.
Publisher: источник данных, который отправляет события
Publisher — это интерфейс для издателя, который генерирует поток данных.
Он как фабрика событий: производит элементы (любые объекты), ошибки или сигнал завершения. Publisher не знает, кто его слушает, — он просто готов отправлять (push) данные, когда они появятся.
Интерфейс прост: всего один метод subscribe(Subscriber s). Когда вы вызываете его, издатель регистрирует подписчика и начинает процесс. Но данные не льются сразу — всё под контролем (об этом ниже).
Пример простого издателя в Project Reactor (который реализует Reactive Streams):
Здесь publisher — источник.
Он может быть асинхронным: например, читать из сети или БД. Когда данные готовы, он отправляет их подписчику через onNext(элемент).
Если ошибка — onError(Throwable).
Если конец — onComplete().
Почему это лучше традиционных подходов? В отличие от потоков (где каждый запрос — отдельный тяжёлый объект), publisher лёгкий: он не выделяет ресурсы заранее, а реагирует на подписку. Нет блокировок: если данных нет, ничего не происходит.
#Java #middle #Reactor #Reactive_Streams_API #Processor #Subscription #Subscriber #Publisher
Концепции реактивного программирования: Reactive Streams API — Publisher и Subscriber
Сегодня разберём Reactive Streams API — это спецификация, которая лежит в сердце реактивного программирования на Java. Она не просто набор интерфейсов, а рамка для построения асинхронных потоков данных с контролем над ними. Представьте это как правила дорожного движения для потока событий: без них — хаос и пробки, с ними — плавный поток.
Reactive Streams API решает ключевые проблемы из первого поста: вместо тяжёлых потоков и callback-ада, мы получаем унифицированный способ обмена данными между компонентами. Это не библиотека, а интерфейсы, которые реализуют фреймворки вроде Project Reactor или RxJava. Они обеспечивают совместимость: один компонент от Reactor может работать с другим от Akka Streams.
Что такое Reactive Streams API
Спецификация Reactive Streams появилась в 2015 году как ответ на хаос асинхронности. До неё каждый фреймворк изобретал велосипед: свои способы обработки потоков, ошибок и давления.
API стандартизирует это: четыре интерфейса (Publisher, Subscriber, Subscription, Processor), которые описывают, как данные текут асинхронно. Главная идея — неблокирующая коммуникация: ничего не ждём синхронно, всё через события.
Это подводит нас ближе к реактивному мышлению: вместо "запроси и жди" (как в Future.get()), мы "подпишись и реагируй". Под нагрузкой это экономит ресурсы: один поток (event-loop) может обслуживать тысячи подписок, без создания новых на каждую задачу. В итоге, системы становятся более отзывчивыми и масштабируемыми — идеально для микросервисов или реального времени.
Publisher: источник данных, который отправляет события
Publisher — это интерфейс для издателя, который генерирует поток данных.
Он как фабрика событий: производит элементы (любые объекты), ошибки или сигнал завершения. Publisher не знает, кто его слушает, — он просто готов отправлять (push) данные, когда они появятся.
Интерфейс прост: всего один метод subscribe(Subscriber s). Когда вы вызываете его, издатель регистрирует подписчика и начинает процесс. Но данные не льются сразу — всё под контролем (об этом ниже).
Пример простого издателя в Project Reactor (который реализует Reactive Streams):
import reactor.core.publisher.Flux; // Flux реализует Publisher
Flux<Integer> publisher = Flux.range(1, 5); // Издатель: поток от 1 до 5
Здесь publisher — источник.
Он может быть асинхронным: например, читать из сети или БД. Когда данные готовы, он отправляет их подписчику через onNext(элемент).
Если ошибка — onError(Throwable).
Если конец — onComplete().
Почему это лучше традиционных подходов? В отличие от потоков (где каждый запрос — отдельный тяжёлый объект), publisher лёгкий: он не выделяет ресурсы заранее, а реагирует на подписку. Нет блокировок: если данных нет, ничего не происходит.
#Java #middle #Reactor #Reactive_Streams_API #Processor #Subscription #Subscriber #Publisher
👍1
Subscriber: получатель, который реагирует на события
Subscriber — интерфейс для подписчика, который "слушает" издателя. Он как потребитель на конвейере: получает элементы и решает, что с ними делать.
Методы:
- onSubscribe(Subscription s): вызывается сразу после подписки. Здесь подписчик получает подписку — объект для контроля.
- onNext(T item): срабатывает на каждый элемент. Здесь обработка: логика, трансформация и т.д.
- onError(Throwable t): если ошибка — обработка исключения.
- onComplete(): поток завершён успешно.
Подписчик пассивен: не получает данные (pull), а ждёт push. Это решает callback-ад из CompletableFuture — реакции в одном месте, код чище.
Пример подписки:
Здесь subscribe() связывает publisher и subscriber. Данные отправляются по одному, по запросу (request(1)). Это асинхронно: код после subscribe() продолжается сразу, без ожидания.
В Reactor есть BaseSubscriber для упрощения — переопределяйте только нужные методы.
Subscription: мост контроля с обратным давлением
Subscription — ключ к управлению: это объект, который подписчик получает в onSubscribe.
Методы:
- request(long n): "Дай мне n элементов". Это обратное давление (backpressure) — подписчик контролирует темп, чтобы не захлебнуться.
- cancel(): "Хватит, отпишись".
Без этого publisher мог бы зафлудить подписчика данными. Например, если источник — бесконечный поток (сенсоры), без request(n) — переполнение памяти.
В примере выше request(1) делает обработку последовательной: получил — обработал — запросил следующий. Для скорости — request(Long.MAX_VALUE) (неограниченно), но осторожно: рискуете буфером.
Это решает проблемы блокировок: вместо висящих потоков, всё в event-loop. Под нагрузкой — graceful degradation (грациозная деградация): если подписчик медленный, publisher замедляется, а не падает.
Processor: промежуточный звено для трансформаций
Processor — комбо: реализует и Publisher, и Subscriber. Он как фильтр в конвейере: принимает данные от одного издателя, обрабатывает и отправляет дальше.
Пример — в цепочках Flux: map() или filter() создают процессоры внутри.
В практике вы редко пишете свой Processor — библиотеки предоставляют готовые операторы. Но понимание помогает: весь конвейер — цепь publisher → processor → ... → subscriber.
Практические советы и подводные камни
- Всегда управляйте подпиской: без request() данные не потекут (по умолчанию unbounded — неограниченно, но лучше явно).
- Обрабатывайте ошибки: onError — ваш спасатель, чтобы не потерять исключения.
- Тестируйте с TestSubscriber (в Reactor): симулируйте сценарии.
- Камень: если в onNext блокирующий код — сломаете асинхронность. Используйте Schedulers для offload (перенос на другой поток).
В реальном коде: в Spring WebFlux контроллеры возвращают Flux/Mono — publisher'ы, клиенты подписываются.
#Java #middle #Reactor #Reactive_Streams_API #Processor #Subscription #Subscriber
Subscriber — интерфейс для подписчика, который "слушает" издателя. Он как потребитель на конвейере: получает элементы и решает, что с ними делать.
Методы:
- onSubscribe(Subscription s): вызывается сразу после подписки. Здесь подписчик получает подписку — объект для контроля.
- onNext(T item): срабатывает на каждый элемент. Здесь обработка: логика, трансформация и т.д.
- onError(Throwable t): если ошибка — обработка исключения.
- onComplete(): поток завершён успешно.
Подписчик пассивен: не получает данные (pull), а ждёт push. Это решает callback-ад из CompletableFuture — реакции в одном месте, код чище.
Пример подписки:
publisher.subscribe(new Subscriber<Integer>() {
private Subscription subscription;
@Override
public void onSubscribe(Subscription s) {
this.subscription = s;
s.request(1); // Запрашиваем первый элемент
}
@Override
public void onNext(Integer item) {
System.out.println("Получено: " + item);
subscription.request(1); // Запрашиваем следующий
}
@Override
public void onError(Throwable t) {
System.err.println("Ошибка: " + t);
}
@Override
public void onComplete() {
System.out.println("Завершено");
}
});
Здесь subscribe() связывает publisher и subscriber. Данные отправляются по одному, по запросу (request(1)). Это асинхронно: код после subscribe() продолжается сразу, без ожидания.
В Reactor есть BaseSubscriber для упрощения — переопределяйте только нужные методы.
Subscription: мост контроля с обратным давлением
Subscription — ключ к управлению: это объект, который подписчик получает в onSubscribe.
Методы:
- request(long n): "Дай мне n элементов". Это обратное давление (backpressure) — подписчик контролирует темп, чтобы не захлебнуться.
- cancel(): "Хватит, отпишись".
Без этого publisher мог бы зафлудить подписчика данными. Например, если источник — бесконечный поток (сенсоры), без request(n) — переполнение памяти.
В примере выше request(1) делает обработку последовательной: получил — обработал — запросил следующий. Для скорости — request(Long.MAX_VALUE) (неограниченно), но осторожно: рискуете буфером.
Это решает проблемы блокировок: вместо висящих потоков, всё в event-loop. Под нагрузкой — graceful degradation (грациозная деградация): если подписчик медленный, publisher замедляется, а не падает.
Processor: промежуточный звено для трансформаций
Processor — комбо: реализует и Publisher, и Subscriber. Он как фильтр в конвейере: принимает данные от одного издателя, обрабатывает и отправляет дальше.
Пример — в цепочках Flux: map() или filter() создают процессоры внутри.
В практике вы редко пишете свой Processor — библиотеки предоставляют готовые операторы. Но понимание помогает: весь конвейер — цепь publisher → processor → ... → subscriber.
Практические советы и подводные камни
- Всегда управляйте подпиской: без request() данные не потекут (по умолчанию unbounded — неограниченно, но лучше явно).
- Обрабатывайте ошибки: onError — ваш спасатель, чтобы не потерять исключения.
- Тестируйте с TestSubscriber (в Reactor): симулируйте сценарии.
- Камень: если в onNext блокирующий код — сломаете асинхронность. Используйте Schedulers для offload (перенос на другой поток).
В реальном коде: в Spring WebFlux контроллеры возвращают Flux/Mono — publisher'ы, клиенты подписываются.
#Java #middle #Reactor #Reactive_Streams_API #Processor #Subscription #Subscriber
👍1
Основы ООП в Java
Глава 7. Принципы проектирования и хорошего кода
Композиция vs Наследование
В объектно-ориентированном программировании композиция и наследование — это два способа организации кода для повторного использования и связи между классами. Они решают разные задачи и подходят для разных ситуаций.
Наследование:
Механизм, при котором класс (подкласс) наследует поля, методы и поведение от другого класса (суперкласса) с помощью ключевого слова extends. Это воплощает отношение "является" (is-a). Например, Dog является Animal.
Композиция:
Механизм, при котором класс содержит объекты других классов как поля, используя их функциональность. Это воплощает отношение "имеет" (has-a). Например, Car имеет Engine.
Оба подхода позволяют повторно использовать код, но их применение влияет на гибкость, читаемость и масштабируемость программы.
Наследование: Когда использовать и проблемы
Наследование удобно, когда классы имеют четкое отношение "является", и подкласс естественным образом расширяет поведение суперкласса.
Пример наследования:
Когда использовать:
Есть четкое is-a отношение: Dog — это Animal.
Нужно переопределить или расширить поведение (через override).
Полиморфизм: Работа с подклассами через ссылку на суперкласс (Animal[] animals).
Преимущества:
Простота: Код автоматически наследуется.
Полиморфизм: Легко реализовать через override.
Иерархия: Удобно для моделирования реальных сущностей (например, Животное → Млекопитающее → Собака).
Недостатки:
Жесткая связь: Подкласс привязан к суперклассу — изменения в суперклассе могут сломать подклассы.
Хрупкий базовый класс: Изменение метода в суперклассе может неожиданно повлиять на подклассы.
Ограничение Java: Только одиночное наследование (один extends).
Сложность в глубоких иерархиях: Много уровней наследования трудно поддерживать.
Нарушение инкапсуляции: Подклассы могут зависеть от внутренней реализации.
Пример проблемы: Если добавить метод в Animal, который не подходит для всех подклассов (например, fly()), подклассы вроде Dog окажутся в неудобном положении.
Композиция: Когда использовать и преимущества
Композиция — это включение объектов других классов как полей, чтобы использовать их функциональность. Это отношения, где класс владеет компонентами.
Пример композиции:
Использование:
Когда использовать:
Отношения: Car имеет Engine, а не является им.
Нужно гибкое сочетание функциональности без жесткой связи.
Требуется заменяемость компонентов (например, разные двигатели).
Преимущества:
Гибкость: Легко заменить компонент (например, Engine на ElectricEngine).
Слабая связь: Изменения в Engine не ломают Car, если интерфейс сохранен.
Инкапсуляция: Car скрывает детали Engine, управляя доступом.
Модульность: Компоненты можно использовать в других классах.
Тестирование: Легче подменять компоненты (mocking) для тестов.
#Java #для_новичков #beginner #OOP #Composition #Extends
Глава 7. Принципы проектирования и хорошего кода
Композиция vs Наследование
В объектно-ориентированном программировании композиция и наследование — это два способа организации кода для повторного использования и связи между классами. Они решают разные задачи и подходят для разных ситуаций.
Наследование:
Механизм, при котором класс (подкласс) наследует поля, методы и поведение от другого класса (суперкласса) с помощью ключевого слова extends. Это воплощает отношение "является" (is-a). Например, Dog является Animal.
Композиция:
Механизм, при котором класс содержит объекты других классов как поля, используя их функциональность. Это воплощает отношение "имеет" (has-a). Например, Car имеет Engine.
Оба подхода позволяют повторно использовать код, но их применение влияет на гибкость, читаемость и масштабируемость программы.
Наследование: Когда использовать и проблемы
Наследование удобно, когда классы имеют четкое отношение "является", и подкласс естественным образом расширяет поведение суперкласса.
Пример наследования:
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void makeSound() {
System.out.println(name + " издает звук.");
}
}
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void makeSound() {
System.out.println(name + " лает: Гав!");
}
}
Когда использовать:
Есть четкое is-a отношение: Dog — это Animal.
Нужно переопределить или расширить поведение (через override).
Полиморфизм: Работа с подклассами через ссылку на суперкласс (Animal[] animals).
Преимущества:
Простота: Код автоматически наследуется.
Полиморфизм: Легко реализовать через override.
Иерархия: Удобно для моделирования реальных сущностей (например, Животное → Млекопитающее → Собака).
Недостатки:
Жесткая связь: Подкласс привязан к суперклассу — изменения в суперклассе могут сломать подклассы.
Хрупкий базовый класс: Изменение метода в суперклассе может неожиданно повлиять на подклассы.
Ограничение Java: Только одиночное наследование (один extends).
Сложность в глубоких иерархиях: Много уровней наследования трудно поддерживать.
Нарушение инкапсуляции: Подклассы могут зависеть от внутренней реализации.
Пример проблемы: Если добавить метод в Animal, который не подходит для всех подклассов (например, fly()), подклассы вроде Dog окажутся в неудобном положении.
Композиция: Когда использовать и преимущества
Композиция — это включение объектов других классов как полей, чтобы использовать их функциональность. Это отношения, где класс владеет компонентами.
Пример композиции:
public class Engine {
private int horsepower;
public Engine(int horsepower) {
this.horsepower = horsepower;
}
public void start() {
System.out.println("Двигатель с " + horsepower + " л.с. запущен.");
}
}
public class Car {
private String model;
private Engine engine; // Композиция: Car "имеет" Engine
public Car(String model, Engine engine) {
this.model = model;
this.engine = engine;
}
public void drive() {
engine.start();
System.out.println(model + " едет.");
}
}
Использование:
public class Main {
public static void main(String[] args) {
Engine engine = new Engine(200);
Car car = new Car("Toyota", engine);
car.drive(); // Двигатель с 200 л.с. запущен. Toyota едет.
}
}
Когда использовать:
Отношения: Car имеет Engine, а не является им.
Нужно гибкое сочетание функциональности без жесткой связи.
Требуется заменяемость компонентов (например, разные двигатели).
Преимущества:
Гибкость: Легко заменить компонент (например, Engine на ElectricEngine).
Слабая связь: Изменения в Engine не ломают Car, если интерфейс сохранен.
Инкапсуляция: Car скрывает детали Engine, управляя доступом.
Модульность: Компоненты можно использовать в других классах.
Тестирование: Легче подменять компоненты (mocking) для тестов.
#Java #для_новичков #beginner #OOP #Composition #Extends
👍4
Недостатки:
Больше кода: Нужно явно создавать и передавать компоненты.
Сложнее полиморфизм: Требуются интерфейсы для единообразия.
Композиция vs Наследование: Как выбирать
Выбирайте композицию, если:
Нет четкого is-a отношения, а скорее has-a.
Нужна гибкость: Компоненты могут меняться (например, разные реализации Engine).
Хотите избежать жесткой связи или хрупкого базового класса.
Нужно реализовать поведение, которое не должно быть частью иерархии.
Выбирайте наследование, если:
Есть четкое is-a отношение, и подкласс логично расширяет суперкласс.
Нужен полиморфизм через ссылки на суперкласс.
Иерархия простая (1-2 уровня).
Пример: Композиция вместо наследования:
Наследование (проблема):
Все нюансы
Наследование:
Жесткая связь: Подкласс зависит от реализации суперкласса.
Проблема ромба: В Java избежана (одиночное наследование), но множественное через интерфейсы.
Нюанс: Переопределение может нарушить контракт (Liskov Substitution Principle).
Композиция:
Требует интерфейсов для полиморфизма (например, Movable).
Нюанс: Больше кода для передачи компонентов (конструктор, сеттеры).
Делегирование: Car вызывает методы Engine — это нормально.
Когда комбинировать:
Используйте наследование для базовой структуры, композицию для поведения.
Пример: extends AbstractVehicle, но Engine как поле.
Ошибки:
Наследование вместо композиции: Глубокие иерархии или неподходящие is-a.
Композиция без интерфейсов: Теряется полиморфизм.
Дизайн:
Композиция соответствует принципу KISS (меньше связей).
Наследование — YAGNI: Не создавайте иерархии "на будущее".
DRY: Оба подхода помогают повторно использовать код.
Java-специфика:
Интерфейсы + default методы часто заменяют наследование.
Dependency Injection (Spring) — пример композиции.
Как создать это в IntelliJ IDEA
Композиция: Создайте интерфейс (New → Interface), класс с полем и методами.
Наследование: New → Class, укажите extends, IDE поможет override.
Рефакторинг: Extract Interface для перехода от наследования к композиции.
Полезные советы для новичков
Композиция по умолчанию: Если сомневаетесь, начните с has-a.
Проверяйте is-a: Dog is-an Animal? Да. Car is-an Engine? Нет.
Интерфейсы для гибкости: Комбинируйте композицию с интерфейсами.
Избегайте глубоких иерархий: Не больше 2-3 уровней.
#Java #для_новичков #beginner #OOP #Composition #Extends
Больше кода: Нужно явно создавать и передавать компоненты.
Сложнее полиморфизм: Требуются интерфейсы для единообразия.
Композиция vs Наследование: Как выбирать
Выбирайте композицию, если:
Нет четкого is-a отношения, а скорее has-a.
Нужна гибкость: Компоненты могут меняться (например, разные реализации Engine).
Хотите избежать жесткой связи или хрупкого базового класса.
Нужно реализовать поведение, которое не должно быть частью иерархии.
Выбирайте наследование, если:
Есть четкое is-a отношение, и подкласс логично расширяет суперкласс.
Нужен полиморфизм через ссылки на суперкласс.
Иерархия простая (1-2 уровня).
Пример: Композиция вместо наследования:
Наследование (проблема):
public class Vehicle {
public void move() {
System.out.println("Движется с помощью двигателя.");
}
}
public class Car extends Vehicle {
// Проблема: А если электромобиль или велосипед?
}
Композиция (решение):
public interface Movable {
void move();
}
public class EngineMovable implements Movable {
@Override
public void move() {
System.out.println("Движется с помощью двигателя.");
}
}
public class Car {
private Movable movement;
public Car(Movable movement) {
this.movement = movement;
}
public void drive() {
movement.move();
}
}
Теперь Car может использовать любой Movable (двигатель, электромотор), без привязки к иерархии.
Все нюансы
Наследование:
Жесткая связь: Подкласс зависит от реализации суперкласса.
Проблема ромба: В Java избежана (одиночное наследование), но множественное через интерфейсы.
Нюанс: Переопределение может нарушить контракт (Liskov Substitution Principle).
Композиция:
Требует интерфейсов для полиморфизма (например, Movable).
Нюанс: Больше кода для передачи компонентов (конструктор, сеттеры).
Делегирование: Car вызывает методы Engine — это нормально.
Когда комбинировать:
Используйте наследование для базовой структуры, композицию для поведения.
Пример: extends AbstractVehicle, но Engine как поле.
Ошибки:
Наследование вместо композиции: Глубокие иерархии или неподходящие is-a.
Композиция без интерфейсов: Теряется полиморфизм.
Дизайн:
Композиция соответствует принципу KISS (меньше связей).
Наследование — YAGNI: Не создавайте иерархии "на будущее".
DRY: Оба подхода помогают повторно использовать код.
Java-специфика:
Интерфейсы + default методы часто заменяют наследование.
Dependency Injection (Spring) — пример композиции.
Как создать это в IntelliJ IDEA
Композиция: Создайте интерфейс (New → Interface), класс с полем и методами.
Наследование: New → Class, укажите extends, IDE поможет override.
Рефакторинг: Extract Interface для перехода от наследования к композиции.
Полезные советы для новичков
Композиция по умолчанию: Если сомневаетесь, начните с has-a.
Проверяйте is-a: Dog is-an Animal? Да. Car is-an Engine? Нет.
Интерфейсы для гибкости: Комбинируйте композицию с интерфейсами.
Избегайте глубоких иерархий: Не больше 2-3 уровней.
#Java #для_новичков #beginner #OOP #Composition #Extends
👍4
Реактивное программирование
Концепции реактивного программирования: Backpressure — что делать, если данных слишком много
Представьте: источник данных — как бурная река, а ваш подписчик — маленькая лодка. Если вода хлынет слишком быстро в лодку, она утонет.
Здесь на помощь приходит backpressure (обратное давление) — механизм, который позволяет подписчику контролировать скорость потока, говоря "дай мне столько, сколько я могу переварить". И это не просто тормоз, а умный регулятор, который предотвращает перегрузки и делает реактивные системы устойчивыми под любой нагрузкой.
Backpressure — ключевая фишка реактивного программирования, которая отличает его от традиционных подходов. В старых моделях (как потоки или Future) вы либо блокируете всё, либо тонете в очередях данных, рискуя исчерпать память или CPU. Здесь же контроль у потребителя: он решает, когда и сколько брать. Это решает проблемы callback-ада и блокировок, позволяя строить масштабируемые приложения — от мобильных до облачных кластеров.
Что такое backpressure и почему оно нужно?
Обратное давление — это способ, при котором получатель данных сигнализирует источнику: "замедлись, если я не успеваю". В реактивном мире данные передаются асинхронно, но без контроля это может привести к проблемам: если издатель генерирует 1 млн элементов в секунду, а подписчик обрабатывает только 100, буфер переполнится, и приложение упадёт с ошибкой "израсходована память" (OutOfMemoryError).
Backpressure вводит "обратную связь": подписчик запрашивает элементы порциями, а издатель выдаёт ровно столько.
Это встроено в Reactive Streams: в методе onSubscribe подписчик получает Subscription и использует request(long n) — "запроси n элементов". Если не запросить — данные не потекут. Это как шлюзы на реке: открываешь по мере нужды, избегая наводнения. В отличие от pull-модели (где вы тянете всё сразу), здесь баланс: push для динамики, но с контролем.
Пример базового использования в Flux (поток для множества элементов):
#Java #middle #Reactor #Reactive_Streams_API #backpressure
Концепции реактивного программирования: Backpressure — что делать, если данных слишком много
Представьте: источник данных — как бурная река, а ваш подписчик — маленькая лодка. Если вода хлынет слишком быстро в лодку, она утонет.
Здесь на помощь приходит backpressure (обратное давление) — механизм, который позволяет подписчику контролировать скорость потока, говоря "дай мне столько, сколько я могу переварить". И это не просто тормоз, а умный регулятор, который предотвращает перегрузки и делает реактивные системы устойчивыми под любой нагрузкой.
Backpressure — ключевая фишка реактивного программирования, которая отличает его от традиционных подходов. В старых моделях (как потоки или Future) вы либо блокируете всё, либо тонете в очередях данных, рискуя исчерпать память или CPU. Здесь же контроль у потребителя: он решает, когда и сколько брать. Это решает проблемы callback-ада и блокировок, позволяя строить масштабируемые приложения — от мобильных до облачных кластеров.
Что такое backpressure и почему оно нужно?
Обратное давление — это способ, при котором получатель данных сигнализирует источнику: "замедлись, если я не успеваю". В реактивном мире данные передаются асинхронно, но без контроля это может привести к проблемам: если издатель генерирует 1 млн элементов в секунду, а подписчик обрабатывает только 100, буфер переполнится, и приложение упадёт с ошибкой "израсходована память" (OutOfMemoryError).
Backpressure вводит "обратную связь": подписчик запрашивает элементы порциями, а издатель выдаёт ровно столько.
Это встроено в Reactive Streams: в методе onSubscribe подписчик получает Subscription и использует request(long n) — "запроси n элементов". Если не запросить — данные не потекут. Это как шлюзы на реке: открываешь по мере нужды, избегая наводнения. В отличие от pull-модели (где вы тянете всё сразу), здесь баланс: push для динамики, но с контролем.
Пример базового использования в Flux (поток для множества элементов):
import reactor.core.publisher.Flux;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
Flux<Integer> fastPublisher = Flux.range(1, 1000); // Быстрый источник: 1000 элементов
fastPublisher.subscribe(new Subscriber<Integer>() {
private Subscription sub;
@Override
public void onSubscribe(Subscription s) {
sub = s;
sub.request(5); // Сначала запрашиваем 5 элементов
}
@Override
public void onNext(Integer item) {
System.out.println("Обработано: " + item);
// Симулируем медленную обработку: Thread.sleep(100); (но в реальности избегайте блокировок!)
sub.request(1); // После каждого — запрашиваем следующий
}
@Override
public void onError(Throwable t) { /* обработка */ }
@Override
public void onComplete() { System.out.println("Готово"); }
});
Здесь подписчик контролирует темп: запросил 5 — получил 5, обработал — запросил ещё. Если не request() — поток остановится. Это асинхронно: издатель не блокируется, а ждёт сигналов.
#Java #middle #Reactor #Reactive_Streams_API #backpressure
👍2