
Неизменяемые объекты в Java с использованием шаблона Builder с функциональным интерфейсом
Почему важны неизменяемые объекты
Неизменяемые объекты в Java — это объекты, состояния которых нельзя изменить после создания. Этот атрибут неизменности дает несколько преимуществ:
Потокобезопасность.Неизменяемые объекты по своей природе потокобезопасны, поскольку их состояние не может измениться после создания. Это делает их подходящими для многопоточных сред, где важны синхронизация и безопасность потоков.
Простота и ясность. Неизменяемые объекты проще проектировать, реализовывать и использовать. Поскольку они не могут изменить свое состояние, вам не нужно беспокоиться об изменении их состояния с течением времени. Это делает код понятным и простым для понимания.
Хеширование. Неизменяемые объекты отлично подходят для использования в качестве ключей в HashMap или элементов в HashSet, поскольку их хэш-код остается постоянным.
Угрозы безопасности при десериализации и RCE.Уязвимости десериализации и удаленного выполнения кода (RCE) в Java часто связаны с изменяемыми объектами. Десериализация преобразует поток байтов обратно в объект. Если этот процесс происходит без надлежащей проверки, это может привести к RCE. Изменяемые объекты, которые могут изменять свое состояние после создания, могут быть использованы во время десериализации для изменения потока управления программой или вызова произвольных функций. Примером этого риска является уязвимость десериализации коллекций Apache Commons (CVE-2015–4852), когда во время десериализации манипулировали изменяемыми объектами, что приводило к RCE. Неизменяемые объекты, которые не могут изменить свое состояние после создания, по своей природе устойчивы к таким манипуляциям, что подчеркивает важность неизменности при обработке потенциально ненадежных входных данных.
Как сделать объекты Java неизменяемыми с помощью функционального интерфейса и шаблона Builder
Чтобы проиллюстрировать, как создавать сложные неизменяемые объекты с помощью шаблона Builder и функционального интерфейса, давайте представим, что мы создаем объект House. Этот дом содержит несколько других объектов, в том числе MainDoor, Terrace, Lobby, MainBedroom, GuestRoom, каждый со своими свойствами. Мы хотим создать эти объекты неизменным образом и интегрировать их в объект House.
Настройка проекта
Весь код этой статьи размещен на GitHub и доступен здесь. Это проект на основе Maven, разработанный в Java 17. Пожалуйста, клонируйте этот репозиторий и следите за нами, пока мы обсуждаем различные классы и их функции.
Определение функционального интерфейса
Начнем с определения функционального интерфейса настройщика. Этот интерфейс используется в наших классах построителей, чтобы предоставить клиентам механизм настройки создаваемых объектов.
@FunctionalInterface
public interface Customizer<T> {
T customize(T t);
}
Определение класса MainDoor
Класс MainDoor является примером того, как строится каждый компонент House. Класс объявлен как final, а все его поля являются private final, что делает его неизменяемым классом. Он использует шаблон Builder для создания экземпляра, который возвращается с помощью метода build().
public final class MainDoor {
private final String doorType;
private final String doorColor;
private final String doorLockType;
// Constructor is private to control the creation of instances.
private MainDoor(Builder builder) {
this.doorType = builder.doorType;
this.doorColor = builder.doorColor;
this.doorLockType = builder.doorLockType;
}
// Getters for all fields, no setters provided to maintain immutability.
public String getDoorType() {
return doorType;
}
public String getDoorColor() {
return doorColor;
}
public String getDoorLockType() {
return doorLockType;
}
// The Builder class
public static class Builder {
private String doorType;
private String doorColor;
private String doorLockType;
public Builder doorType(String doorType) {
this.doorType = doorType;
return this;
}
public Builder doorColor(String doorColor) {
this.doorColor = doorColor;
return this;
}
public Builder doorLockType(String doorLockType) {
this.doorLockType = doorLockType;
return this;
}
public MainDoor build() {
return new MainDoor(this);
}
}
}
Определение дома
Класс House использует Builder для инкапсуляции логики построения. Каждый компонент создается с помощью Customizer и включается в объект House. Этот дизайн позволяет клиенту использовать лямбда-выражения для настройки каждого компонента дома во время строительства.
public final class House {
private final MainDoor mainDoor;
//... other components
// Private constructor to control instance creation.
private House(Builder builder) {
this.mainDoor = builder.mainDoor;
//... other components
}
// Getters for all components, no setters to maintain immutability.
public MainDoor getMainDoor() {
return mainDoor;
}
//... other getters
public static class Builder {
private MainDoor mainDoor;
//... other components
public Builder mainDoor(Customizer<MainDoor.Builder> customizer) {
this.mainDoor = customizer.customize(new MainDoor.Builder()).build();
return this;
}
//... other builder methods
public House build() {
return new House(this);
}
}
}
Строительство дома
Объект House может быть построен с использованием лямбда-выражений для настройки каждого компонента. Этот подход интуитивно понятен и обеспечивает превосходную гибкость при создании объектов.
House house = new House.Builder()
.mainDoor(md -> md.doorType("Wooden Door"))
.terrace(t -> t.view("Garden View").size(300).hasGarden(true))
.lobby(l -> l.style("Modern").area(500).hasFurniture(true))
.mainBedroom(mb -> mb.wallColor("White").bedType("King Size").hasAirConditioner(true))
.guestRoom(gr -> gr.wallColor("Cream").bedType("Queen Size").hasAirConditioner(false))
.build();
Вот и все! Надежный метод создания неизменяемых объектов в Java. Комбинируя шаблон Builder с функциональным интерфейсом, мы получаем значительный контроль над созданием объектов.
Если вы хотите поиграть еще немного, зайдите и проверьте репозиторий.
Примечание. По крайней мере, для больших объектов конфигурации лучше следовать этому правилу.
Отказ от ответственности. Мнения, отраженные в этой статье, являются взглядами автора и не обязательно отражают взгляды любого бывшего или настоящего работодателя автора.