DMdev talks
3.21K subscribers
156 photos
13 videos
89 links
Авторский канал Дениса Матвеенко, создателя DMdev - обучение Java программированию

То, что все ищут по Java:
https://taplink.cc/denis.dmdev

P.S. Когда не программирую - я бегаю:
https://t.iss.one/dmdev_pro_run
Download Telegram
Названия тестов - очень важны!

Название теста должно описывать поведение (test case). Когда программист просто читает название теста - этого уже должно быть достаточно, чтобы понять, что тест пытается проверить и какой ожидаемый результат.

Другими словами говоря, название теста служит для своего рода самодокументации.

На своем при
мере:
Когда я хочу посмотреть возможные варианты использования какого-то API, то при хорошо написанных тестах - мне достаточно просто взглянуть на них. Хорошие тестовые имена очень здорово описывают поведение API при самых разных условиях/входных параметрах, которые ты, порой, даже не ожидал. Ибо у того, кто писал этот API, как правило было больше контекста, чем у тебя, и он больше понимал его логику и его нюансы.

Для интереса можно сравнить два примера одного и того же test case:

@Test
void processMessage_failed()

@Test
void processMessage_missingMessageId_throwsValidationException()


Лишь взглянув на второй вариант, мне будет достаточно понять его суть.
Поэтому я нашел для себя этот паттерн именования тестов наилучшим:
apiUnderTest_behavior_expectedResult


#dmdev_best_practices
🔥108👍385❤‍🔥5
Избегай boolean параметров

Если это возможно, то лучше избегать boolean параметров в public методах (открытый API). Все потому, что такие структуры очень плохо и читаются в коде, и пишутся, да и тестировать тоже неприятно. А создавать целые переменные только для того, чтобы описывать true/false флаг, программисты попросту не будут (да и не поможет это).

Например, глядя на этот код, можно только гадать, что значит true, и что будет, если в метод передать false:

Result result = processMessage(message, true);

Как можно этого избежать?

1️⃣ Джошуа Блох в своей книге Effective Java (Item 51) советует создавать enum с двумя значениями. Что и легко читается, легко документируется, и в случае чего - можно добавлять третье/четвертое значение и т.д.

Result result = processMessage(message, ProcessingType.DRY_RUN);


2️⃣ Заменить один метод на два других с более подходящими названиями.

Result result = processMessage(message);

Result result = processMessageDryRun(message);


3️⃣ Воспользоваться техникой уменьшения количества параметров в методах, т.е. просто создать объект, который будет содержать все параметры:

ProcessOptions options = ProcessOptions.builder().message(message).dryRun(true).build();

Result result = process(options);


4️⃣ Если по каким-то стечениям обстоятельств не помогли три предыдущие пункта, то нужно хотя бы оставить комментарий, что означает boolean параметр:

Result result = processMessage(message, /* dryRun= */ true);


#dmdev_best_practices
👍108🔥50❤‍🔥6
Используй Builders

Builders - это короткоживущие mutable объекты, которые имеют одну единственную задачу: собрать все необходимые данные, чтобы создать другой объект, который в свою очередь обычно immutable и долгоживующий.


User user = User.builder()
.firstname("Ivan")
.lastname("Ivanov")
.age(35)
.build();

На примере представлено классическое использование этого паттерна:
- объект builder короткоживующий (можно сразу его забыть, потому что даже не пришлось создавать переменную для его хранения)
- используется лаконичная цепочка вызовов методов (fluent API)
- установка всех параметров самодокументируется и сложно спутать один с другим

Когда следует использовать паттерн Buil
der?
В большинстве случаев для замены статических фабричных методов или конструкторов.


User user = User.of("Ivan", "Ivanov", 35);

User user = new User("Ivan", "Ivanov", 35);

Ибо эти варианты подходят только когда класс очень простой и выражение получается компактным. Но в других ситациях это становится неприятным:
- очень часто параметры могут быть optional или иметь дефолтные значения
- параметры имеют свойство разрастаться
- легко попутать похожие параметры одного и того же типа, как на примере firstname, lastname

Более подробную информацию можно найти также в книге Effective Java (Item 2)

