Java: fill the gaps – Telegram
Java: fill the gaps
12.9K subscribers
7 photos
215 links
Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк

🔥Тот самый курс по многопочке🔥
https://fillthegaps.ru/mt

Комплименты, вопросы, предложения: @utki_letyat
Download Telegram
Принципы SOLID и групповой транс

Недавно слушала записи собеседований и обратила внимание на интересный феномен. Связан он, как вы уже догадались, с обсуждением 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+ лет в разработке я встречала мало людей, которые его поняли. Это не просто "делаем всё через интерфейс". Можно реализовать его даже без интерфейсов, суть вообще в другом.

К четвергу напишу пост и подробно объясню🔥
🔥369👍6526👎17
Dependency Inversion

Сегодня расскажу, в чем суть принципа 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. В основе та же идея: компоненты отдельно, взаимодействие отдельно.

Всё очень просто❤️
🔥297139👍77👎47
В БД складываются задачи, которые затем распределяются между несколькими сервисами. Сервис извлекает задачи и обрабатывает их примерно так👇

Какая проблема возможна в этом коде?
🔥36👍10
Популярная ошибка в блокировках, часть 1

Недавно делала ревью одного сервиса и нашла там ошибки, связанные с блокировками. Я встречала подобные ошибки и на других проектах, поэтому давайте разберём их вместе, будет полезно:)

Исходная цель: распределить задачи между сервисами. Решается очень просто:
✍️ Складываем задачи в отдельную таблицу
🙋 Каждый сервис выбирает из таблицы одну строку и ставит на неё блокировку. Благодаря блокировке другой сервис не может взять эту задачу в работу.

Примерно такая реализация и сделана на картинке выше. С виду вроде всё хорошо, но есть две проблемы. Первая - задача может быть обработана несколько раз💔

🤔 Почему?

Блокировка действует до конца транзакции. В коде выше транзакция заканчивается после получения нужной строки, затем блокировка снимается. И по сути блокировки нет. Одну задачу могут взять несколько сервисов и обработать её несколько раз.

Что делать:
Поставить аннотацию Transactional над Scheduled методом. Тогда блокировка держится во время всей работы над задачей
Плюс: просто реализовать
Минус: если обработка сложная, мы долго и нерационально держим соединение с базой

Альтернатива — отказаться от блокировок. Берём задачу в работу — меняем статус в БД на "in progress". Делается одним запросом с помощью RETURNING:
UPDATE tasks SET status = 'in progress'
WHERE status = 'not processed'
RETURNING id, …;

Плюс: нет длинной транзакции
Минус: если сервис упадёт, задача останется в БД со статусом "в работе", и в итоге не будет обработана. Нужно дополнительно следить за такими ситуациями.

Итого

Блокировки работают до конца работы транзакции. Если полагаетесь на блокировки - не отпускайте их раньше времени.

Это просто, но в большой кодовой базе легко упустить этот момент. Будьте внимательнее❤️ Вторую проблему опишу в следующем посте!
🔥193👍6940👎17
Продолжение прошлого вопроса. В БД складываются задачи, которые затем распределяются между несколькими сервисами. Сервис извлекает задачи и обрабатывает их примерно так👇

Над Scheduled задачей добавилась аннотация Transactional.

Какая проблема возможна в этом коде?
🔥24👍4
Популярная ошибка в блокировках, часть 2

Продолжим разбирать ошибки при распределении задач. В прошлом посте мы разобрали, что не нужно отпускать блокировку слишком рано, иначе одну задачку возьмут несколько сервисов. Сегодня разберём следующую проблему:

Задачи в разных сервисах не обрабатываются параллельно

Почему?

🧑‍💻 Сервис 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. Очень рада, что в прошлом опросе было много правильных ответов, вы умнички🥰
🔥220👍5945👎5
Критикую Object

В последнее время анализировала чудовищное количество API, изучала контракты и обязанности разных систем и сервисов. На этой волне и созрел сегодняшний пост.

Предлагаю вам взглянуть по-новому на класс Object. Удивиться, насколько он плох с точки зрения API🙈

Object — базовый класс для остальных классов. В родительский класс помещают методы, полезные для подклассов. Пишутся удобные реализации по умолчанию. Но Object явно не следует этим рекомендациям.

Возьмём метод hashCode. Для чего объекту нужен хэш?

Для некоторых алгоритмов и структур данных. Чаще всего хэш используется в HashMap или HashSet.

