Как изменится результат выполнения кода выше, если заменить collect(toList()) на toList()?
Anonymous Poll
4%
В списке providers не будет элементов из special
4%
В списке providers изменится порядок элементов
13%
Возникнет ошибка компиляции
42%
Возникнет ошибка в рантайме
37%
Ничего не изменится
Коллекторы и тесты
Сразу разберём вопрос выше. Можно ли безопасно заменить collect(toList()) на toList()?
Ответ: нет, в коде выше получим ошибку в рантайме:
❤️ collect(toList()) возвращает ArrayList, который с радостью принимает новые элементы.
💔 toList() возвращает неизменяемый список. Вызов addAll выбросит исключение.
Это очевидная ошибка JDK. Коллекторы - часть Stream API. В рамках функционального стиля логично возвращать неизменяемую коллекцию.
Плюс ошибка на уровне проектирования - неизменяемые коллекции должны иметь интерфейс без методов add/set/remove. Пользователь не должен запоминать эти нюансы и получать внезапные ошибки в рантайме.
Побухтеть на JDK - дело святое, но сфокусируемся на другом. Для начала опишу историю целиком:
Человек поправил toList в коде фичи А. Там не было addAll, и все отлично работало. Затем человек сделал автозамену по всему сервису. Изменение с виду абсолютно безобидное, подвоха никто не ждал.
К счастью, код был хорошо покрыт тестами. Некоторые упали, отсюда мы и узнали эту милую особенность JDK💅
Этот случай очень наглядно показал: невозможно предусмотреть всё. Есть миллион нюансов языка, фреймворков, конфигурации, среды исполнения и тд.
Всегда могут вылезти неочевидные ошибки и странное поведение. Даже если мы сделали простейшие изменения. Даже если мы ничего не делали, но кто-то что-то делал рядом.
Пишите тесты. Это не формальность и не причуды вашего лида. Когда на проекте хорошие тесты, работа идёт на волне спокойствия и комфорта☺️
Сразу разберём вопрос выше. Можно ли безопасно заменить collect(toList()) на toList()?
Ответ: нет, в коде выше получим ошибку в рантайме:
❤️ collect(toList()) возвращает ArrayList, который с радостью принимает новые элементы.
💔 toList() возвращает неизменяемый список. Вызов addAll выбросит исключение.
Это очевидная ошибка JDK. Коллекторы - часть Stream API. В рамках функционального стиля логично возвращать неизменяемую коллекцию.
Плюс ошибка на уровне проектирования - неизменяемые коллекции должны иметь интерфейс без методов add/set/remove. Пользователь не должен запоминать эти нюансы и получать внезапные ошибки в рантайме.
Побухтеть на JDK - дело святое, но сфокусируемся на другом. Для начала опишу историю целиком:
Человек поправил toList в коде фичи А. Там не было addAll, и все отлично работало. Затем человек сделал автозамену по всему сервису. Изменение с виду абсолютно безобидное, подвоха никто не ждал.
К счастью, код был хорошо покрыт тестами. Некоторые упали, отсюда мы и узнали эту милую особенность JDK💅
Этот случай очень наглядно показал: невозможно предусмотреть всё. Есть миллион нюансов языка, фреймворков, конфигурации, среды исполнения и тд.
Всегда могут вылезти неочевидные ошибки и странное поведение. Даже если мы сделали простейшие изменения. Даже если мы ничего не делали, но кто-то что-то делал рядом.
Пишите тесты. Это не формальность и не причуды вашего лида. Когда на проекте хорошие тесты, работа идёт на волне спокойствия и комфорта☺️
Что будет в консоли при выполнении кода выше?
Anonymous Poll
21%
3
14%
4
25%
Ошибка компиляции
39%
Ошибка в рантайме
Загадка Set.of🔮
В прошлом посте обсуждали toList, в этом обсудим Set.of. Здесь ситуация сложнее и гораздо интереснее! Прекрасный пример для тренировки анализа и критического мышления.
Начнем с ответа: код с Set.of выбросит IllegalArgumentException(duplicate element: 2)
Неожиданно! Set - коллекция уникальных элементов. Мы привыкли, что сет сам фильтрует дубликаты и часто это используем.
Почему теперь сет ругается на дубликаты? Почему фильтрацию по уникальности должен делать разработчик перед вставкой?
Но это ещё не всё. Если убрать дубликаты и оставить код таким:
получим NPE Cannot invoke "Object.hashCode()" because "pe" is null. Какая-то ошибка внутри реализации🤯
Поищем ответ в документации, точнее в JEP. Set.of появился в java 9, его цель обозначена явно - создать удобную альтернативу ручному заполнению сета:
Удобно, конечно, но альтернатива получилась не равнозначная.
Почему новый код не умеет фильтровать дубликаты? Почему не умеет работать с null? Почему “удобная” версия работает по-другому?
Я с джавой работаю давно, поэтому причину знаю и расскажу вам в следующем посте. А пока давайте оценим единственную версию из интернета:
🧔 Элементы задаются все и сразу, программист их видит. Если передаются дубликаты или null - это точно ошибка разработчика.
Здесь можно потренировать критическое мышление и подумать, что не так с этой версией.
А потом прочитать 3 причины:
✨ Не всегда значения наглядно записаны. Их могут передать через конструктор или через поля обьекта:
✨Бросать исключение в рантайме при подозрении на опечатку как-то слишком брутально
✨ Если мы не считаем null нормальным элементом, почему не отфильтровать его в начале? Почему мы видим NPE из глубин реализации?
Итого, вариант “чтобы разработчик был внимательнее” нам не подходит. Причина в чем-то другом.
Оставлю вас подумать над этим, а в следующем посте расскажу, почему Set.of такой странный. Там интересно😊
В прошлом посте обсуждали toList, в этом обсудим Set.of. Здесь ситуация сложнее и гораздо интереснее! Прекрасный пример для тренировки анализа и критического мышления.
Начнем с ответа: код с Set.of выбросит IllegalArgumentException(duplicate element: 2)
Неожиданно! Set - коллекция уникальных элементов. Мы привыкли, что сет сам фильтрует дубликаты и часто это используем.
Почему теперь сет ругается на дубликаты? Почему фильтрацию по уникальности должен делать разработчик перед вставкой?
Но это ещё не всё. Если убрать дубликаты и оставить код таким:
Set.of(1, 2, null)
получим NPE Cannot invoke "Object.hashCode()" because "pe" is null. Какая-то ошибка внутри реализации🤯
Поищем ответ в документации, точнее в JEP. Set.of появился в java 9, его цель обозначена явно - создать удобную альтернативу ручному заполнению сета:
// 8
Set<String> set = new HashSet<>();
set.add("a");
set.add("b");
set = Collections.unmodifiableSet(set);
// 9
Set<String>set = Set.of(“a“, “b“);
Удобно, конечно, но альтернатива получилась не равнозначная.
Почему новый код не умеет фильтровать дубликаты? Почему не умеет работать с null? Почему “удобная” версия работает по-другому?
Я с джавой работаю давно, поэтому причину знаю и расскажу вам в следующем посте. А пока давайте оценим единственную версию из интернета:
🧔 Элементы задаются все и сразу, программист их видит. Если передаются дубликаты или null - это точно ошибка разработчика.
Здесь можно потренировать критическое мышление и подумать, что не так с этой версией.
А потом прочитать 3 причины:
✨ Не всегда значения наглядно записаны. Их могут передать через конструктор или через поля обьекта:
Set<Long> adminIds = Set.of(user1.getId(), user2.getId());
✨Бросать исключение в рантайме при подозрении на опечатку как-то слишком брутально
✨ Если мы не считаем null нормальным элементом, почему не отфильтровать его в начале? Почему мы видим NPE из глубин реализации?
Итого, вариант “чтобы разработчик был внимательнее” нам не подходит. Причина в чем-то другом.
Оставлю вас подумать над этим, а в следующем посте расскажу, почему Set.of такой странный. Там интересно😊
Почему в Set.of нельзя добавить дубликаты и null?
Потому что разработчики замечтались и немного выпали из реальности😅 Но обо всем по порядку.
Set.of вышел в рамках Java 9 в 2017 году.
В 2014 в джаве начали Project Valhalla. Его основная фича - value types. Грубо говоря, это компактные классы-значения. Все поля лежат в памяти рядышком, без лишних заголовков и прыжков по куче.
Энтузиазм зашкаливает, у новой фичи большие перспективы. Идёт активная работа и обсуждение. Stuart Marks, наш сегодняшний герой, активно вовлечён в этот процесс.
Но в джаве много направлений. Одна из задач Стюарта - реализовать апи для маленького сета, сделать удобную форму для такого кода:
Просто скрыть эту логику за Set.of кажется неэффективным. Значений мало, они не будут меняться. Так и просится какая-нибудь оптимизация.
Зимой у меня выходил пост Как ускорить HashMap. Там я описала альтернативный алгоритм для сета, в котором значения лежат рядом и не надо прыгать по ссылкам по всей куче. Сейчас в джаве его применить нельзя из-за особенностей работы с объектами.
“Но ведь скоро мы реализуем value types!”: подумал Стюарт и взял этот алгоритм за основу Set.of.
Реализация Set.of - линейное пробирование в чистом виде. Сверху стоит аннотация @ValueBased - отметка для будущего использования value types.
Отсюда понятно, почему Set.of не принимает дубликаты и null: в концепции value types таких понятий просто не существует. Любая сущность уникальна и не может быть null. С этой точки зрения, поведение Set.of абсолютно логично.
Чем меня веселит эта история.
Работу над Project Valhalla начали в 2014. Java 9 и Set.of вышли в 2017. Сейчас 2025, прошло 8 лет.
Как говорится: если разработчики сказали, что сделают value types, они сделают. Не нужно напоминать об этом каждые полгода.
Задел на прекрасную джаву будущего понятен. Но по сути разработчики выкатили реализацию с оглядкой на фичу, которая не вышла и в обозримом будущем не выйдет. Очаровательно😄
Потому что разработчики замечтались и немного выпали из реальности😅 Но обо всем по порядку.
Set.of вышел в рамках Java 9 в 2017 году.
В 2014 в джаве начали Project Valhalla. Его основная фича - value types. Грубо говоря, это компактные классы-значения. Все поля лежат в памяти рядышком, без лишних заголовков и прыжков по куче.
Энтузиазм зашкаливает, у новой фичи большие перспективы. Идёт активная работа и обсуждение. Stuart Marks, наш сегодняшний герой, активно вовлечён в этот процесс.
Но в джаве много направлений. Одна из задач Стюарта - реализовать апи для маленького сета, сделать удобную форму для такого кода:
Set<String> set = new HashSet<>();
set.add("a");
set.add("b");
set = Collections.unmodifiableSet(set);
Просто скрыть эту логику за Set.of кажется неэффективным. Значений мало, они не будут меняться. Так и просится какая-нибудь оптимизация.
Зимой у меня выходил пост Как ускорить HashMap. Там я описала альтернативный алгоритм для сета, в котором значения лежат рядом и не надо прыгать по ссылкам по всей куче. Сейчас в джаве его применить нельзя из-за особенностей работы с объектами.
“Но ведь скоро мы реализуем value types!”: подумал Стюарт и взял этот алгоритм за основу Set.of.
Реализация Set.of - линейное пробирование в чистом виде. Сверху стоит аннотация @ValueBased - отметка для будущего использования value types.
Отсюда понятно, почему Set.of не принимает дубликаты и null: в концепции value types таких понятий просто не существует. Любая сущность уникальна и не может быть null. С этой точки зрения, поведение Set.of абсолютно логично.
Чем меня веселит эта история.
Работу над Project Valhalla начали в 2014. Java 9 и Set.of вышли в 2017. Сейчас 2025, прошло 8 лет.
Как говорится: если разработчики сказали, что сделают value types, они сделают. Не нужно напоминать об этом каждые полгода.
Задел на прекрасную джаву будущего понятен. Но по сути разработчики выкатили реализацию с оглядкой на фичу, которая не вышла и в обозримом будущем не выйдет. Очаровательно😄
Сколько операций на запись может одновременно проводить ConcurrentHashMap?
Anonymous Poll
13%
1
7%
16
11%
Количество бакетов / 16
54%
Количество бакетов
3%
Количество бакетов * 2
12%
Неограниченное количество
Необычный вопрос на собеседовании
Искали мы как-то человека в команду. Пришел кандидат, обсудили опыт, прошлись по основным вопросам.
Дошли до многопоточки. Человек сказал, что нужно ее избегать, потому что тема сложная и легко ошибиться. А потом добавил:
🧑🦰: Вот Redis однопоточный, внутри у него нет блокировок, поэтому он такой быстрый!
У меня в голове сразу возникла задача “на подумать”. Решила с вами поделиться, тк такое утверждение про Redis слышу довольно часто.
Рассмотрим два сервиса:
🍓Первый - тот самый однопоточный Redis.
🚲 Второй - key-value хранилище на базе ConcurrentHashMap и Spring MVC. Код примерно такой:
Задача: сравнить оба варианта. В расчет берём только базовую функцию - записать и прочитать ключ-значение из оперативной памяти.
В итоге получился очень интересный разговор. Люблю такое на собеседованиях😊
Ещё немного вводных, чтобы глубже погрузиться в вопрос.
Структура данных
В Redis используется простая хэш-таблица. Считаем хэш ключа, определяем бакет, добавляем в список. Алгоритм даже проще, чем в джаве. В джаве список перестраивается в дерево, когда элементов много. В Redis такого нет.
Многопоточный доступ
В Redis хэш-таблица никак не синхронизирована, безопасно работать может только один поток.
ConcurrentHashMap не зря называется concurrent. Область синхронизации при записи ограничена одним бакетом, т.е число одновременно пишущих потоков ~ числу бакетов. На чтение ограничений вообще нет.
Потенциально ConcurrentHashMap способен обслужить миллионы запросов одновременно. Redis в каждый момент времени работает с одним запросом.
Явный перевес в сторону нашего велосипеда🧐
На этом этапе кандидат согласился, что всё очень загадочно. И фраза, что Redis однопоточный и потому такой быстрый, звучит странно.
Почему же считается, что Redis лучше, и как он справляется с нагрузкой своим одним потоком? Об этом расскажу завтра❤️
Искали мы как-то человека в команду. Пришел кандидат, обсудили опыт, прошлись по основным вопросам.
Дошли до многопоточки. Человек сказал, что нужно ее избегать, потому что тема сложная и легко ошибиться. А потом добавил:
🧑🦰: Вот Redis однопоточный, внутри у него нет блокировок, поэтому он такой быстрый!
У меня в голове сразу возникла задача “на подумать”. Решила с вами поделиться, тк такое утверждение про Redis слышу довольно часто.
Рассмотрим два сервиса:
🍓Первый - тот самый однопоточный Redis.
🚲 Второй - key-value хранилище на базе ConcurrentHashMap и Spring MVC. Код примерно такой:
public class MapController {
private final Map<String, Object> map = new ConcurrentHashMap<>();
@PostMapping
public void putValue(@RequestParam String k, @RequestParam Object v) {
map.put(k, v);
}
@GetMapping
public Object getValue(@RequestParam String k) {
return map.get(k);
}
}
Задача: сравнить оба варианта. В расчет берём только базовую функцию - записать и прочитать ключ-значение из оперативной памяти.
В итоге получился очень интересный разговор. Люблю такое на собеседованиях😊
Ещё немного вводных, чтобы глубже погрузиться в вопрос.
Структура данных
В Redis используется простая хэш-таблица. Считаем хэш ключа, определяем бакет, добавляем в список. Алгоритм даже проще, чем в джаве. В джаве список перестраивается в дерево, когда элементов много. В Redis такого нет.
Многопоточный доступ
В Redis хэш-таблица никак не синхронизирована, безопасно работать может только один поток.
ConcurrentHashMap не зря называется concurrent. Область синхронизации при записи ограничена одним бакетом, т.е число одновременно пишущих потоков ~ числу бакетов. На чтение ограничений вообще нет.
Потенциально ConcurrentHashMap способен обслужить миллионы запросов одновременно. Redis в каждый момент времени работает с одним запросом.
Явный перевес в сторону нашего велосипеда🧐
На этом этапе кандидат согласился, что всё очень загадочно. И фраза, что Redis однопоточный и потому такой быстрый, звучит странно.
Почему же считается, что Redis лучше, и как он справляется с нагрузкой своим одним потоком? Об этом расскажу завтра❤️
Почему однопоточный Redis такой быстрый?
В прошлом посте предложила вам задачку: сравнить Redis и велосипедик на основе ConcurrentHashMap + Spring MVC.
ConcurrentHashMap — многопоточный, и вроде должен быть лучше. Но именно однопоточный Redis является базовым выбором для кэша.
Как однопоточный Redis справляется с нагрузкой?
Секрет в том, как он работает с запросами. Есть 2 основные модели:
🌊 Каждый запрос обрабатывается в своем потоке (thread per request).
Такая модель используется, когда мы подключаем Spring MVC. Наш велосипедик тоже на ней работает.
У каждого потока свой стэк, переменные изолированы. Код легко писать, читать и дебажить. Идеальный вариант для сложных энтерпрайзных задач!
Но есть недостаток - число запросов в работе ограничено числом потоков в ОС. Обычно это несколько тысяч.
Из-за этой модели наш велосипед и проигрывает:
😒 Миллионы запросов просто не дойдут до ConcurrentHashMap, максимум несколько тысяч.
😒 Прочитать и записать в мэп - простые операции. Отправлять таких малышей в отдельный поток - как забивать краном гвозди. Очень большие накладные расходы на каждый запрос.
Redis использует другую модель:
🏃 EventLoop - малое число потоков бешено переключаются между запросами. В работу можно взять миллионы запросов!
Такая схема используется в реактивных серверах типа Netty, поддерживает многопоточность в JS и питоне.
Поэтому Redis и побеждает наш велосипед: возни с потоками нет, ограничений на запросы нет. Вся мощь процессора уходит на полезную работу, поэтому даже один поток справляется с большим объемом задач.
❓ Можно ли взять лучшее из двух миров? Использовать многопоточность вместе с EventLoop?
Можно! Один поток Redis не использует все доступные ядра процессора, поэтому добавить десяток потоков - вполне рабочая идея.
Такую схему используют KeyDB и DragonflyDB. На сайте публикуют бенчмарки, где они обходят Redis в 5-25 раз. 25 раз звучит слишком мощно, но про 5-10 раз можно верить.
❓ Почему чаще используется Redis, а не более быстрые альтернативы?
Потому что Redis появился в 2009, используется на сотнях проектов и закрепился в сознании как базовое решение для кэша. Подводные камни известны, инфраструктура налажена, куча статей и докладов.
KeyDB и DragonflyDB - свежие БД пирожки. Один вышел в 19 году, другой в 22. На конференциях особо не светились, громких кейсов внедрения пока нет.
Энтерпрайз мир тяжело принимает новые технологии. Плюс не всегда нужно лучшее решение, иногда достаточно хорошего😊
В прошлом посте предложила вам задачку: сравнить Redis и велосипедик на основе ConcurrentHashMap + Spring MVC.
ConcurrentHashMap — многопоточный, и вроде должен быть лучше. Но именно однопоточный Redis является базовым выбором для кэша.
Как однопоточный Redis справляется с нагрузкой?
Секрет в том, как он работает с запросами. Есть 2 основные модели:
🌊 Каждый запрос обрабатывается в своем потоке (thread per request).
Такая модель используется, когда мы подключаем Spring MVC. Наш велосипедик тоже на ней работает.
У каждого потока свой стэк, переменные изолированы. Код легко писать, читать и дебажить. Идеальный вариант для сложных энтерпрайзных задач!
Но есть недостаток - число запросов в работе ограничено числом потоков в ОС. Обычно это несколько тысяч.
Из-за этой модели наш велосипед и проигрывает:
😒 Миллионы запросов просто не дойдут до ConcurrentHashMap, максимум несколько тысяч.
😒 Прочитать и записать в мэп - простые операции. Отправлять таких малышей в отдельный поток - как забивать краном гвозди. Очень большие накладные расходы на каждый запрос.
Redis использует другую модель:
🏃 EventLoop - малое число потоков бешено переключаются между запросами. В работу можно взять миллионы запросов!
Такая схема используется в реактивных серверах типа Netty, поддерживает многопоточность в JS и питоне.
Поэтому Redis и побеждает наш велосипед: возни с потоками нет, ограничений на запросы нет. Вся мощь процессора уходит на полезную работу, поэтому даже один поток справляется с большим объемом задач.
❓ Можно ли взять лучшее из двух миров? Использовать многопоточность вместе с EventLoop?
Можно! Один поток Redis не использует все доступные ядра процессора, поэтому добавить десяток потоков - вполне рабочая идея.
Такую схему используют KeyDB и DragonflyDB. На сайте публикуют бенчмарки, где они обходят Redis в 5-25 раз. 25 раз звучит слишком мощно, но про 5-10 раз можно верить.
❓ Почему чаще используется Redis, а не более быстрые альтернативы?
Потому что Redis появился в 2009, используется на сотнях проектов и закрепился в сознании как базовое решение для кэша. Подводные камни известны, инфраструктура налажена, куча статей и докладов.
KeyDB и DragonflyDB - свежие БД пирожки. Один вышел в 19 году, другой в 22. На конференциях особо не светились, громких кейсов внедрения пока нет.
Энтерпрайз мир тяжело принимает новые технологии. Плюс не всегда нужно лучшее решение, иногда достаточно хорошего😊
⚡Требуется Java эксперт⚡
Последнее время плотно занимаюсь новым проектом. Цель проста - помочь джавистам профессионально расти и быть на коне в текущих сложных условиях.
Планы амбициозные, но на первом этапе буду работать над ультра востребованной темой - подготовке к собеседованиям. Хочу сделать этот процесс проще, лучше и приятнее. Заодно протестирую несколько концепций. Пойму, в ту ли сторону я смотрю и находит ли это отклик.
Для реализации мне нужны помощники, поэтому открываю вакансию: Java эксперт. Ищу людей, которые
⭐ Знают и любят джава разработку
⭐ Работали минимум на 3 разных проектах и видели всякое
⭐ Критически мыслят, видят логические несостыковки
⭐ Умеют работать с первоисточниками и проверять информацию. Не боятся простыни текста на английском
⭐ Большой плюс, если вы часто проводите собеседования и видите, где у людей случаются затыки
Что нужно делать: писать статьи по моему ТЗ. Сначала проработаем популярные темы типа HashMap, ACID, N+1, потом углубимся в другие нужные вещи.
Это будет серьезный умственный труд. Естественно, оплачиваемый.
Как присоединиться:
▫️ Заполнить анкету с небольшим тех.скринингом
▫️ Если все ок, я пришлю вам на почту оплачиваемое тестовое задание
Если вам интересно в этом поучаствовать, заполняйте анкету, не стесняйтесь. Если у вас есть знакомый, который подходит под описание, пришлите ему этот пост❤️
Последнее время плотно занимаюсь новым проектом. Цель проста - помочь джавистам профессионально расти и быть на коне в текущих сложных условиях.
Планы амбициозные, но на первом этапе буду работать над ультра востребованной темой - подготовке к собеседованиям. Хочу сделать этот процесс проще, лучше и приятнее. Заодно протестирую несколько концепций. Пойму, в ту ли сторону я смотрю и находит ли это отклик.
Для реализации мне нужны помощники, поэтому открываю вакансию: Java эксперт. Ищу людей, которые
⭐ Знают и любят джава разработку
⭐ Работали минимум на 3 разных проектах и видели всякое
⭐ Критически мыслят, видят логические несостыковки
⭐ Умеют работать с первоисточниками и проверять информацию. Не боятся простыни текста на английском
⭐ Большой плюс, если вы часто проводите собеседования и видите, где у людей случаются затыки
Что нужно делать: писать статьи по моему ТЗ. Сначала проработаем популярные темы типа HashMap, ACID, N+1, потом углубимся в другие нужные вещи.
Это будет серьезный умственный труд. Естественно, оплачиваемый.
Как присоединиться:
▫️ Заполнить анкету с небольшим тех.скринингом
▫️ Если все ок, я пришлю вам на почту оплачиваемое тестовое задание
Если вам интересно в этом поучаствовать, заполняйте анкету, не стесняйтесь. Если у вас есть знакомый, который подходит под описание, пришлите ему этот пост❤️
Самая унылая тема на собеседовании это
Anonymous Poll
30%
SOLID
27%
Методы Object
13%
Интерфейс против абстрактного класса
14%
ArrayList против LinkedList
16%
Пулы строк и Integer с задачками на ==
Принципы SOLID и групповой транс
Недавно слушала записи собеседований и обратила внимание на интересный феномен. Связан он, как вы уже догадались, с обсуждением SOLID.
Типичный диалог на собеседовании выглядит так:
👨💼: Расскажите про SOLID
👱♂️: [расшифровка каждой буквы]
👨💼: Используете в работе?
👱♂️: Конечно!
Хорошо, если на этом все заканчивается. Но бывает и продолжение: "расскажите принципы своими словами". На этом месте даже опытные и умные люди впадают в транс. Например, кандидат описывает принцип Open-closed:
👱♂️: Чтобы добавить новый метод, нужно не менять существующий класс, а создать новый
👨💼 : Угу, давайте дальше
Ни кандидат, ни собеседующий не создают новый класс для каждого метода. Почему один так отвечает, а другой принимает такой ответ — загадка.
Dependency Inversion часто объясняют как "нужно всё делать через интерфейс".
Это считается реализацией DI? Чем отличается от инкапсуляции? Для каждого класса нужно создавать интерфейс? Работа с любым классом напрямую — это нарушение принципа? Почему в названии есть инвершн, что инвертируется-то?
Вопросы здравые, но люди в трансе:) У других абстрактных тем вроде принципов ООП или паттернов такого эффекта нет.
SOLID — самая унылая тема на собеседовании. Формальная, абстрактная и скучная для всех сторон. Собеседование длится всего час-полтора, нет никакого смысла тратить время на ритуальный обмен фразами.
❓ Применяются ли принципы SOLID на практике?
▫️ Single Responsibility - да. Что неудивительно, это самый понятный и простой принцип.
▫️ Open-closed, Liskov и Interface Segregation тесно связаны с наследованием и сложными иерархиями классов. Для бизнес логики большинства систем это не очень актуально. Там максимум интерфейс и несколько реализаций.
▫️ Dependency Inversion - самый непонятный и недооцененный принцип. За 10+ лет в разработке я встречала мало людей, которые его поняли. Это не просто "делаем всё через интерфейс". Можно реализовать его даже без интерфейсов, суть вообще в другом.
К четвергу напишу пост и подробно объясню🔥
Недавно слушала записи собеседований и обратила внимание на интересный феномен. Связан он, как вы уже догадались, с обсуждением SOLID.
Типичный диалог на собеседовании выглядит так:
👨💼: Расскажите про SOLID
👱♂️: [расшифровка каждой буквы]
👨💼: Используете в работе?
👱♂️: Конечно!
Хорошо, если на этом все заканчивается. Но бывает и продолжение: "расскажите принципы своими словами". На этом месте даже опытные и умные люди впадают в транс. Например, кандидат описывает принцип Open-closed:
👱♂️: Чтобы добавить новый метод, нужно не менять существующий класс, а создать новый
👨💼 : Угу, давайте дальше
Ни кандидат, ни собеседующий не создают новый класс для каждого метода. Почему один так отвечает, а другой принимает такой ответ — загадка.
Dependency Inversion часто объясняют как "нужно всё делать через интерфейс".
Map<String, String> map = new HashMap<>();
Это считается реализацией DI? Чем отличается от инкапсуляции? Для каждого класса нужно создавать интерфейс? Работа с любым классом напрямую — это нарушение принципа? Почему в названии есть инвершн, что инвертируется-то?
Вопросы здравые, но люди в трансе:) У других абстрактных тем вроде принципов ООП или паттернов такого эффекта нет.
SOLID — самая унылая тема на собеседовании. Формальная, абстрактная и скучная для всех сторон. Собеседование длится всего час-полтора, нет никакого смысла тратить время на ритуальный обмен фразами.
❓ Применяются ли принципы SOLID на практике?
▫️ Single Responsibility - да. Что неудивительно, это самый понятный и простой принцип.
▫️ Open-closed, Liskov и Interface Segregation тесно связаны с наследованием и сложными иерархиями классов. Для бизнес логики большинства систем это не очень актуально. Там максимум интерфейс и несколько реализаций.
▫️ Dependency Inversion - самый непонятный и недооцененный принцип. За 10+ лет в разработке я встречала мало людей, которые его поняли. Это не просто "делаем всё через интерфейс". Можно реализовать его даже без интерфейсов, суть вообще в другом.
К четвергу напишу пост и подробно объясню🔥
Dependency Inversion
Сегодня расскажу, в чем суть принципа DI из SOLID, и почему с ним так много проблем. Вспомним формулировку:
✍️ Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.
✍️ Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Ставь огонёк, если даже не вчитывался в этот набор слов😊
Формулировка и правда полный отстой. Первая часть про какие-то модули, зависящие от каких-то абстракций. Вторая напоминает инкапсуляцию.
Роберт Мартин, он же дядюшка Боб, в целом молодец, но конкретно здесь плохо донёс свою идею. В оригинальной статье всё очень сумбурно. Интерфейсы добавляются на каждом шагу и отвлекают от сути dependency inversion. Ничего удивительного, что большинство людей не поняли, о чём речь.
Дядюшка Боб не смог, тетя Диана объяснит🤌
Все очень просто. У каждого класса есть своя область ответственности, single responsibility:
🔴 Кнопку нажимают, и она отправляет сигнал об этом. Ничего больше
💡 Лампа включается и выключается, когда ей говорят. Ничего больше
💏 Взаимодействие этих сущностей должно быть описано отдельно
Где здесь инверсия:
В нашем мире все описывается последовательно, причина -> следствие. Нажали кнопку -> включилась лампа. Перекладывая на код, в классе Кнопка будет поле Лампа:
В мире с Dependency inversion Кнопка и Лампа ничего не знают друг о друге, логика взаимодействия описана в классе Электросхема. Она ловит сигналы от Кнопки и отправляет команды Лампе:
Если у лампы появится новый режим, разбираться с этим будет электросхема, а не кнопка. Если кнопка поменяется на датчик движения, обработка поменяется только в электросхеме.
Интерфейсы можно добавить, чтобы жонглировать реализациями, но это не главное. Dependency inversion говорит о том, что логика взаимодействия компонентов должна находиться НЕ в самих компонентах.
Кстати, если перейти с уровня классов на уровень контроллеры/сервисы, получится Clean architecture. В основе та же идея: компоненты отдельно, взаимодействие отдельно.
Всё очень просто❤️
Сегодня расскажу, в чем суть принципа DI из SOLID, и почему с ним так много проблем. Вспомним формулировку:
✍️ Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.
✍️ Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Ставь огонёк, если даже не вчитывался в этот набор слов😊
Формулировка и правда полный отстой. Первая часть про какие-то модули, зависящие от каких-то абстракций. Вторая напоминает инкапсуляцию.
Роберт Мартин, он же дядюшка Боб, в целом молодец, но конкретно здесь плохо донёс свою идею. В оригинальной статье всё очень сумбурно. Интерфейсы добавляются на каждом шагу и отвлекают от сути dependency inversion. Ничего удивительного, что большинство людей не поняли, о чём речь.
Дядюшка Боб не смог, тетя Диана объяснит🤌
Все очень просто. У каждого класса есть своя область ответственности, single responsibility:
🔴 Кнопку нажимают, и она отправляет сигнал об этом. Ничего больше
💡 Лампа включается и выключается, когда ей говорят. Ничего больше
💏 Взаимодействие этих сущностей должно быть описано отдельно
Где здесь инверсия:
В нашем мире все описывается последовательно, причина -> следствие. Нажали кнопку -> включилась лампа. Перекладывая на код, в классе Кнопка будет поле Лампа:
public class Button
private Lamp lamp;
public void push() {
// включить/выключить лампу
}
}
В мире с Dependency inversion Кнопка и Лампа ничего не знают друг о друге, логика взаимодействия описана в классе Электросхема. Она ловит сигналы от Кнопки и отправляет команды Лампе:
public class Circuit {
private Button button;
private Lamp lamp;
public void process() {
// получить сигнал от кнопки
// сказать лампе включиться / выключиться
}
}
Если у лампы появится новый режим, разбираться с этим будет электросхема, а не кнопка. Если кнопка поменяется на датчик движения, обработка поменяется только в электросхеме.
Интерфейсы можно добавить, чтобы жонглировать реализациями, но это не главное. Dependency inversion говорит о том, что логика взаимодействия компонентов должна находиться НЕ в самих компонентах.
Кстати, если перейти с уровня классов на уровень контроллеры/сервисы, получится Clean architecture. В основе та же идея: компоненты отдельно, взаимодействие отдельно.
Всё очень просто❤️
В БД складываются задачи, которые затем распределяются между несколькими сервисами. Сервис извлекает задачи и обрабатывает их примерно так👇
Какая проблема возможна в этом коде?
Какая проблема возможна в этом коде?
Популярная ошибка в блокировках, часть 1
Недавно делала ревью одного сервиса и нашла там ошибки, связанные с блокировками. Я встречала подобные ошибки и на других проектах, поэтому давайте разберём их вместе, будет полезно:)
Исходная цель: распределить задачи между сервисами. Решается очень просто:
✍️ Складываем задачи в отдельную таблицу
🙋 Каждый сервис выбирает из таблицы одну строку и ставит на неё блокировку. Благодаря блокировке другой сервис не может взять эту задачу в работу.
Примерно такая реализация и сделана на картинке выше. С виду вроде всё хорошо, но есть две проблемы. Первая - задача может быть обработана несколько раз💔
🤔 Почему?
Блокировка действует до конца транзакции. В коде выше транзакция заканчивается после получения нужной строки, затем блокировка снимается. И по сути блокировки нет. Одну задачу могут взять несколько сервисов и обработать её несколько раз.
Что делать:
✅ Поставить аннотацию Transactional над Scheduled методом. Тогда блокировка держится во время всей работы над задачей
Плюс: просто реализовать
Минус: если обработка сложная, мы долго и нерационально держим соединение с базой
✅ Альтернатива — отказаться от блокировок. Берём задачу в работу — меняем статус в БД на "in progress". Делается одним запросом с помощью RETURNING:
Плюс: нет длинной транзакции
Минус: если сервис упадёт, задача останется в БД со статусом "в работе", и в итоге не будет обработана. Нужно дополнительно следить за такими ситуациями.
Итого
Блокировки работают до конца работы транзакции. Если полагаетесь на блокировки - не отпускайте их раньше времени.
Это просто, но в большой кодовой базе легко упустить этот момент. Будьте внимательнее❤️ Вторую проблему опишу в следующем посте!
Недавно делала ревью одного сервиса и нашла там ошибки, связанные с блокировками. Я встречала подобные ошибки и на других проектах, поэтому давайте разберём их вместе, будет полезно:)
Исходная цель: распределить задачи между сервисами. Решается очень просто:
✍️ Складываем задачи в отдельную таблицу
🙋 Каждый сервис выбирает из таблицы одну строку и ставит на неё блокировку. Благодаря блокировке другой сервис не может взять эту задачу в работу.
Примерно такая реализация и сделана на картинке выше. С виду вроде всё хорошо, но есть две проблемы. Первая - задача может быть обработана несколько раз💔
🤔 Почему?
Блокировка действует до конца транзакции. В коде выше транзакция заканчивается после получения нужной строки, затем блокировка снимается. И по сути блокировки нет. Одну задачу могут взять несколько сервисов и обработать её несколько раз.
Что делать:
✅ Поставить аннотацию Transactional над Scheduled методом. Тогда блокировка держится во время всей работы над задачей
Плюс: просто реализовать
Минус: если обработка сложная, мы долго и нерационально держим соединение с базой
✅ Альтернатива — отказаться от блокировок. Берём задачу в работу — меняем статус в БД на "in progress". Делается одним запросом с помощью RETURNING:
UPDATE tasks SET status = 'in progress'
WHERE status = 'not processed'
RETURNING id, …;
Плюс: нет длинной транзакции
Минус: если сервис упадёт, задача останется в БД со статусом "в работе", и в итоге не будет обработана. Нужно дополнительно следить за такими ситуациями.
Итого
Блокировки работают до конца работы транзакции. Если полагаетесь на блокировки - не отпускайте их раньше времени.
Это просто, но в большой кодовой базе легко упустить этот момент. Будьте внимательнее❤️ Вторую проблему опишу в следующем посте!
Продолжение прошлого вопроса. В БД складываются задачи, которые затем распределяются между несколькими сервисами. Сервис извлекает задачи и обрабатывает их примерно так👇
Над Scheduled задачей добавилась аннотация Transactional.
Какая проблема возможна в этом коде?
Над Scheduled задачей добавилась аннотация Transactional.
Какая проблема возможна в этом коде?
Какая проблема возможна в коде выше?
Anonymous Poll
13%
Утечка соединений к базе данных
8%
Задача отметится как невыполненная, хотя была обработана
8%
Некоторые задачи будут пропущены
24%
Задача может быть обработана несколько раз
42%
Задачи в разных сервисах не обрабатываются параллельно
6%
Всё нормально
Популярная ошибка в блокировках, часть 2
Продолжим разбирать ошибки при распределении задач. В прошлом посте мы разобрали, что не нужно отпускать блокировку слишком рано, иначе одну задачку возьмут несколько сервисов. Сегодня разберём следующую проблему:
Задачи в разных сервисах не обрабатываются параллельно
Почему?
🧑💻 Сервис 1 выполняет запрос "найди задачу для обработки", получает строку 1 и блокирует её
🧑💻 Сервис 2 выполняет такой же запрос, получает в результате ту же строку. Но блокировку поставить не может, так как строка уже заблокирована.
💅 Сервис 2 ждёт, пока блокировка снимется
В итоге все сервисы ждут, пока сервис 1 закончит задачу 1 и отпустит блокировку. С другими задачами ситуация повторится - один сервис работает, остальные висят на блокировке.
Не самая эффективная командная работа🙈
Чтобы исправить ситуацию, сервисы должны пропускать заблокированные строки и брать себе задачу из "свободных". При взятии блокировки нужно добавить SKIP LOCKED.
Но средствами Spring Data SKIP LOCKED не сделать, поэтому напишем SQL запрос над методом в репозитории:
Теперь задачи обрабатываются параллельно несколькими сервисами🥳
P.S. Очень рада, что в прошлом опросе было много правильных ответов, вы умнички🥰
Продолжим разбирать ошибки при распределении задач. В прошлом посте мы разобрали, что не нужно отпускать блокировку слишком рано, иначе одну задачку возьмут несколько сервисов. Сегодня разберём следующую проблему:
Задачи в разных сервисах не обрабатываются параллельно
Почему?
🧑💻 Сервис 1 выполняет запрос "найди задачу для обработки", получает строку 1 и блокирует её
🧑💻 Сервис 2 выполняет такой же запрос, получает в результате ту же строку. Но блокировку поставить не может, так как строка уже заблокирована.
💅 Сервис 2 ждёт, пока блокировка снимется
В итоге все сервисы ждут, пока сервис 1 закончит задачу 1 и отпустит блокировку. С другими задачами ситуация повторится - один сервис работает, остальные висят на блокировке.
Не самая эффективная командная работа🙈
Чтобы исправить ситуацию, сервисы должны пропускать заблокированные строки и брать себе задачу из "свободных". При взятии блокировки нужно добавить SKIP LOCKED.
Но средствами Spring Data SKIP LOCKED не сделать, поэтому напишем SQL запрос над методом в репозитории:
@Query(value = "SELECT * FROM outbox
WHERE is_done = false
ORDER BY id ASC LIMIT 1
FOR UPDATE SKIP LOCKED",
nativeQuery = true
)
Optional<OutboxEntry> findFirstByIsDoneFalseOrderByIdAsc();
Теперь задачи обрабатываются параллельно несколькими сервисами🥳
P.S. Очень рада, что в прошлом опросе было много правильных ответов, вы умнички🥰