#dmdev_best_practices
👍82🔥35❤‍🔥6
Kонструктивная и деструктивная критика в Code Review

Если простыми словами, то критика - это указание на недостатки (реже достоинства) чего-то, что сделал другой человек. Она бывает конструктивной и деструктивной.

Конструктивная критика не причиняет вреда человеку. Она лишь аккуратно указывает на ошибки и через их исправление подталкивает к улучшению и развитию. Конструктивная критика объективна, аргументированна и уважительна.

Деструктивная критика, наоборот, бьет по самооценке. Она обвиняет человека и нападает на него. Деструктивная критика необъективна, не аргументирована и зачастую звучит унизительно.

Критику мы наблюдаем во всех сферах жизнедеятельности человека, особенно деструктивную в комментариях под моими видео 😁
Но где чаще всего мы ее встречаем в программировании? Правильно - это Code Review, где мы оцениваем код другого человека и оставляем свои комменатрии. И конечно же, нужно использовать только конструктивную критику во время Code Review.

Другими словами говоря, важно вести себя вежливо и уважительно, а также быть предельно ясным и полезным для разработчика, код которого ты просматриваешь. Один из способов сделать это - всегда комментируете ТОЛЬКО код и никогда не комментировать разработчика. Например:

Плохо:
Зачем ты использовал здесь многопоточность, когда очевидно, что от параллелизма нет никакой пользы?


Хорошо:
Параллелизм здесь усложняет систему без какого-либо реального выигрыша в производительности. Поскольку никакого выигрыша в производительности нет, я бы сделал этот код однопоточным. Так его и читать, и тестировать, и поддерживать гораздо проще.


Хорошие комментарии помогают разработчику понять, почему ты их оставил (содержат объяснение), а также довольно часто направляют в сторону принятия более правильного решения (как в данном случае - использовать однопоточный код). А это как раз-таки и есть конструктивная критика 🙂

#dmdev_best_practices
👍104🔥37❤‍🔥6👏3💯1
Параллелизм - это сложно

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

Например, если бы мне нужно было решить проблему многопоточности, то я бы следовал следующим шагам:
1. Сначала глянул на существуюющий фреймворк, может ли он мне помочь с проблемой. Это самый безопасный вариант, который не только помогает избегать багов, но еще и предлагает кучу метрик из коробки.
2. Если нужно решить low-level проблему, то конечно же я открыл бы пакет java.util.concurrent
3. Если все еще недостаточно, то я прибегнул бы к synchronized блокам или volatile (для неблокирующей синхронизации)
4. Только здесь я бы начал думать о своем собственном решении, если ничего другого не помогло, предварительно глянув решения в других проектах (возможно, кто-то уже реализовал)

В любом случае, я уже советовал книгу "Java Concurrency in Practice" в своих курсах (Java Level 2), если кто-то хочет действительно разобраться в многопоточности. Ну и конечно же нужно очень хорошо понимать Java Memory Model.

#dmdev_best_practices
👍75🔥255❤‍🔥3
Избегай какой-либо логики в конструкторах

Самый простой и общепринятый вид конструкторов - это присваивание параметров полям объекта, которые имеют те же названия и типы.
Если ваш конструктор выглядит не так, то подумайте, возможно, он все-таки должен быть таким:

User(Long id, String username, LocalDate birthDate) {
this.id = id;
this.username = username;
this.birthDate = birthDate;
}

Чем больше логики конструктор включает, тем сложнее тестировать такие классы. И особенно писать тесты на те классы, которые транзитивно зависят от них. Частично эту проблему решает Mockito, который позволяет создавать mock объект не вызывая конструктор. Но здесь есть несколько неприятных моментов:
- такой класс не может быть final
- лучше использовать настоящие объекты в тестах, вместо mock

Если конструктор вызывает внутри себя другой не статический метод, то в нем можно наблюдать частично проинициализированный объект. Особенно эта проблема чаще наблюдается при использовании таких классов в наследовании.

Еще хуже выполнять в конструкторах IO операции, особенно сторонние вызовы в другие сервисы! Если это будет какой-нибудь Spring bean и произойдет исключение (например, сервис не отвечает), то контекст попросту не сможет подняться, что чревато в принципе неработающим приложением в продакшене. Такие ситуации я уже не раз встречал на своем опыте, и это действительно печально, когда даже откатиться на предыдущие версию не поможет восстановить работосопособность приложения.