Но как часто объект становится ключом в HashMap? Часто ли в бизнес-логике используются хэши? Нужен ли hashCode каждому классу?

Вряд ли.

А ещё есть контракт equals/hashcode. Он висит невидимой тенью (хорошей практикой) над каждым разработчиком. Хочешь сравнивать объекты через equals — не забудь определить hashСode. Даже если хэш в коде не нужен.

Как можно по-другому?

Альтернативный путь использует compareTo. Его нет в наборе методов обжекта. Чтобы сравнить объекты или сложить их в TreeSet, мы либо реализуем Comparable, либо передаём логику сравнения через Comparator.

Такой подход отлично подошёл бы для хэша!

Захотим использовать объект внутри хэш-структуры — реализуем интерфейс или передадим лямбду в HashMap. Захотим посчитать хэш для контрольной суммы — просто реализуем метод, где это необходимо.
Не думаем про хэш, когда он не нужен
Компилятор укажет, где хэш не определен, но используется

В других языках, кстати, нет по умолчанию всеобщего хэша. В С++ используется compareTo-подход. В Go не всё может стать ключом, зато хэш для таблицы автоматом считается по всем полям. Мне оба варианта нравятся, работа с хэшем более явная и предсказуемая👌

А что с другими методами Object?

К ним тоже вопросики. Нужны ли каждому классу методы wait и notify? Почему clone такой странный? Зачем нужен equals по умолчанию, если внутри просто ==? Хорошо хоть finalize отметили как deprecated.

Польза базовых hashCode, equals, toString похоже чисто синтаксическая. Показать, что именно можно переопределить.

Итого: лишние методы, слабые варианты по умолчанию. Поэтому общая оценка Object с точки зрения API - слабая троечка

PS
В целом java и её вселенная — конфетка. Критикую любя и чисто в образовательных целях. Посмотреть на привычные вещи под другим углом и подумать "как можно иначе" всегда интересно
👍150🔥5444👎24
Java 25: новый формат конструктора

Сегодня расскажу про новую фичу в осенней джаве. Суть простая - this и super не обязательно должны идти первой строкой в конструкторе.

Зачем это нужно? Чтобы упростить валидацию.

Сейчас, чтобы добавить проверку аргументов, приходится оборачивать аргументы в методы:
class Employee extends Person {
  private static int verifyAge(int value) {
  if (age < 18)
    throw new IllegalArgumentException(...);
    return value;
  }

  Employee(int age) {
     super(verifyAge(age));
  }
}


С новым JEP эти костыли не нужны, нужные проверки пишем в начале конструктора:
class Employee extends Person {
  Employee(int age) {
  if (age < 18)
   throw new IllegalArgumentException(...);
  super(age);
  }
}


Области кода вокруг this/super называются очень литературно: пролог и эпилог🥰
public Person {
   // prologue
   super();
   // epilogue
}

В пролог нельзя вставить любой код:
Нельзя обращаться к переменным родителя
Нельзя вызывать нестатические методы
Нельзя вызвать return
Можно присвоить поля текущего класса

Особо не разгуляешься, всё же основной сценарий фичи — валидация входных параметров.

Ещё из интересного:

1️⃣ В JVM не пришлось ничего менять

Потому что правила "this обязательно первый" в JVM нет. Это ограничение только на уровне языка, чтобы упростить работу компилятора:)

2️⃣ Меняется ответ на частый собесный вопрос "в каком порядке инициализируются переменные". Раньше порядок для нестатических полей был такой:

Поля Parent - Конструктор Parent - Поля Child - Конструктор Child

В Java 25 поля наследника можно инициализировать ДО вызова конструктора родителя:
Employee(int age, String officeID) {
   this.officeID = officeID;
   super(age);
}

Общая схема с этими прологами-эпилогами очень усложняется.

3️⃣ Фича называется Flexible Constructor Bodies. Я не смогла придумать адекватный перевод, поэтому очень интересно, как её переведут в статьях-обзорах:)
🔥106👍5222👎6
Как прогреть кэши в Spring Boot?

и вообще сделать что-то на старте приложения?

Обычно подобные операции делают в методе с PostConstruct или ловят событие ApplicationReadyEvent. Но у таких вариантов есть существенный минус.

Если "прогрев" находится в PostConstruct, как отключить его в тестах? Можно добавить флажок, сделать подкласс и заменить его в тестовом конфиге, но это не всегда помогает и часто выглядит как костыль.

Расскажу более аккуратный способ "прогреть кэши". Однажды точно пригодится💯

