Обеспечение идемпотентности в Create операциях
Я думаю, что важность идемпотентности в Create операциях особенно высока в различных платежных системах, вроде Stripe, где повторные запросы на изменение баланса могут привести к таким плачевным ошибкам как дублирование платежей. Кстати, даже я на своем опыте встречался с этой проблемой - мне два раза перевели 10.000$ вместо одного (к сожалению, пришлось вернуть).
Почему так часто повторяются одни и те же операции?
- проблемы на стороне клиента, например, пользователю не понятно - была нажата кнопка "отправить" или нет, поэтому нажимает кнопку вновь
- запросы идут по сети, которая ненадежна
- соединение может оборваться в момент отправки сообщения, тогда клиент получит ошибку и будет пробовать переотправить сообщение вновь (retry mechanism)
- запрос может успеть дойти и обработаться сервером, но клиент об этом не узнает, потому что соединение оборвалось в момент возвращение ответа
Так какие же существуют способы для того, чтобы добиться идемпотентности в Create операциях?
1️⃣ Клиент может генерировать уникальный идентификатор Request Id (обычный UUID) для каждого запроса Create и включать его в теле запроса или заголовке. Далее сервер проверять этот идентификатор у себя в базе (можно даже кэш использовать с небольшим временем жизни) и игнорировать повторные запросы с тем же идентификатором.
2️⃣ Чем-то похож на первый вариант, но вместо создания Request id - клиент (или даже сам сервер) может генерировать уникальный идентификатор на основании выбранных полей в теле запроса. И точно также дополнительно хранить этот идентификатор у себя в базе (или кэше).
Вариантов не много (кстати, первый является более предпочтительным), потому что само по себе создание нового пораждает уникальный идентификатор, и сложно определить (а потом еще и реализовать!) было идентичное уже создано на сервере или нет.
Примеры API, как это мы обычно решаем в Google, можно глянуть в спецификации AIP-155
Еще больше про идемпотентный API можно почитать тут:
Stripe: Designing robust and predictable APIs with idempotency
Optimistic Locking in a REST API
The Amazon Builders' Library: Making retries safe with idempotent APIs
#dmdev_best_practices
Я думаю, что важность идемпотентности в Create операциях особенно высока в различных платежных системах, вроде Stripe, где повторные запросы на изменение баланса могут привести к таким плачевным ошибкам как дублирование платежей. Кстати, даже я на своем опыте встречался с этой проблемой - мне два раза перевели 10.000$ вместо одного (к сожалению, пришлось вернуть).
Почему так часто повторяются одни и те же операции?
- проблемы на стороне клиента, например, пользователю не понятно - была нажата кнопка "отправить" или нет, поэтому нажимает кнопку вновь
- запросы идут по сети, которая ненадежна
- соединение может оборваться в момент отправки сообщения, тогда клиент получит ошибку и будет пробовать переотправить сообщение вновь (retry mechanism)
- запрос может успеть дойти и обработаться сервером, но клиент об этом не узнает, потому что соединение оборвалось в момент возвращение ответа
Так какие же существуют способы для того, чтобы добиться идемпотентности в Create операциях?
1️⃣ Клиент может генерировать уникальный идентификатор Request Id (обычный UUID) для каждого запроса Create и включать его в теле запроса или заголовке. Далее сервер проверять этот идентификатор у себя в базе (можно даже кэш использовать с небольшим временем жизни) и игнорировать повторные запросы с тем же идентификатором.
2️⃣ Чем-то похож на первый вариант, но вместо создания Request id - клиент (или даже сам сервер) может генерировать уникальный идентификатор на основании выбранных полей в теле запроса. И точно также дополнительно хранить этот идентификатор у себя в базе (или кэше).
Вариантов не много (кстати, первый является более предпочтительным), потому что само по себе создание нового пораждает уникальный идентификатор, и сложно определить (а потом еще и реализовать!) было идентичное уже создано на сервере или нет.
Примеры API, как это мы обычно решаем в Google, можно глянуть в спецификации AIP-155
Еще больше про идемпотентный API можно почитать тут:
Stripe: Designing robust and predictable APIs with idempotency
Optimistic Locking in a REST API
The Amazon Builders' Library: Making retries safe with idempotent APIs
#dmdev_best_practices
👍73🔥33❤🔥10❤4
Нужно закрывать ресурсы
Самый лучший вариант не забыть закрыть ресурсы после их использования - это использовать библиотеки или встроенный в язык функционал, который работает с ресурсами на уровень выше.
Другими словами говоря, если ты пишешь код так, чтобы не давать доступ пользователю к открытому ресурсу, то он попросто не сможет забыть его закрыть. Поэтому большинстве IO операций могут быть решены при помощи таких классов как ByteSource, CharSink (guava), и другие, но общий принцип таков:
Где FileSource берет на себя работу по инициализации resource, вызывает пользовательскую функцию doWork, и после всего закрывает сам resource.
Второй отличный вариант - это конечно же блок try-with-resources (начиная с Java 7), в который можно поместить любой объект (или даже несколько!), реализующий интерфейс AutoCloseable:
И опять же в методе doWork программисту не нужно думать о закрытии ресурсов, потому что они будут закрыты автоматически на уровень выше в обратном от их инициализации порядке, т.е. сначала закроется объект out, потом in. Более того, даже если произайдет ошибка при открытии потока OutputStream, первый InputStream все равно будет закрыт.
Кстати, в Java 9 добавили функционал по закрытию ресурсов, которые НЕ БЫЛИ проинициализированы в блоке try-with-resources. Тем самым дав возможность программистам закрывать ресурсы, которые были созданы на уровень выше. Поэтому советую прибегать к этому функционалу только в случае необходимости 🙂
#dmdev_best_practices
Самый лучший вариант не забыть закрыть ресурсы после их использования - это использовать библиотеки или встроенный в язык функционал, который работает с ресурсами на уровень выше.
Другими словами говоря, если ты пишешь код так, чтобы не давать доступ пользователю к открытому ресурсу, то он попросто не сможет забыть его закрыть. Поэтому большинстве IO операций могут быть решены при помощи таких классов как ByteSource, CharSink (guava), и другие, но общий принцип таков:
File file = new File("/path/to/file");
FileSource.of(file).use(resource -> doWork(resource));
Где FileSource берет на себя работу по инициализации resource, вызывает пользовательскую функцию doWork, и после всего закрывает сам resource.
Второй отличный вариант - это конечно же блок try-with-resources (начиная с Java 7), в который можно поместить любой объект (или даже несколько!), реализующий интерфейс AutoCloseable:
try (InputStream in = Files.newInputStream(sourcePath);
OutputStream out = Files.newOutputStream(destinationPath)) {
doWork(in, out);
}
И опять же в методе doWork программисту не нужно думать о закрытии ресурсов, потому что они будут закрыты автоматически на уровень выше в обратном от их инициализации порядке, т.е. сначала закроется объект out, потом in. Более того, даже если произайдет ошибка при открытии потока OutputStream, первый InputStream все равно будет закрыт.
Кстати, в Java 9 добавили функционал по закрытию ресурсов, которые НЕ БЫЛИ проинициализированы в блоке try-with-resources. Тем самым дав возможность программистам закрывать ресурсы, которые были созданы на уровень выше. Поэтому советую прибегать к этому функционалу только в случае необходимости 🙂
#dmdev_best_practices
👍67🔥27❤🔥8
Не создавай singleton - используй Dependency Injection Framework
Для того, чтобы создать singleton в Java - есть два основных варианта:
1. Самый общеизвестный и простой (и который я показываю в своих курсах), требующий создать static final поле, хранящее единственный экземпляр класса, и private конструктор, чтобы никто не создал объект этого класса
Но здесь появляются проблемы с десериализацией (и другие обходные пути создания более одного объекта), сложность управления зависимостями, а значит и затруднения при тестировании из-за тесной связи singleton с другими компонентами приложения.
2. Второй вариант решает часть из этих проблем - это enum. Его также советовал Джошуа Блох (Item 3), но в то же самое время добавляет другие ограничения и чувствуется "не есественность" в таком подходе. Поэтому программисты обходят его стороной.
Тем не менее, лучше избегать создания обоих вариантов синглтонов, если есть возможность использовать Dependency Injection Framework, такого как Spring или Guice. Фреймворк управляет созданием и предоставлением зависимостей для объектов в приложении, что делает код более гибким, легко поддающимся изменениям и сильно упрощается тестирование.
#dmdev_best_practices
Для того, чтобы создать singleton в Java - есть два основных варианта:
1. Самый общеизвестный и простой (и который я показываю в своих курсах), требующий создать static final поле, хранящее единственный экземпляр класса, и private конструктор, чтобы никто не создал объект этого класса
class UserRepository {
private static final UserRepository INSTANCE = new UserRepository();
private UserRepository() {}
public static UserRepository getInstance() {return INSTANCE;}
}
Но здесь появляются проблемы с десериализацией (и другие обходные пути создания более одного объекта), сложность управления зависимостями, а значит и затруднения при тестировании из-за тесной связи singleton с другими компонентами приложения.
2. Второй вариант решает часть из этих проблем - это enum. Его также советовал Джошуа Блох (Item 3), но в то же самое время добавляет другие ограничения и чувствуется "не есественность" в таком подходе. Поэтому программисты обходят его стороной.
enum UserRepository {
INSTANCE;
}
Тем не менее, лучше избегать создания обоих вариантов синглтонов, если есть возможность использовать Dependency Injection Framework, такого как Spring или Guice. Фреймворк управляет созданием и предоставлением зависимостей для объектов в приложении, что делает код более гибким, легко поддающимся изменениям и сильно упрощается тестирование.
#dmdev_best_practices
🔥72👍24❤🔥5❤3🥰1
Невозможное условие
Невозможным является такое условие, которое никогда не может возникнуть. За исключением случаев, когда наши самые глубокие предположения были не верны, или код не был значительно изменен в будущем. Поскольку условие невозможно, то и нет смысла его проверять и писать тест, если только это не требуется компилятором.
Первое, что стоит попробовать - это постараться исправить код так, чтобы невозможное условие никогда не возникало!
Если код исправить не получилось, то лучше обрабатывать такие невозможные условия, вызывая AssertionError. Обычно сообщение об исключении не требуется. Но если же ты все-таки начинаешь долго думать над ним, то это признак того, что условие скорее всего не так уж и невозможно!
Кстати, если невозможное условие является исключением, полезно назвать его impossible (как в примере ниже).
#dmdev_best_practices
Невозможным является такое условие, которое никогда не может возникнуть. За исключением случаев, когда наши самые глубокие предположения были не верны, или код не был значительно изменен в будущем. Поскольку условие невозможно, то и нет смысла его проверять и писать тест, если только это не требуется компилятором.
Первое, что стоит попробовать - это постараться исправить код так, чтобы невозможное условие никогда не возникало!
Если код исправить не получилось, то лучше обрабатывать такие невозможные условия, вызывая AssertionError. Обычно сообщение об исключении не требуется. Но если же ты все-таки начинаешь долго думать над ним, то это признак того, что условие скорее всего не так уж и невозможно!
Кстати, если невозможное условие является исключением, полезно назвать его impossible (как в примере ниже).
// Компилятор требует обработки IOException, но мы знаем, что IO исключение невозможно при конкатенации строк.
// Кстати, это отличный вариант для использования Lombok аннотации @SneakyThrows.
Appendable a = new StringBuilder();
try {
a.append("hello");
} catch (IOException impossible) {
throw new AssertionError(impossible);
}
// Строка, которую невозможно достичь, только если не изменить логику кода в будущем
switch (array.length % 2) {
case 0:
return handleEven(array);
case 1:
return handleOdd(array);
default:
throw new AssertionError("array length is not even or odd: " + array.length);
}
#dmdev_best_practices
🔥60👍25❤🔥7❤1
Избегай громоздких списков параметров
Думаю, программистам интуитивно понятно, что сигнатуры методов (в первую очередь public) - должны быть хорошо продуманы, чтобы в последующем легко читались и использовались в коде. И особенно это касается количества параметров методов. Например, Джошуа Блох (Item 51) рекомендует не более 4, и я с ним практически согласен.
Хотя в идеале метод вообще должен состоять ровно из одного значения, которое передается на вход, и одного значения на выходе (return). А все, что происходит внутри - не должно менять каким-то образом состояние этого параметра.
Кроме количества также важен и тип параметров, ибо очень неприятно использовать методы, например, с 4 типами int. В одном из практическом видео на курсе Java Level 2 (доступно спонсорам Junior level) я демонстрировал домашнее задание с прямоугольником, в котором нужно было указать две точки для его определения:
Второй отличный вариант избегать большого количества параметров (валидно только для конструкторов) - это конечно же паттер Builder, про который мы уже обсуждали в одном из предыдущем best practice.
Третий - объединять несколько параметров в один объект Dto/Value.
#dmdev_best_practices
Думаю, программистам интуитивно понятно, что сигнатуры методов (в первую очередь public) - должны быть хорошо продуманы, чтобы в последующем легко читались и использовались в коде. И особенно это касается количества параметров методов. Например, Джошуа Блох (Item 51) рекомендует не более 4, и я с ним практически согласен.
Хотя в идеале метод вообще должен состоять ровно из одного значения, которое передается на вход, и одного значения на выходе (return). А все, что происходит внутри - не должно менять каким-то образом состояние этого параметра.
Кроме количества также важен и тип параметров, ибо очень неприятно использовать методы, например, с 4 типами int. В одном из практическом видео на курсе Java Level 2 (доступно спонсорам Junior level) я демонстрировал домашнее задание с прямоугольником, в котором нужно было указать две точки для его определения:
// Вот так неудобно использовать
public Rectangle(int top, int bottom, int left, int right) {}
// А вот так и удобнее, и гораздо понятнее. Еще и запутаться сложно
public Rectangle(Point upperLeft, Point lowerRight) {}
Второй отличный вариант избегать большого количества параметров (валидно только для конструкторов) - это конечно же паттер Builder, про который мы уже обсуждали в одном из предыдущем best practice.
Третий - объединять несколько параметров в один объект Dto/Value.
#dmdev_best_practices
🔥89👍15❤7💯3❤🔥2
Предпочитай методы, реализующие функциональные интерфейсы, а не возвращающие функциональные интерфейсы
Довольно часто замечал, особенно среди тех, кто сильно вдохновился функциональным программированием и старается его применить где только можно, вот такой код:
Но гораздо приятнее и более читабельнее будет преобразовать метод так, чтобы он не возвращал функциональный интерфейс
Также хотелось бы добавить, что две парадигмы программирования, функциональное и объектно-ориентированное, не исключают друг друга. Каждая имеет свои плюсы и минусы: какие-то задачи лучше решать с помощью функционального стиля, какие-то с помощью объектно-ориентированного.
#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
This media is not supported in your browser
VIEW IN TELEGRAM
Менторство DMdev как дополнительный уровень в игре, только вместо нового босса тебя ждет опытный ментор, я и крутые знания.
➡️ Стартуем уже через месяц!
Осталось всего 2 места на первую ступень и 5 мест на вторую.
Выбирай свой уровень, записывайся и побеждай в мире IT:
🧩 Первая ступень менторства
🧩 Вторая ступень менторства
Если тебя все еще гложат сомнения или
важные вопросы остались не отвеченными —> просто напиши
@karina_matveyenka
Осталось всего 2 места на первую ступень и 5 мест на вторую.
Выбирай свой уровень, записывайся и побеждай в мире IT:
🧩 Первая ступень менторства
🧩 Вторая ступень менторства
Если тебя все еще гложат сомнения или
важные вопросы остались не отвеченными —> просто напиши
@karina_matveyenka
Please open Telegram to view this post
VIEW IN TELEGRAM
👍16🔥6❤2👏1
Генерация тестовых данных
Чтобы написать хороший тест, нужны хорошие тестовые данные, приближенные к 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("test1@gmail.com"));
User user2 = userDao.save(getUser("test2@gmail.com"));
User user3 = userDao.save(getUser("test3@gmail.com"));
// 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