Предпочитай методы, реализующие функциональные интерфейсы, а не возвращающие функциональные интерфейсы
Довольно часто замечал, особенно среди тех, кто сильно вдохновился функциональным программированием и старается его применить где только можно, вот такой код:
Но гораздо приятнее и более читабельнее будет преобразовать метод так, чтобы он не возвращал функциональный интерфейс
Также хотелось бы добавить, что две парадигмы программирования, функциональное и объектно-ориентированное, не исключают друг друга. Каждая имеет свои плюсы и минусы: какие-то задачи лучше решать с помощью функционального стиля, какие-то с помощью объектно-ориентированного.
#dmdev_best_practices
Довольно часто замечал, особенно среди тех, кто сильно вдохновился функциональным программированием и старается его применить где только можно, вот такой код:
csvRows.stream()
.filter(csvRowValidatorPredicate(fileContext))
.toList();
Predicate<CsvRow> csvRowValidatorPredicate(FileContext context) {
return csvRow -> context.findSuitableValidator(csvRow).isValid();
}
Но гораздо приятнее и более читабельнее будет преобразовать метод так, чтобы он не возвращал функциональный интерфейс
csvRows.stream()
.filter(row -> isCsvRowValid(row, fileContext))
.toList();
boolean isCsvRowValid(CsvRow row, FileContext context) {
return context.findSuitableValidator(row).isValid();
}
Также хотелось бы добавить, что две парадигмы программирования, функциональное и объектно-ориентированное, не исключают друг друга. Каждая имеет свои плюсы и минусы: какие-то задачи лучше решать с помощью функционального стиля, какие-то с помощью объектно-ориентированного.
Но как обычно - правда где-то по середине. Поэтому нужно комбинировать плюсы обоих подходов для получение наболее эффективного и читабельного кода, а не наоборот (как в примере выше).
#dmdev_best_practices
👍70🔥19❤🔥9💯3🤔1
Lambdas vs Method references
С предыдущего поста мы уже поняли, что лямбда-выражения лучше всего использовать для небольших и простых фрагментов кода, которые занимают в идеале 1 строчку, а максимум субъективен для каждого, но все-таки не должен превышать 3-5 строк.
С другой стороны в Java есть ссылки на метод (method reference) - это альтернативный синтаксис лямбда-выражения, который, по сути, передает параметры лямбда-выражения именованному методу.
Но когда/что лучше использовать?
В принципе, нужно склоняться в пользу method reference когда только это возможно. Ссылки на методы столь же эффективны, а иногда даже более эффективны, чем лямбда-выражения (правильнее даже сказать, что под-капотом лямбда-выражения преобразуются в ссылки на методы, чем наоборот). Особенно если лямбда-выражение становится слишком длинным - просто перенеси его тело в метод и вместо этого используй ссылку на метод.
К сожалению, при определенных обстоятельствах, например, когда приходится указывать название класса, а оно длинное - ссылки на методы лучше не использовать в угоду читаемости кода:
#dmdev_best_practices
С предыдущего поста мы уже поняли, что лямбда-выражения лучше всего использовать для небольших и простых фрагментов кода, которые занимают в идеале 1 строчку, а максимум субъективен для каждого, но все-таки не должен превышать 3-5 строк.
С другой стороны в Java есть ссылки на метод (method reference) - это альтернативный синтаксис лямбда-выражения, который, по сути, передает параметры лямбда-выражения именованному методу.
Но когда/что лучше использовать?
В принципе, нужно склоняться в пользу method reference когда только это возможно. Ссылки на методы столь же эффективны, а иногда даже более эффективны, чем лямбда-выражения (правильнее даже сказать, что под-капотом лямбда-выражения преобразуются в ссылки на методы, чем наоборот). Особенно если лямбда-выражение становится слишком длинным - просто перенеси его тело в метод и вместо этого используй ссылку на метод.
К сожалению, при определенных обстоятельствах, например, когда приходится указывать название класса, а оно длинное - ссылки на методы лучше не использовать в угоду читаемости кода:
// Lambda expression выглядит приятнее
.map(it -> splitToColumns(it))
// Чем аналогичный method reference
.map(UserDataCsvFileConvertorUtils::splitToColumns)
Тем не менее, в целом ссылки на методы обычно более компактны, чем лямбда-выражения, и им следует отдавать предпочтение, даже если они немного длиннее.
#dmdev_best_practices
🔥50👍23❤🔥5
Генерация тестовых данных
Чтобы написать хороший тест, нужны хорошие тестовые данные, приближенные к production. Плохо подготовленные данные = плохо написанный тест.
Поэтому практически все тесты должны состоять из трех основных частей:
- given (подготовка данных и стабов для mock/spy)
- when (вызов тестируемого API)
- then (проверка результата)
Сложно написать хороший тест, полагаясь на данные, которые уже существуют в базе в момент запуска теста (за исключением справочных данных или тех, что были накатаны на production с помощью миграционных фреймворков вроде liquibase и flyway). Обычно на эти данные полагаются другие тесты, а потому часто меняются, что ломает наши тесты или делает их даже flaky.
Поэтому каждый тест должен в идеале готовить данные только для себя, на которых он планирует проверить API:
А чтобы не испортить состояние базы во время проверки, то:
- открываем транзакцию ПЕРЕД выполнением теста (@BeforeEach)
- накатываем данные, вызываем API и проверяем результат (@Test)
- откатываем транзакцию в конце (@AfterEach)
#dmdev_best_practices
Чтобы написать хороший тест, нужны хорошие тестовые данные, приближенные к production. Плохо подготовленные данные = плохо написанный тест.
Поэтому практически все тесты должны состоять из трех основных частей:
- given (подготовка данных и стабов для mock/spy)
- when (вызов тестируемого API)
- then (проверка результата)
Сложно написать хороший тест, полагаясь на данные, которые уже существуют в базе в момент запуска теста (за исключением справочных данных или тех, что были накатаны на production с помощью миграционных фреймворков вроде liquibase и flyway). Обычно на эти данные полагаются другие тесты, а потому часто меняются, что ломает наши тесты или делает их даже flaky.
Поэтому каждый тест должен в идеале готовить данные только для себя, на которых он планирует проверить API:
@Test
void findAll() {
// given
// Все компактно, содержит только необходимую информацию для программиста
User user1 = userDao.save(getUser("[email protected]"));
User user2 = userDao.save(getUser("[email protected]"));
User user3 = userDao.save(getUser("[email protected]"));
// when
List<User> actualResult = userDao.findAll();
// then
// Легко получить доступ к id объектов, т.к. накатывание данных было в самом тесте
assertThat(actualResult).hasSize(3);
List<Integer> userIds = actualResult.stream()
.map(User::getId)
.toList();
assertThat(userIds).contains(user1.getId(), user2.getId(), user3.getId());
}
А чтобы не испортить состояние базы во время проверки, то:
- открываем транзакцию ПЕРЕД выполнением теста (@BeforeEach)
- накатываем данные, вызываем API и проверяем результат (@Test)
- откатываем транзакцию в конце (@AfterEach)
#dmdev_best_practices
👍63🔥25❤🔥7
Wildcards
Параметризация является одним из самых мощных, но в то же самое время одним и самых сложных инструментов в Java. Как сейчас помню, что начал ее понимать как положено только после года работы инженер-программистом.
Поэтому не стоит усложнять свой API еще больше, когда можно обойтись, например, wildcards вместо создания нового параметризованного типа:
Ну и еще раз напомню то, что объяснял на курсе Java Level 2 (Generics), и что является ключевым в понимании параметризации и wildcars в Java - это аббревиатура PECS.
PECS - Producer: Extends; Consumer: Super
PS. Более подробно про Generics также можно почитать в топ 1 книге для джавистов Effective Java (Item 31)
#dmdev_best_practices
Параметризация является одним из самых мощных, но в то же самое время одним и самых сложных инструментов в Java. Как сейчас помню, что начал ее понимать как положено только после года работы инженер-программистом.
Поэтому не стоит усложнять свой API еще больше, когда можно обойтись, например, wildcards вместо создания нового параметризованного типа:
// Wrong
// Параметр <T> просто мешает чтению сигнатуры метода и не дает никакой пользы.
public <T extends Number> void update(Collection<T> ids) { ... }
// Right
public void update(Collection<? extends Number> ids) { ... }
Ну и еще раз напомню то, что объяснял на курсе Java Level 2 (Generics), и что является ключевым в понимании параметризации и wildcars в Java - это аббревиатура PECS.
PECS - Producer: Extends; Consumer: Super
// Метод добавляет (produce) элементы в коллекцию, поэтому Extends
void addAll(Collection<? extends E> collection) {
for (E item : c) {
add(item);
}
}
// Объект filter потребяет (consume) элемент из коллекции, чтобы отвалидировать его, поэтому Super
void removeIf(Predicate<? super E> filter) {
Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
}
}
}
PS. Более подробно про Generics также можно почитать в топ 1 книге для джавистов Effective Java (Item 31)
#dmdev_best_practices
🔥72👍28❤🔥4
Interruption
Остановить поток выполнения - не совсем тривиальная задача. Мы не знаем, какие ресурсы были заняты, а значит и не знаем, как их освободить. Поэтому еще во 2 версии Java метод stop был deprecated. Теперь мы можем только послать "сигнал" потоку, чтобы он увидел, что мы хотим "прервать" его выполнение. Тогда он сможет спокойно завершиться и почистить все необходимые ресурсы.
Когда в принципе может понадобится это прерывание? На самом деле основных вариантов немного (но встречаются очень часто в приложениях):
- graceful shutdown (всего приложения или его части)
- cancel (пользователь хочет отменить выполнение задачи)
- timeouts (задача/запрос выполняется дольше, чем отведено на это время)
- остановить ненужные параллельные задачи
Раньше я пытался реализовывать свой механизм отмены выполнения задачи, чтобы не мучиться с InterruptedException. Но в итоге код сводился к тому, что мне приходилось и проверять свой велосипед, и стандартный механизм interrupt, потому что он никуда не уходит.
Поэтому я выработал для себя несколько основных правил, которым всегда следую:
1. Когда это только возможно в логике (чаще всего перед каждой итерацией цикла) - проверяй Thread.isInterrupted()
2. Если ты увидел, что нужно прервать выполнение - то делаешь весь необходимый cleanup и пробрасываешь дальше InterruptedException
3. Если пробросить InterruptedException не получается (например, нужно свое кастомное исключение), то обязательно вызови Thread.currentThread().interrupt(), чтобы восстановить interrupted status
#dmdev_best_practices
Остановить поток выполнения - не совсем тривиальная задача. Мы не знаем, какие ресурсы были заняты, а значит и не знаем, как их освободить. Поэтому еще во 2 версии Java метод stop был deprecated. Теперь мы можем только послать "сигнал" потоку, чтобы он увидел, что мы хотим "прервать" его выполнение. Тогда он сможет спокойно завершиться и почистить все необходимые ресурсы.
Когда в принципе может понадобится это прерывание? На самом деле основных вариантов немного (но встречаются очень часто в приложениях):
- graceful shutdown (всего приложения или его части)
- cancel (пользователь хочет отменить выполнение задачи)
- timeouts (задача/запрос выполняется дольше, чем отведено на это время)
- остановить ненужные параллельные задачи
Раньше я пытался реализовывать свой механизм отмены выполнения задачи, чтобы не мучиться с InterruptedException. Но в итоге код сводился к тому, что мне приходилось и проверять свой велосипед, и стандартный механизм interrupt, потому что он никуда не уходит.
Поэтому я выработал для себя несколько основных правил, которым всегда следую:
1. Когда это только возможно в логике (чаще всего перед каждой итерацией цикла) - проверяй Thread.isInterrupted()
2. Если ты увидел, что нужно прервать выполнение - то делаешь весь необходимый cleanup и пробрасываешь дальше InterruptedException
3. Если пробросить InterruptedException не получается (например, нужно свое кастомное исключение), то обязательно вызови Thread.currentThread().interrupt(), чтобы восстановить interrupted status
try {
logicThatCanBeTimeOutOrCancelled();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // Restore the interrupted status
throw new MyCheckedException("describe what task was interrupted", e);
}
for (Path file : files) {
if (Thread.isInterrupted()) { // Check interruption before each iteration
// do cleanup and throw an InterruptedException
throw new InterruptedException();
}
doExpensiveTask(file);
}
#dmdev_best_practices
🔥72👍23❤🔥3🎉3
Избегай длительного выполнения задач
Довольно часто на практике необходимо реализовывать задачи или scheduled jobs, которые должны долго выполнять какую-то работу. Например:
- рассылка недельных отчетов (статистика) для миллионов пользователей в системе
- обновление статуса всех expired подписок
- обработка файлов/фото/видео
Если такие задачи решать в лоб, то можно просто создать отдельный поток, который, например, начинает идти по всем пользователям в базе данных и рассылать им email.
Но какие проблемы мы получаем в таком случае?
1️⃣ Мы даже примерно не знаем, когда закончит выполнение задача. А в это время мы хотели бы рестартовать приложение, выкатывать новые версии его и т.д. - что означает прерывание задачи (что мы писали в предыдущем посте) и последующий запуск с того момента, на котором мы остановились.
2️⃣ Сложно распределить нагрузку между несколькими инстансами сервиса. Чаще всего такие задачи можно запустить только на одном, иначе возникает конфликт (который еще нужно избежать, потратив время на соответствующую реализацию!).
3️⃣ Трудоемкие долго живущие задачи могут замедлять оставшийся функционал сервиса, что негативно скажется на пользователях, а мы не сможем даже это исправить.
4️⃣ Такие задачи сложны в поддержке и обслуживании (как остановить ее? как поставить на паузу? как перезапустить? на все эти операции писать отдельный API?). Они более подвержены ошибкам и требуют больше усилий для исправления и оптимизации.
Это помогает распределить нагрузку на систему, упростить отладку и обслуживание, а также обеспечить более быструю реакцию на изменения и улучшить общую производительность. Кроме того, такой подход позволяет более гибко и эффективно управлять ресурсами и временем разработки.
#dmdev_best_practices
Довольно часто на практике необходимо реализовывать задачи или scheduled jobs, которые должны долго выполнять какую-то работу. Например:
- рассылка недельных отчетов (статистика) для миллионов пользователей в системе
- обновление статуса всех expired подписок
- обработка файлов/фото/видео
Если такие задачи решать в лоб, то можно просто создать отдельный поток, который, например, начинает идти по всем пользователям в базе данных и рассылать им email.
Но какие проблемы мы получаем в таком случае?
1️⃣ Мы даже примерно не знаем, когда закончит выполнение задача. А в это время мы хотели бы рестартовать приложение, выкатывать новые версии его и т.д. - что означает прерывание задачи (что мы писали в предыдущем посте) и последующий запуск с того момента, на котором мы остановились.
2️⃣ Сложно распределить нагрузку между несколькими инстансами сервиса. Чаще всего такие задачи можно запустить только на одном, иначе возникает конфликт (который еще нужно избежать, потратив время на соответствующую реализацию!).
3️⃣ Трудоемкие долго живущие задачи могут замедлять оставшийся функционал сервиса, что негативно скажется на пользователях, а мы не сможем даже это исправить.
4️⃣ Такие задачи сложны в поддержке и обслуживании (как остановить ее? как поставить на паузу? как перезапустить? на все эти операции писать отдельный API?). Они более подвержены ошибкам и требуют больше усилий для исправления и оптимизации.
Поэтому предпочтительнее разбивать сложные задачи на более мелкие, более управляемые подзадачи
Например:
- запускать задачу гораздо чаще и обрабатывать только batch пользователей (например, по 1000)
- ограничивать по времени (например, отсылать emails не более минуты - после чего завершать работу).
Это помогает распределить нагрузку на систему, упростить отладку и обслуживание, а также обеспечить более быструю реакцию на изменения и улучшить общую производительность. Кроме того, такой подход позволяет более гибко и эффективно управлять ресурсами и временем разработки.
#dmdev_best_practices
🔥68👍19❤🔥4💯1
Не следует сохранять лямбда-выражения в переменных/константах
Возможно, кто-то как и я встречался с кодом, где лямбда-выражения выносятся в переменные внутри метода или даже константы, чтобы якобы улучшить производительность приложения. Выглядит это примерно так:
На самом деле, тут не все так плохо, но все-таки лучше рассматривать другой более читабельный вариант, где лямбда-выражения остаются внутри Stream API/Optional или вообще выносятся в отдельные методы:
Здесь также есть другие преимущества:
- код будет разрастаться, и естественнее это будет выглядеть в методах, а не переменной
- намного проще именовать методы, чем переменные
- более естественно тестировать в последующем методы, а не переменные
- если поведение не совсем очевидно и хитро, то проще будет написать javadoc для метода
И последнее:
#dmdev_best_practices
Возможно, кто-то как и я встречался с кодом, где лямбда-выражения выносятся в переменные внутри метода или даже константы, чтобы якобы улучшить производительность приложения. Выглядит это примерно так:
Function<String, CsvRow> CONVERT_TO_CSV_ROW_FUNCTION =
line -> new CsvRow(line.split(","));
Predicate<CsvRow> VALIDATE_CSV_ROW_PREDICATE =
csvRow -> !csvRow.hasEmptyColumns();
return lines.stream()
.map(CONVERT_TO_CSV_ROW_FUNCTION)
.filter(VALIDATE_CSV_ROW_PREDICATE)
.toList();
На самом деле, тут не все так плохо, но все-таки лучше рассматривать другой более читабельный вариант, где лямбда-выражения остаются внутри Stream API/Optional или вообще выносятся в отдельные методы:
return lines.stream()
.map(line -> new CsvRow(line.split(",")))
.filter(csvRow -> !csvRow.hasEmptyColumns())
.toList();
Здесь также есть другие преимущества:
- код будет разрастаться, и естественнее это будет выглядеть в методах, а не переменной
- намного проще именовать методы, чем переменные
- более естественно тестировать в последующем методы, а не переменные
- если поведение не совсем очевидно и хитро, то проще будет написать javadoc для метода
И последнее:
Нет необходимости сохранять лямбда-выражения в константы, чтобы "улучшить производительность" приложения. JVM оптимизирует те участки кода, где это необходимо. И делает это очень даже умно! Лучше обратить свое внимание на IO операции: там 90%+ проблемных по перфомансу мест.
#dmdev_best_practices
👍72🔥22❤7❤🔥2
Используй deadline propagation
Обычно, каждый RPC запрос должен выполняться с установленным deadline, который означает максимальное количество времени, отведенное на выполнение этого запроса. Если время превышает максимально допустимое - прерывается выполнение запроса и пробрасывается timout exception. Более того, сервер может пробросить это исключение сразу же, если видит, что deadline короче, чем требуется для выполнения запроса.
Но зачем в принципе использовать deadlines?
- чтобы не допускать запросов, которые выполняются бесконечно (особенно критично для user-facing)
- чтобы не тратить ресурсы сервера на поддержание долгих/бесконечных запросов
Зачем тогда deadline propagation?
Дело в том, что зачастую сервер не обрабатывает запрос полностью сам, а делает дополнительно другие downstream RPC запросы. И тогда становится вопрос - какой deadline устанавливать для них? Устанавливать какой-то дефолтный - плохо, он может быть меньше или больше необходимого. Поэтому deadline высчитывается из оставшегося времени, которое отвел на выполнение инициатор запроса (например, frondend).
Например, как на картинке:
- Client (инициатор) отправляет запрос с deadline = 10 секундам
- Server A обрабатывает запрос сам за 5 секунд и делает 2 асинхронных RPCs в Server B и D, установив каждому deadline = 5 секунд (10 - 5)
- Server B обрабывает запрос сам за 3 секунды и делает 1 RPC запрос в Server C, установив deadline = 2 секунды (5 - 3)
Как это работает?
- в случае синхронных серверов, вроде Apache Tomcat, то можно просто хранить значение deadline в ThreadLocal и при выполнении любой IO операции - получать ее оттуда (иначе использовать дефолтный или не ограничивать вовсе)
- в случае асинхронных серверов, вроде Netty, использовать аналог ThreadLocal - это Local Context.
#dmdev_best_practices
Обычно, каждый RPC запрос должен выполняться с установленным deadline, который означает максимальное количество времени, отведенное на выполнение этого запроса. Если время превышает максимально допустимое - прерывается выполнение запроса и пробрасывается timout exception. Более того, сервер может пробросить это исключение сразу же, если видит, что deadline короче, чем требуется для выполнения запроса.
Но зачем в принципе использовать deadlines?
- чтобы не допускать запросов, которые выполняются бесконечно (особенно критично для user-facing)
- чтобы не тратить ресурсы сервера на поддержание долгих/бесконечных запросов
Зачем тогда deadline propagation?
Дело в том, что зачастую сервер не обрабатывает запрос полностью сам, а делает дополнительно другие downstream RPC запросы. И тогда становится вопрос - какой deadline устанавливать для них? Устанавливать какой-то дефолтный - плохо, он может быть меньше или больше необходимого. Поэтому deadline высчитывается из оставшегося времени, которое отвел на выполнение инициатор запроса (например, frondend).
Например, как на картинке:
- Client (инициатор) отправляет запрос с deadline = 10 секундам
- Server A обрабатывает запрос сам за 5 секунд и делает 2 асинхронных RPCs в Server B и D, установив каждому deadline = 5 секунд (10 - 5)
- Server B обрабывает запрос сам за 3 секунды и делает 1 RPC запрос в Server C, установив deadline = 2 секунды (5 - 3)
Как это работает?
- в случае синхронных серверов, вроде Apache Tomcat, то можно просто хранить значение deadline в ThreadLocal и при выполнении любой IO операции - получать ее оттуда (иначе использовать дефолтный или не ограничивать вовсе)
- в случае асинхронных серверов, вроде Netty, использовать аналог ThreadLocal - это Local Context.
#dmdev_best_practices
🔥56👍14❤🔥3🥰2❤1
JUnit assertions
Во фреймворке JUnit есть свои классические методы для проверки ожидаемого (expected) и актуального (actual) значений. Но как показывает практика, довольно часто программисты делают ошибки, путая местами параметры actual и expected. Из-за этого логи могут вводишь в замешательство, почему, например, актуальное значение не верно, хотя на самом деле - оно просто попутано местами.
Именно поэтому я предпочитаю использовать дополнительные библиотеки, такие как AssertJ или Truth. Потому что я всегда знаю, что на вход каждый метод assertThat принимает именно актуальное значение. Кстати, в своем курсе JUnit 5 я более подробно рассказывал про это.
Есть только несколько исключений, когда я могу воспользоваться классическими методами из JUnit 5, потому что они короче и проще/лучше читаются:
В остальных случаях все-таки не следует мешать в одном тесте сразу несколько библиотек, а предпочитать одну!
#dmdev_best_practices
Во фреймворке JUnit есть свои классические методы для проверки ожидаемого (expected) и актуального (actual) значений. Но как показывает практика, довольно часто программисты делают ошибки, путая местами параметры actual и expected. Из-за этого логи могут вводишь в замешательство, почему, например, актуальное значение не верно, хотя на самом деле - оно просто попутано местами.
// Только заглянув в название параметров, можно понять где что
public static void assertEquals(Object expected, Object actual) {
AssertEquals.assertEquals(expected, actual);
}
Именно поэтому я предпочитаю использовать дополнительные библиотеки, такие как AssertJ или Truth. Потому что я всегда знаю, что на вход каждый метод assertThat принимает именно актуальное значение. Кстати, в своем курсе JUnit 5 я более подробно рассказывал про это.
// Пример метода assertThat из библиотеки AssertJ
public static <T> ObjectAssert<T> assertThat(T actual) {
return AssertionsForClassTypes.assertThat(actual);
}
Есть только несколько исключений, когда я могу воспользоваться классическими методами из JUnit 5, потому что они короче и проще/лучше читаются:
assertNull(Object actual);
assertNotNull(Object actual);
assertTrue(boolean condition);
assertFalse(boolean condition);
В остальных случаях все-таки не следует мешать в одном тесте сразу несколько библиотек, а предпочитать одну!
#dmdev_best_practices
👍46❤15🔥8
Не отправляй stale notifications пользователю
На практике довольно часто приходится отправлять различные уведомления пользователю (email, in-app notifications), когда происходит какое-то событие. Например:
- у пользователя заканчивается срок действия подписки
- он пробежал свой первый марафон и нужно вручить особенный badge
- необходимо поставить электронную подпись на только что отправленные документы
- пришел счет на оплату электроэнергии
- и т.д.
И также очень часто такие уведомление отправляются асинхронно от основной логики приложения, потому что если что-то пойдет не так с отправкой - это не должно останавливать или повторять заново основную логику. Да и время не хочется тратить на ожидание того, когда закончится отправка уведомления.
Поэтому их часто складывают в какую-нибудь очередь в базе данных, или в Message Brokers вроде Kafka.
И казалось бы, что может пойти не так - но на деле сообщения из таких источников могут приходить в места их обратки с запозданием (а порой и с запозданием в несколько дней/недель, если что-то пошло не так). Получить badge об успешном прохождении марафонской дистанции через пару дней - хоть и не приятно, но еще не критично. Но вот поставить электронную подпись на документы, которые нужно было подписать еще несколько недель назад - уже более критично. Не говоря про оплату элекроэнергии за прошлый месяц, когда уже пора платить за новый и с пеней.
- действительно ли дата предполагаемой отправки не outdated?
- действительно ли событие (истекла подписка) произошло совсем недавно, а не далеко в прошлом?
- действительно ли состояние пользователя в базе данных сооветствует отправляемому уведомлению?
- и т.д.
Только благодаря таким double checks получится избежать отправки stale notifications и не вводить пользователя в заблуждение!
#dmdev_best_practices
На практике довольно часто приходится отправлять различные уведомления пользователю (email, in-app notifications), когда происходит какое-то событие. Например:
- у пользователя заканчивается срок действия подписки
- он пробежал свой первый марафон и нужно вручить особенный badge
- необходимо поставить электронную подпись на только что отправленные документы
- пришел счет на оплату электроэнергии
- и т.д.
И также очень часто такие уведомление отправляются асинхронно от основной логики приложения, потому что если что-то пойдет не так с отправкой - это не должно останавливать или повторять заново основную логику. Да и время не хочется тратить на ожидание того, когда закончится отправка уведомления.
Поэтому их часто складывают в какую-нибудь очередь в базе данных, или в Message Brokers вроде Kafka.
И казалось бы, что может пойти не так - но на деле сообщения из таких источников могут приходить в места их обратки с запозданием (а порой и с запозданием в несколько дней/недель, если что-то пошло не так). Получить badge об успешном прохождении марафонской дистанции через пару дней - хоть и не приятно, но еще не критично. Но вот поставить электронную подпись на документы, которые нужно было подписать еще несколько недель назад - уже более критично. Не говоря про оплату элекроэнергии за прошлый месяц, когда уже пора платить за новый и с пеней.
Поэтому прямо перед самой отправкой уведомлений нужно всегда проверять еще раз актуальность этой отправки
- действительно ли дата предполагаемой отправки не outdated?
- действительно ли событие (истекла подписка) произошло совсем недавно, а не далеко в прошлом?
- действительно ли состояние пользователя в базе данных сооветствует отправляемому уведомлению?
- и т.д.
Только благодаря таким double checks получится избежать отправки stale notifications и не вводить пользователя в заблуждение!
#dmdev_best_practices
❤🔥34👍29🔥9❤1