Смотрите, SpringApplication.run(...) возвращает полностью готовый контекст. Можно достать из него компонент и вызвать нужный метод "прогрева". В коде выглядит так:
@SpringBootApplication
public class MainApplication {
  public static void main(String[] args) {
    ApplicationContext ctx = SpringApplication.run(MainApplication.class, args);
    AccountService accService = ctx.getBean(AccountService.class);
    accService.loadDictionary();
  }
}

В чём плюс?

В интеграционных тестах с @SpringBootTest метод main не запускается. Соответственно, код внутри не выполняется. Нет костылей вокруг PostConstruct, всё чисто и красиво❤️‍🩹

Когда в тестах все же нужен "прогрев", добавляем параметр "использовать main метод":
@SpringBootTest(useMainMethod = SpringBootTest.UseMainMethod.ALWAYS)

Итого. Если код должен выполниться после старта, но будет мешаться в тестах — пишите его в main. Очень полезный приём🔥
🔥219100👍40👎32
Вопрос на понимание транзакций.

Есть таблица c твитами. На каждый твит можно ответить, id "исходного" твита сохраняется в поле parent_id:
CREATE TABLE tweets (
id BIGINT ... PRIMARY KEY,
text VARCHAR(255) NOT NULL,
parent_id BIGINT REFERENCES tweets(id)
);


Код сохранения твита примерно такой:
public Tweet saveTweet(Tweet tweet) {
// проверяем, что родитель существует
if (tweet.getParentId() != null) {
boolean parentExists = tweetRepository.existsById(tweet.getParentId());
if (!parentExists) {
throw new IllegalArgumentException("Parent not exists");
}
}
return tweetRepository.save(tweet);
}


Какую аннотацию ставим над методом saveTweet?
👍254
Какую аннотацию ставим над методом saveTweet?
Anonymous Poll
27%
@Transactional(isolation = Isolation.REPEATABLE_READ)
9%
@Transactional(isolation = Isolation.SERIALIZABLE)
31%
Никакую
👍19🔥63👎1
Сохранение сущностей внутри Transactional

Часто встречаю такую логику при работе с Spring Data:

💁: Если в одном методе несколько обращений к БД, надо ставить Transactional.

К сожалению, на практике всё сложнее. В этом посте разберу работу с транзакциями на примере из задачки выше.

Итак, вот у нас код. Внутри метода несколько обращений к БД. Что может пойти не так?

Допустим, пользователь написал провокационный твит, потом одумался и удалил его. Но другие пользователи успели увидеть и настрочили гневных ответов. Что делать, если родительская сущность удалится после проверки existsById? Никто этому не мешает, удаление происходит в другом запросе.

Аналитик говорит: если родительский твит удалён, гневные ответы не сохраняем. Пользователю шлём сообщение "не получилось".

🤔 Как это реализовать?

Транзакция - это не аналог synchronized, она не запрещает другим транзакциям менять данные.

Задача решается на уровне ограничений в БД. Указываем, что parent_id - это ссылка на другую запись. Если на момент вставки не будет поля с таким id, получим DataIntegrityViolationException. Ну или concurrent update при уровне изоляции Repeatable Read и выше.

Уровень изоляции влияет на то, с какими данными мы работаем внутри транзакции. Но в данном случае это не важно. Итоговая проверка происходит в момент вставки. В коде выше Transactional не нужен.

Наблюдение из опыта: в Spring Data аннотация ставится за полсекунды, и осмысление иногда занимает столько же:) И либо Transactional вообще не ставят, и за счёт низких нагрузок проблем не возникает. Либо ставят на каждый чих и упираются в проблему с соединениями для вроде бы небольшой нагрузки.

Не надо так, будьте внимательнее с транзакциями в своем коде и на код-ревью❤️
👍10842🔥35👎29
Вопрос для матёрых бекендеров. Укажите элемент, который отличается от остальных в группе:
Anonymous Poll
33%
2181
14%
5432
16%
6379
9%
9092
29%
9200
🔥28👍9👎6
Протекающая абстракция

Магические числа в вопросе выше — это порты. В этом посте расскажу, зачем они нужны и что с ними не так.

Немного теории. Компьютеры обмениваются данными по модели OSI. Каждый запрос проходит через 7 этапов(уровней) на компьютере отправителя, передаётся по сети, потом проходит те же этапы на компьютере получателя в обратном порядке. Упрощённо, процесс выглядит так:

▫️Пользователь шлёт select запрос в БД
▫️▫️Запрос разбивается на TCP пакеты
⚡️⚡️⚡️Пакеты передаются по сети и прибывают на сервер БД
▫️▫️TCP пакеты собираются в select запрос
▫️Запрос выполняется в базе данных

По сети передаются миллиарды пакетов вперемешку. Чтобы понять, какие пакеты к чему относятся, в каждый пакет добавляется поле "порт". Получатель собирает пакеты с полем 5432 и преобразует их в SQL запрос.

Хорошая абстракция скрывает детали реализации и упрощает жизнь пользователю. Если пользователю приходится учитывать детали реализации, такая абстракция называется протекающей (leaky abstraction).

Порт — яркий пример протекающей абстракции.

Теоретически в модели OSI каждый уровень занят своим делом и рассматривает данные соседнего уровня как чёрный ящик.

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

Есть другие транспортные протоколы и схемы адресации, которым достаточно только IP-адреса. Но стек TСP/IP самый распространённый, хорошо работает, да и к портам все привыкли:)

Ответ на вопрос перед постом. Основная группа — это порты сервисов по работе с данными: 5432 - Postgres, 6379 - Redis, 9092 - Kafka, 9200 - ElasticSearch. Порт 2181 использует Zookeeper, он занимается координацией сервисов👩‍✈️
🔥116👍44👎2913
LinkedHashMap и проектирование API

Если я больше 3 месяцев не пишу посты про хэшмэп, значит у меня угнали канал.

Люблю разбирать классы в JDK. Они используются каждый день и интересны сами по себе. А ещё на их примере удобно объяснять разные концепции, хорошие и плохие практики.

На этой неделе расскажу пару неочевидных моментов на основе класса LinkedHashMap.

Начнём с базы. LinkedHashMap - это HashMap, внутри которого есть связный список. По умолчанию список сохраняет порядок вставки элементов:
LinkedHashMap<Integer, Integer> map = new LinkedHashMap<>();
map.put(1, 1);
map.put(2, 2);
int lastKey = map.sequencedKeySet().getLast(); // 2


Если в конструкторе передать accessOrder=true, список запоминает порядок извлечения элементов. Вызов get(1) отправляет 1 в конец списка:
LinkedHashMap<Integer, Integer> map = new LinkedHashMap<>(16, 0.8f, true);
map.put(1, 1);
map.put(2, 2);

map.get(1);

int lastKey = map.sequencedKeySet().getLast(); // 1


С теорией закончили, переходим к интересному.

В первую очередь, бросается в глаза флажок в конструкторе. Гораздо симпатичнее в параметрах выглядел бы enum. Что-то вроде ELEMENT_ORDER.INSERT. Подробно бэд пректис с флажками и альтернативы разбирала тут.

Второй момент касается проектирования. Зачем вообще нужен LinkedHashMap с accessOrder=true?

Документация пишет, что это отличная база для LRU кэша. С первого взгляда похоже на правду, но есть пара нюансов:
▫️ У LinkedHashMap нет ограничений на размер. У кэша — есть
▫️ С кэшами работают много потоков. LinkedHashMap - не потокобезопасен, единственный вариант для корректной работы — synchronized обёртка:
Map map = Collections.synchronizedMap(new LinkedHashMap(…)) 

Получится кэш с пропускной способностью в один поток😐

В итоге:
В чистом виде LinkedHashMap с accessOrder=true нужен либо никому, либо в редких случаях
В качестве LRU кэша (как предлагается в документации) класс использовать сразу не получится. Либо доделывать, либо взять уже готовые и более эффективные реализации кэша.

Подобные опции в API - лишние.

Хорошие библиотеки и фреймворки состоят из двух частей: базовые многофункциональные элементы + удобные методы для популярных кейсов. Так достигается баланс между простотой и гибкостью.

Хороший пример: экзекьюторы

Базовый элемент для экзекьютора — класс ThreadPoolExecutor. В конструкторе 5 параметров, можно переопределить методы. Есть готовые варианты, которые подойдут для большинства задач:
Executors.newFixedThreadPool(int nThreads) 
Executors.newSingleThreadExecutor()
Executors.newCachedThreadPool()

Всё вместе - приятное и удобное апи.

Примеры выше простые, но сам подход универсален. Комбо базовых и удобных методов отлично подходит для библиотек интеграции или для общих модулей.

Ответ на вопрос перед постом: с флажком true запоминается порядок извлечения элементов. Подписчики канала - умнички независимо от каких-то флажков🥰
🔥125👍4234👎3