В мире разработки обертки (wrappers) — это мощный инструмент, который позволяет инкапсулировать сложную логику, добавлять новую функциональность или адаптировать существующие классы для более удобного использования. В этой статье мы рассмотрим, что такое обертки, какие задачи они решают, и как их правильно создавать на Java. Мы также приведем примеры реального применения, чтобы показать их пользу.
Отмечу, что приведенные примеры, не являются практической рекомендацией применения оберток, а лишь показывают варианты создания оберток.
Что такое обертка (wrapper)?
Обертка — это класс, который "оборачивает" другой объект или функциональность, предоставляя новый интерфейс, изменяя поведение или добавляя дополнительные функции. Обертки часто используют для следующих целей:
- Инкапсуляция логики. Упрощение взаимодействия с более сложными системами. 
- Добавление новой функциональности. Расширение поведения объекта без изменения исходного кода. 
- Повышение удобства использования. Создание более понятного или лаконичного интерфейса. 
- Интеграция. Адаптация сторонних библиотек или 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();
        }
    }
}Для корректной работы кода, привожу класс ConnectionWrapper:
import java.sql.*;
public class ConnectionWrapper implements AutoCloseable {
    private final Connection connection;
    public ConnectionWrapper(String url, String user, String password) throws SQLException {
        this.connection = DriverManager.getConnection(url, user, password);
    }
    public ConnectionWrapper(String url) throws SQLException {
        this.connection = DriverManager.getConnection(url);
    }
    public void executeQuery(String query) throws SQLException {
        try (Statement stmt = connection.createStatement()) {
            stmt.execute(query);
        }
    }
    @Override
    public void close() throws SQLException {
        if (connection != null && !connection.isClosed()) {
            connection.close();
        }
    }
}Преимущества:
- Автоматизация рутинных задач (закрытие ресурсов). 
- Улучшение читаемости кода. 
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."));
    }
}Преимущества:
- Простое управление асинхронными операциями. 
- Уменьшение шаблонного кода. 
Когда использовать обертки?
Использование оберток оправдано в следующих случаях:
- Когда исходный интерфейс слишком сложный. 
- Когда требуется добавить новую функциональность без изменения оригинального кода. 
- Для соблюдения принципа единственной ответственности (SRP). 
- Для повышения читаемости и модульности кода. 
Обертки в Java — это универсальный инструмент, который помогает разработчикам улучшать качество и удобство работы с кодом. Они могут быть использованы в самых разных сценариях: от простой валидации данных до интеграции с внешними системами. Если правильно их применять, обертки значительно упрощают разработку и делают ваш код более поддерживаемым.
Комментарии (2)
 - aleksandy23.12.2024 14:45- TL/DR. КГ/АМ - public class ConnectionWrapper {
 ...
 - try (ConnectionWrapper db = new ConnectionWrapper("jdbc:h2:mem:test", "user", "password")) {- И на основании чего такой код будет скомпилирован? - Создадим обертку, упрощающую обработку задач. - Вообще какой-то говнокод. Что такого делает обёртка, чего не делает - CompletableFuture.
 
           
 
DenSigma
Вы работали в проекте, в котором классы как капустные кочаны? Не рекомендую.
Не надо оборачивать стандартные классы, это усложняет понимание, стандартные классы нужно знать, понимать и любить.
SOLID! Классы должны делать ровно то, что они должны делать. Класс списка не должен заниматься логированием. Если это пример (согласен), то неудачный.
С библиотеками - согласен, полезно. Но с библиотеками, это называется "адаптер". Такой адаптер должен обеспечивать интерфейс не только для данной библиотеки, а для всего множества аналогов, раз. И должен обеспечивать легкую замену одной библиотеки другой, всякий DI и прочий SOLID, пример хороший, но лучше развить.