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

Что такое обертка (wrapper)?

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

  1. Инкапсуляция логики. Упрощение взаимодействия с более сложными системами.

  2. Добавление новой функциональности. Расширение поведения объекта без изменения исходного кода.

  3. Повышение удобства использования. Создание более понятного или лаконичного интерфейса.

  4. Интеграция. Адаптация сторонних библиотек или API под нужды проекта.

Примеры оберток в стандартной библиотеке Java

Java предоставляет множество встроенных оберток. Например:

  • Классы Integer, Double, Boolean — это обертки для примитивных типов данных.

  • BufferedReader и BufferedWriter — обертки для работы с потоками, добавляющие буферизацию.

Давайте создадим свои обертки и разберем, как их использовать.

Типы оберток и их применение

1. Обертки для примитивных типов

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

public class PositiveInteger {
    private final int value;

    public PositiveInteger(int value) {
        if (value <= 0) {
            throw new IllegalArgumentException("Value must be positive!");
        }
        this.value = value;
    }

    public int getValue() {
        return value;
    }

    public PositiveInteger add(int number) {
        return new PositiveInteger(this.value + number);
    }

    @Override
    public String toString() {
        return "PositiveInteger{" + "value=" + value + '}';
    }
}

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

public class Main {
    public static void main(String[] args) {
        PositiveInteger number = new PositiveInteger(10);
        System.out.println(number); // PositiveInteger{value=10}

        PositiveInteger newNumber = number.add(5);
        System.out.println(newNumber); // PositiveInteger{value=15}

        // PositiveInteger invalid = new PositiveInteger(-5); // Ошибка IllegalArgumentException
    }
}

Преимущества:

  • Обеспечение неизменности значения (final).

  • Валидация на этапе создания объекта.

2. Обертки для сторонних библиотек

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

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class ConnectionWrapper {
    private Connection connection;

    public ConnectionWrapper(String url, String user, String password) throws SQLException {
        this.connection = DriverManager.getConnection(url, user, password);
    }

    public void executeQuery(String query) throws SQLException {
        try (var statement = connection.createStatement()) {
            var resultSet = statement.executeQuery(query);
            while (resultSet.next()) {
                System.out.println(resultSet.getString(1));
            }
        }
    }

    public void close() throws SQLException {
        if (connection != null) {
            connection.close();
        }
    }
}

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

public class Main {
    public static void main(String[] args) {
        try (ConnectionWrapper db = new ConnectionWrapper("jdbc:h2:mem:test", "user", "password")) {
            db.executeQuery("SELECT 1");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

Преимущества:

  • Автоматизация рутинных задач (закрытие ресурсов).

  • Улучшение читаемости кода.

3. Обертки для добавления функциональности

Часто требуется добавить к объекту логирование или мониторинг. Например, обернем List, чтобы фиксировать добавленные элементы.

import java.util.ArrayList;
import java.util.List;

public class LoggingList<T> {
    private final List<T> list = new ArrayList<>();

    public void add(T element) {
        System.out.println("Adding element: " + element);
        list.add(element);
    }

    public T get(int index) {
        System.out.println("Getting element at index: " + index);
        return list.get(index);
    }

    public int size() {
        return list.size();
    }
}

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

public class Main {
    public static void main(String[] args) {
        LoggingList<String> loggingList = new LoggingList<>();
        loggingList.add("Hello");
        loggingList.add("World");
        System.out.println(loggingList.get(0)); // Hello
    }
}

Преимущества:

  • Логирование операций.

  • Минимальное изменение исходного кода.

4. Обертки для асинхронных задач

Обертки полезны для управления асинхронным кодом, особенно с CompletableFuture. Создадим обертку, упрощающую обработку задач.

import java.util.concurrent.CompletableFuture;

public class AsyncTask {
    private final CompletableFuture<Void> task;

    public AsyncTask(Runnable action) {
        this.task = CompletableFuture.runAsync(action);
    }

    public void onComplete(Runnable callback) {
        task.thenRun(callback);
    }

    public void onError(Runnable errorCallback) {
        task.exceptionally(ex -> {
            errorCallback.run();
            return null;
        });
    }
}

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

public class Main {
    public static void main(String[] args) {
        AsyncTask task = new AsyncTask(() -> {
            System.out.println("Task is running...");
            if (Math.random() > 0.5) throw new RuntimeException("Error occurred!");
        });

        task.onComplete(() -> System.out.println("Task completed successfully."));
        task.onError(() -> System.out.println("Task failed."));
    }
}

Преимущества:

  • Простое управление асинхронными операциями.

  • Уменьшение шаблонного кода.

Когда использовать обертки?

Использование оберток оправдано в следующих случаях:

  1. Когда исходный интерфейс слишком сложный.

  2. Когда требуется добавить новую функциональность без изменения оригинального кода.

  3. Для соблюдения принципа единственной ответственности (SRP).

  4. Для повышения читаемости и модульности кода.

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

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