Поэтому, вместо того, чтобы добавлять логику в конструктор, лучше перенести ее в какие-нибудь фабричные методы/классы, или вообще попросить у Dependency Injection framework нужные тебе параметры.

#dmdev_best_practices
👍88🔥2613
Когда литералы выносить в константы

Литералы, такие как строки или числа (более подробно рассказывал про них в курсе Computer Science) можно либо просто оставлять в коде как есть, либо выносить их в константы и потом уже использовать именно константы.

В принципе, предпочтение нужно отдавать созданию констант, особенно если:
1. Одно и то же значение используется в нескольких местах, ибо это важно иметь one source of truth (иначе легко изменить значение в одном месте и забыть поменять в другом, из-за чего один и тот же функционал будет работать по-разному)
2. Значение требует пояснения, и гораздо лучше создать константы с хорошим названием и объяснением над ней, нежели оставлять неуклюжие комментарии прямо в месте использования литерала.

Например:

static final int GRACE_PERIOD_HOURS = 12;
scheduleSubscriptionRenewal(validUntil + GRACE_PERIOD_HOURS);

// или для сравнения:

scheduleSubscriptionRenewal(validUntil + 12);


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

Самые часто распространенные примеры, когда лучше оставлять литералы:
- true, false
- 0, 1
- SQL queries

#dmdev_best_practices
👍53🔥25❤‍🔥43👏1
Операции должны быть идемпотентны

Идемпотентность означает, что выполнение одного и того же запроса несколько раз подряд приведет к тому же результату, что и выполнение этого запроса в первый раз. Сюда относятся любые операции и по любым протоколам, которые использует сервис для обработки внешних запросов. Например, REST API, gRPC, Thrift, Message Brokers (Kafka, Cloud Pub/Sub) и т.д.

Почему это так важно?

1️⃣ Идемпотентные запросы могут предотвратить случайное выполнение повторных операций, что может привести к ошибкам или нежелательным изменениям в системе. Обычно из приведенных протоколов общения - никто не гарантирует, даже Message Brokers, что одно и то же сообщение будет доставлено ровно один раз!

2️⃣ Идемпотентные запросы обеспечивают надежность операций, позволяя клиентам повторять запросы в случае неудачи или потери связи.

3️⃣ Идемпотентные запросы облегчают работу с API, так как клиенты могут повторять запросы без необходимости дополнительной логики для управления состоянием. Другими словами говоря, клиенту не стоит переживать и усложнять свою систему для отправки запросов сервису.

Если брать идемпотентность в контексте CRUD операций, про которые я довольно много рассказывал в курсе Spring:

- Read операции (HTTP GET) по умолчанию должны быть идемпотентными. Ибо никаких изменений на сервере не должно происходить при обработки запросов на чтение данных.

- Update операции (HTTP PUT) точно также легко сделать идемпотентными. Можно сколько угодно обновлять один и тот же объект с теми же самыми полями в запросе.

- Delete операции (HTTP DELETE) чуть сложнее, но удаление одного и того же ресурса не должно вызывать ошибки на сервере, если такого ресурса не оказалось во втором и последующих запросах

- Create операции (HTTP POST) сложнее всего добиться идемпотентности, поскольку их выполнение приводит к созданию нового ресурса с уникальным идентификатором. Повторный запрос на создание ресурса с теми же параметрами может привести к созданию дубликатов или ошибкам. Но все-таки способы есть, про которые я расскажу в отдельном посте!

#dmdev_best_practices
👍76🔥27❤‍🔥72
Обеспечение идемпотентности в 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
👍73🔥33❤‍🔥104
Нужно закрывать ресурсы

Самый лучший вариант не забыть закрыть ресурсы после их использования - это использовать библиотеки или встроенный в язык функционал, который работает с ресурсами на уровень выше.

Другими словами говоря, если ты пишешь код так, чтобы не давать доступ пользователю к открытому ресурсу, то он попросто не сможет забыть его закрыть. Поэтому большинстве 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