Эта статья демонстрирует реальный пример рефакторинга 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 позволяет легко повторно использовать функцию повторной попытки в других операциях с базой данных.

Комментарии (1)


  1. aamonster
    00.00.0000 00:00
    +1

    Уфф... Мне кажется, или проще было сразу переписать с лямбдой (паттерн уж больно знакомый – получить и использовать функцию), а не проходить через все эти шаги рефакторинга?

    Или это просто иллюстрация хода мысли?