JavaRush /Java блог /Random /Кофе-брейк #215. Дженерики: спасение, о котором вы даже н...

Кофе-брейк #215. Дженерики: спасение, о котором вы даже не подозревали. Что такое внедрение зависимостей в Java

Статья из группы Random

Дженерики в Java: спасение, о котором вы даже не подозревали

Источник: Medium Этот гайд посвящен работе с дженериками в Java. Вы узнаете, для чего они необходимы и как с их помощью можно ускорить работу. Кофе-брейк #215. Дженерики: спасение, о котором вы даже не подозревали. Что такое внедрение зависимостей в Java - 1Представьте себе: вы — Java-разработчик, которому поручили работу над сложным проектом, включающим в себя обработку множества различных типов данных. У вас есть много List, Map, Set, и все они заполнены разными видами объектов. Пытаться отслеживать их все — настоящий кошмар, поэтому вы постоянно беспокоитесь о том, чтобы случайно не передать объект неправильного типа методу. Но не бойтесь — у ваших проблем есть решение в виде дженериков. Дженерики (Generics) — это функция Java, которая позволяет вам писать код, способный работать с несколькими типами данных. Их можно считать своего рода заполнителями, которые в работе можно заменить реальными типами. Впервые дженерики появились в Java 5, и с тех пор они стали неотъемлемой частью синтаксиса языка Java. Давайте изучим несколько примеров кода. Допустим, вы работаете над проектом, который включает в себя сортировку списка объектов. Без дженериков вы могли бы написать что-то вроде этого:

List unsortedList = new ArrayList();
unsortedList.add("apple");
unsortedList.add("banana");
unsortedList.add(42); // упс, мы добавили целое число в список строк
Collections.sort(unsortedList);
Этот код скомпилируется без каких-либо ошибок, но при его запуске вы получите исключение ClassCastException. А все потому, что пытаетесь отсортировать список, содержащий как строки, так и целые числа. С помощью дженериков мы можем полностью избежать этой проблемы:

List<String> unsortedList = new ArrayList<>();
unsortedList.add("apple");
unsortedList.add("banana");
unsortedList.add(42); // ошибка компилятора: несовместимые типы
Collections.sort(unsortedList);
Указав, что наш список должен содержать только строки, мы устранили возможность добавления в него целого числа (integer). То есть, если мы попытаемся добавить целое число, компилятор выдаст нам ошибку. Это может показаться небольшим улучшением, но оно может избавить вас от многих головных болей в будущем. На этом сила дженериков не заканчивается. Допустим, вы работаете над методом, который должен возвращать объект определенного типа. Без дженериков вы могли бы написать что-то примерно такое:

public Object getObject() {
    // здесь какой-то код 
    return someObject;
}
Этот метод может возвращать любой тип объекта, что может быть хорошо в определенных случаях. С помощью дженериков мы можем сделать наш код более точным:

public <T> T getObject(Class<T> clazz) {
    // здесь какой-то код
    return clazz.cast(someObject);
}
Теперь наш метод может возвращать объект любого типа, если мы указываем, какой тип нам нужен, когда вызываем метод. Например:

String myString = getObject(String.class);
Integer myInteger = getObject(Integer.class);
Используя дженерики, мы сделали наш код более гибким и в то же время более конкретным. Это беспроигрышный вариант. Теперь давайте посмотрим на ситуацию, с которой я столкнулся в своей повседневной работе. Я работал над обширной устаревшей кодовой базой с более чем тысячей классов и таблиц в базе данных. Базовый код изначально был написан на Java 7 и с тех пор был обновлен до Java 8. В этом конкретном случае мы не можем реорганизовать весь базовый код для реализации Builder Factory во всех имеющихся у нас классах. Это огромная и утомительная задача. И вот здесь дженерики спасли мне кучу времени! Я реализовал универсальный класс, чтобы использовать шаблон Builder в любом классе, который мне нужен! Взгляните на класс:

public class Builder<T> {
    // Supplier — это функциональный интерфейс, который не принимает аргументов и возвращает значение. 
    // В этом случае Supplier используется для создания нового экземпляра универсального типа T. 
    private final Supplier<T> instantiator;
    
    // Список функций Consumer, где каждый Consumer каким-то образом модифицирует экземпляр T. 
    private final List<Consumer<T>> instanceModifiers = new ArrayList<>();

    // Конструктор, который принимает Consumer экземпляров T.
    public Builder(Supplier<T> instantiator) {
        this.instantiator = instantiator;
    }

    // Статический фабричный метод, который создает новый Builder с 
    заданным  экземпляром
    public static <T> Builder<T> of(Supplier<T> instantiator) {
        return new Builder<>(instantiator);
    }

    // Метод, который принимает BiConsumer и значение типа U и добавляет новый Consumer в список instanceModifiers. Новый Consumer использует BiConsumer для изменения экземпляра T заданным значением. 
    public <U> Builder<T> with(BiConsumer<T, U> consumer, U value) {
        Consumer<T> c = instance -> consumer.accept(instance, value);
        instanceModifiers.add(c);
        return this;
    }

