Рефакторинг кода PersonService

1. Провести рефакторинг PersonService

Условие задачи:
Дан метод getAdultMaleUsersAverageBMI(), который:

  • самостоятельно открывает соединение с БД;

  • получает совершеннолетних мужчин;

  • создаёт для каждой записи объект Person;

  • вычисляет средний BMI;

  • выводит результат в консоль;

  • самостоятельно обрабатывает ошибки.

Необходимо провести рефакторинг кода: убрать лишние операции, корректно управлять JDBC-ресурсами, обработать граничные случаи и разделить ответственность метода.

Код:

public class PersonService {

    public void getAdultMaleUsersAverageBMI() {
        double totalIMT = 0.0;
        long countOfPerson = 0;
        double heightInMeters = 0d;
        double imt = 0d;

        List<Person> adultPersons = new ArrayList<>();

        try {
            Connection c = DriverManager.getConnection(
                    "jdbc:hsqldb:mem:test",
                    "admin",
                    "qwerty54"
            );

            Statement s = c.createStatement();

            ResultSet rs = s.executeQuery(
                    "SELECT * FROM person " +
                    "WHERE sex = 'male' AND age >= 18"
            );

            while (rs.next()) {
                Person p = new Person();

                p.setId(rs.getLong("id"));
                p.setSex(rs.getString("sex"));
                p.setName(rs.getString("name"));
                p.setAge(rs.getLong("age"));
                p.setWeight(rs.getLong("weight"));
                p.setHeight(rs.getLong("height"));

                adultPersons.add(p);
            }

            for (Person p : adultPersons) {
                heightInMeters = p.getHeight() / 100d;
                imt = p.getWeight()
                        / (heightInMeters * heightInMeters);

                totalIMT += imt;
            }

            countOfPerson = adultPersons.size();
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println(
                "Average imt = "
                        + totalIMT / countOfPerson
        );
    }
}

Спойлеры к решению

Подсказки
💡 Для вычисления BMI нужны только weight и height, поэтому создавать объекты Person необязательно.
💡 JDBC-ресурсы следует закрывать через try-with-resources.
💡 Строку подключения и credentials лучше передавать через DataSource, а не хранить в сервисе.
💡 Метод лучше возвращает результат, а не занимается выводом в консоль.
💡 Нужно обработать ситуацию, когда подходящих людей нет, а также некорректный height = 0.

Решение

Один из вариантов рефакторинга:

public class PersonService {

    private static final String QUERY = """
            SELECT weight, height
            FROM person
            WHERE sex = ?
              AND age >= ?
            """;

    private final DataSource dataSource;

    public PersonService(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public OptionalDouble getAdultMaleUsersAverageBMI() {
        double totalBmi = 0.0;
        long count = 0;

        try (Connection connection = dataSource.getConnection();
             PreparedStatement statement =
                     connection.prepareStatement(QUERY)) {

            statement.setString(1, "male");
            statement.setInt(2, 18);

            try (ResultSet rs = statement.executeQuery()) {
                while (rs.next()) {
                    double weight = rs.getDouble("weight");
                    double heightCm = rs.getDouble("height");

                    if (heightCm <= 0) {
                        continue;
                    }

                    double heightMeters = heightCm / 100.0;

                    totalBmi += weight
                            / (heightMeters * heightMeters);

                    count++;
                }
            }

        } catch (SQLException e) {
            throw new IllegalStateException(
                    "Не удалось вычислить средний BMI",
                    e
            );
        }

        if (count == 0) {
            return OptionalDouble.empty();
        }

        return OptionalDouble.of(totalBmi / count);
    }
}

Использование:

personService.getAdultMaleUsersAverageBMI()
        .ifPresentOrElse(
                bmi -> System.out.printf(
                        "Average BMI = %.2f%n",
                        bmi
                ),
                () -> System.out.println(
                        "Подходящие пользователи не найдены"
                )
        );

В исходном методе выполнялось:

БД
ResultSet
Person
List<Person>
повторный обход списка
BMI

Для вычисления среднего значения объекты Person и промежуточный список не нужны. Достаточно считать два необходимых поля:

SELECT weight, height
FROM person
WHERE sex = ?
  AND age >= ?

и сразу обновлять сумму:

totalBmi += weight
        / (heightMeters * heightMeters);

count++;

try-with-resources гарантирует закрытие:

Connection
PreparedStatement
ResultSet

в том числе при возникновении исключения.

Получение соединения вынесено в:

private final DataSource dataSource;

поэтому сервис больше не содержит внутри себя:

URL БД
логин
пароль

и его проще конфигурировать и тестировать.

Вместо:

catch (Exception e) {
    e.printStackTrace();
}

перехватывается конкретный SQLException. Ошибка не скрывается, а передаётся вызывающему коду как исключение более высокого уровня.

Возвращается:

OptionalDouble

поскольку при отсутствии подходящих записей среднего значения не существует. Это также предотвращает деление:

0 / 0

и получение NaN.

Метод теперь отвечает за вычисление среднего BMI, а не за форматирование и вывод результата.

Основные улучшения:

  • убран ненужный List<Person>;

  • не создаются лишние объекты Person;

  • выбираются только необходимые столбцы;

  • JDBC-ресурсы гарантированно закрываются;

  • конфигурация БД отделена от бизнес-логики;

  • обработан случай отсутствия данных;

  • исключено деление на нулевой рост;

  • ошибка работы с БД не скрывается;

  • вычисление отделено от вывода результата.

Временная сложность — O(n), где n — количество найденных записей. Дополнительная память — O(1).