DMdev talks
3.24K 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
Параллелизм - это сложно

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

Например, если бы мне нужно было решить проблему многопоточности, то я бы следовал следующим шагам:
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
👍4
Избегай какой-либо логики в конструкторах

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

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
👍5
Когда литералы выносить в константы

Литералы, такие как строки или числа (более подробно рассказывал про них в курсе 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
Хороший вопрос! Сейчас подробно отвечу...
Forwarded from Dmitry Alekhine
Денис, в энтерпрайз проге существует куда более масштабная проблема, я бы хотел услышать твое мнение по поводу нее:

Существует тонна библиотек и фреймворков, каждый(ая) решает проблему на все случаи жизни

Увы, наблюдаю, что в самом большом софте, будь то даже blink engine (движок хрома) - челы не используют stdlib, другие либы - они пишут свои собственные реализации корутин, виртуальных тредов, пулов и так далее.

С чем это скорее всего связано:
> Многоуровневый оберточный пи*дец "для удобства"
> Нежелание разобраться с библиотекой: "я свою реализацию быстрее напишу" - именно это больше всего и влияет на observability проекта и на его overbloat'ность

С другой стороны есть такие гении, которые ради одной функции импортируют целый фреймворк. Открываешь POMник - 500+ строк

Как находить баланс при разработке проекта, чтобы проект медленнее скатывался в оверблоат, но и не был тяжеловесным говном благодаря ленивым и глупым васянам, которые не в силах помимо чейнинга фремворочков и жсончиков придумать свою реализацию
🔥22👍5❤‍🔥31
Ответ на вопрос 👆

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

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

Далее идет использование инструмента другими разработчиками и их онбординг в проект/компанию. Если это общеизвестная библиотека или фреймворк вроде Spring, Hibernate, которые еще и обновляются “сами” - то с очень большой вероятностью человек будет знать его, что сэкономит время, а значит и деньги для компании.

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

Пример с хромом и другими продуктами от гигантов вроде Google, Amazon и т.д. не стоит брать во внимание, потому что у них штат сотрудников в десятки и сотни тысяч человек. У них практически все используется свое самописное, потому что для них так проще. У них есть достаточно денежных и человеческий ресурсов, чтобы не только создавать такие инструменты, но и поддерживать их и даже выкладывать в open source. И онбординг в 3-6 месяцев для них ничего не стоит.
👍64🔥20❤‍🔥21👏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
Не создавай singleton - используй Dependency Injection Framework

Для того, чтобы создать 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❤‍🔥53🥰1
Невозможное условие

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

Первое, что стоит попробовать - это постараться исправить код так, чтобы невозможное условие никогда не возникало!

Если код исправить не получилось, то лучше обрабатывать такие невозможные условия, вызывая 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❤‍🔥71
🔥52🎉2
Избегай громоздких списков параметров

Думаю, программистам интуитивно понятно, что сигнатуры методов (в первую очередь 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👍157💯3❤‍🔥2