    // Метод, который создает новый экземпляр T с помощью экземпляра, применяет к нему все instanceModifiers и возвращает результат. 
    public T build() {
        T value = instantiator.get();
        instanceModifiers.forEach(modifier -> modifier.accept(value));
        instanceModifiers.clear();
        return value;
    }
}
Этот класс позволяет нам реализовать шаблон Builder без необходимости изменять каждый класс в нашей кодовой базе. Мы можем просто создать новый экземпляр класса Builder и использовать его для создания объектов любого типа. Давайте посмотрим еще на несколько примеров кода:

public class Person {
    private String name;
    private int age;
    private String address;

    // геттеры и сеттеры
}

// использование компоновщика для создания объекта Person
Person person = Builder.of(Person::new)
                .with(Person::setName, "John Doe")
                .with(Person::setAge, 30)
                .build();
В этом примере мы используем класс Builder для создания объекта Person. Мы начинаем с создания нового экземпляра класса Builder и передачи нового объекта Person по ссылке. Затем мы используем метод with для установки полей name и age объекта Person. Наконец, мы вызываем метод build для создания конечного объекта Person. И еще один пример:

public class Car {
    private String make;
    private String model;
    private int year;

    public Car(String make, String model, int year) {
        this.make = make;
        this.model = model;
        this.year = year;
    }

    // геттеры и сеттеры
}

// использование компоновщика для создания объекта Car
Car car = Builder.of(() -> new Car(null, null, 0))
            .with(Car::setMake, "Toyota")
            .with(Car::setModel, "Camry")
            .with(Car::setYear, 2021)
            .build();
В данном примере кода мы используем класс Builder для создания объекта Car. Мы следуем той же схеме, что и раньше, создавая новый экземпляр класса Builder и передавая новый объект Car по ссылке. Затем мы используем метод with для установки полей make, model и year объекта Car. Наконец, мы вызываем метод build для создания конечного объекта Car.

Заключение

Дженерики в Java могут заметно упростить жизнь разработчикам, работающим со сложными типами данных. Они позволяют нам писать код, который является более гибким и конкретным, что может избавить от головной боли в будущем. Так что в следующий раз, когда вы будете работать над проектом Java, не забудьте использовать дженерики.

Внедрение зависимостей в Java

Источник: Medium Эта публикация поможет вам лучше понять принцип работы шаблона проектирования Внедрение зависимостей (Dependency Injection). Кофе-брейк #215. Дженерики: спасение, о котором вы даже не подозревали. Что такое внедрение зависимостей в Java - 2Внедрение зависимостей (Dependency Injection) — это шаблон проектирования, используемый в объектно-ориентированном программировании. Он предоставляет внешнюю зависимость программному компоненту. В Java внедрение зависимостей обычно реализуется с использованием Spring, Guice или CDI. Эти платформы используют комбинацию аннотаций, файлов конфигурации и отражения (reflection) для внедрения зависимостей в объекты во время их выполнения. Основная идея внедрения зависимостей заключается в том, что вместо того, чтобы объекты создавали свои собственные зависимости, они лишь объявляют, какие зависимости им нужны, а внешний объект (инжектор) отвечает за предоставление этих зависимостей при создании объекта. Предположим, что у нас есть класс UserService, который зависит от UserRepository при извлечении информации из базы данных. При внедрении зависимостей мы бы объявили UserRepository как зависимость для UserService. Тогда как инжектор будет отвечать за создание и UserService, и UserRepository, а также за внедрение UserRepository в UserService, когда он будет создан. Все это позволяет лучше разделять задачи, упрощает тестирование и повышает гибкость управления жизненным циклом объектов в крупном приложении. Вот простой пример использования среды Spring в Java. Допустим, у нас есть UserService, который зависит от UserRepository во время извлечения информации из базы данных. Мы можем использовать внедрение зависимостей, чтобы представить UserRepository в UserService.

public interface UserRepository {
    public List<User> findAll();
}

public class UserRepositoryImpl implements UserRepository {
    public List<User> findAll() {
        // получаем данные из базы данных
    }
}

public class UserService {
    private UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public List<User> getAllUsers() {
        return userRepository.findAll();
    }
}
В приведенном выше примере UserService объявляет зависимость от UserRepository в своем конструкторе. Это сообщает платформе Spring, что при создании UserService его необходимо предоставить UserRepository для внедрения. Чтобы это сделать, в Spring нужно настроить ApplicationContext. Он отвечает за создание объектов и управление их зависимостями.

@Configuration
public class AppConfig {
    @Bean
    public UserRepository userRepository() {
        return new UserRepositoryImpl();
    }

    @Bean
    public UserService userService() {
        return new UserService(userRepository());
    }
}
В этом примере мы используем аннотацию @Configuration, чтобы сообщить Spring, что это класс конфигурации. Мы также определяем два метода @Bean: userRepository() и userService(). Метод userRepository() возвращает новый UserRepositoryImpl, а метод userService() создает новый UserService и внедряет UserRepository в него с помощью метода userRepository(). Теперь, когда мы хотим использовать UserService, мы можем просто получить его из ApplicationContext:

public static void main(String[] args) {
    ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
    UserService userService = context.getBean(UserService.class);
    List<User> users = userService.getAllUsers();
}
В этом примере мы создаем новый ApplicationContext, используя класс AppConfig, а затем извлекаем компонент UserService из контекста. Поскольку UserService был создан с внедренной в него зависимостью от UserRepository, мы можем использовать ее для получения списка всех пользователей из базы данных.
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