Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Эта статья демонстрирует реальный пример рефакторинга Java, направленного на достижение более чистого кода и лучшего разделения задач. Идея возникла из моего опыта программирования в профессиональной среде.
Однажды в производственном коде
Когда я работал над кодом, сохраняющим некоторые данные домена, у меня получилось следующее:
public void processMessage(InsuranceProduct product) throws Exception {
for (int retry = 0; retry <= MAX_RETRIES; retry++) {
try {
upsert(product);
return;
} catch (SQLException ex) {
if (retry >= MAX_RETRIES) {
throw ex;
}
LOG.warn("Fail to execute database update. Retrying...", ex);
reestablishConnection();
}
}
}
private void upsert(InsuranceProduct product) throws SQLException {
//содержание не актуально
}
Метод processMessage
является частью контракта фреймворка и вызывается для сохранения каждого обработанного сообщения. Код выполняет идемпотентное обновление базы данных (метод upsert) и обрабатывает логику повторных попыток в случае ошибок.
Основная ошибка, беспокоившая меня заключалась в том, что истек таймаут JDBC соединения, которое необходимо восстановить.
Меня не удовлетворила первоначальная версия processMessage
с точки зрения чистоты кода. Я рассчитывал на что-то, моментально показывающее его замысел без необходимости погружаться в код.
Метод полон низкоуровневых деталей, которые необходимо понять, чтобы узнать, что он делает. Кроме того, я хотел отделить логику повторных попыток от повторяемой операции базой данных, чтобы ее можно было легко использовать повторно.
Я решил переписать его, чтобы решить указанные проблемы.
Менее процедурный, более декларативный
Первый шаг — перенести вызов updateDatabase()
в переменную с лямбда выражением. Пусть IDE поможет нам в этом, используя рефакторинг Introduce Functional Variable. К сожалению, мы получаем сообщение об ошибке:
No applicable functional interfaces found
Причиной этого является отсутствие функционального интерфейса, обеспечивающего SAM-интерфейс, совместимый с методом upsert.
Чтобы решить эту проблему, нам нужно определить функциональный интерфейс, который объявляет единственный абстрактный метод, не принимающий никаких параметров, ничего не возвращающий и выбрасывающий исключение SQLException
.
Вот интерфейс, который нам нужно предоставить:
@FunctionalInterface
interface SqlRunnable {
void run() throws SQLException;
}
Создав функциональный интерфейс, давайте повторим рефакторинг. На этот раз все прошло успешно. Кроме того, давайте перенесем присвоение переменной перед циклом for:
public void processMessage(InsuranceProduct product) throws Exception {
final SqlRunnable handle = () -> upsert(product);
for (int retry = 0; retry <= MAX_RETRIES; retry++) {
try {
handle.run();
return;
} catch (SQLException ex) {
if (retry >= MAX_RETRIES) {
throw ex;
}
LOG.warn("Fail to execute database update. Retrying...", ex);
reestablishConnection();
}
}
}
Используйте рефакторинг Extract Method для перемещения цикла for и его содержимое в новый метод с именем retryOnSqlException
:
public void processMessage(InsuranceProduct product) throws Exception {
final SqlRunnable handle = () -> upsert(product);
retryOnSqlException(handle);
}
private void retryOnSqlException(SqlRunnable handle) throws SQLException {
//skipped for clarity
}
Последний шаг заключается в использовании рефакторинга Inline Variable для встраивания переменной handle
.
Окончательный результат приведен ниже.
public void processMessage(InsuranceProduct product) throws Exception {
retryOnSqlException(() -> upsert(product));
}
Теперь метод ввода фреймворка четко указывает, что он делает. Он занимает всего одну строку, что исключает когнитивную нагрузку.
Представленный код содержит подробную информацию о том, как он выполняет свои функции и обеспечивает возможность повторного использования:
private void retryOnSqlException(SqlRunnable handle) throws SQLException {
for (int retry = 0; retry <= MAX_RETRIES; retry++) {
try {
handle.run();
return;
} catch (SQLException ex) {
if (retry >= MAX_RETRIES) {
throw ex;
}
LOG.warn("Fail to execute database update. Retrying...", ex);
reestablishConnection();
}
}
}
@FunctionalInterface
interface SqlRunnable {
void run() throws SQLException;
}
Заключение
Стоило ли это усилий? Безусловно. Давайте подытожим преимущества.
Метод processMessage
теперь четко выражает свое намерение, используя декларативный подход с высокоуровневым кодом. Логика повторных попыток отделена от работы с базой данных и помещена в собственный метод, который благодаря хорошему именованию точно раскрывает свое назначение. Кроме того, синтаксис Lambda позволяет легко повторно использовать функцию повторной попытки в других операциях с базой данных.