Привет, друзья! Сегодня рассмотрим реализацию паттерна «Строитель» в Java. Паттерн может превратить необъятный хаос параметров в аккуратную и управляемую конструкцию. И всё это на примере наших пушистых друзей — котиков.
Коротко про сам паттерн
Итак, представьте: вы решили создать класс Cat
. Только не просто кота, а настоящего гурмана, пушистого и с множеством уникальных характеристик. Имя, цвет, возраст, вес, степень пушистости, громкость мяуканья и, конечно же, любимые лакомства. Как не запутаться в этом калейдоскопе параметров?
Тут может помочь паттерн «Строитель» или как его называют на английском Builder. Технически, паттерн позволяет разделить процесс создания объекта от его представления. То есть можно создавать различные представления одного и того же объекта, изменяя только шаги строительства. К примеру в том же примере с котом, можно легко создавать разных котиков с разными наборами характеристик, не дублируя код и не усложняя конструкцию классов, это мы увидим в примерах кода ниже.
Кроме того, паттерн способствует поддержанию принципа единственной ответственности из SOLID. Допустим, класс Cat
отвечает только за хранение данных, а отдельный строитель — за их инициализацию. Это разграничение обязанностей делает код более модульным и тем самым удобным для тестирования.
Пример на котиках
Давайте сразу к делу! Представьте себе класс Cat
с множеством параметров: имя, цвет, возраст, вес, пушистость, громкость мяуканья и любимые лакомства. Для такого количества параметров идеально подходит паттерн «Строитель». И вот код:
import java.util.Collections;
import java.util.ArrayList;
import java.util.List;
public class Cat {
private final String name;
private final String color;
private final int age;
private final double weight;
private final boolean isFluffy;
private final int meowVolume;
private final List<String> favoriteFoods;
private Cat(Builder builder) {
this.name = builder.name;
this.color = builder.color;
this.age = builder.age;
this.weight = builder.weight;
this.isFluffy = builder.isFluffy;
this.meowVolume = builder.meowVolume;
this.favoriteFoods = Collections.unmodifiableList(new ArrayList<>(builder.favoriteFoods));
}
// Статический внутренний класс Builder
public static class Builder {
private final String name; // обязательный параметр
private String color = "Grey"; // значение по умолчанию
private int age = 0;
private double weight = 0.0;
private boolean isFluffy = false;
private int meowVolume = 1;
private List<String> favoriteFoods = new ArrayList<>();
public Builder(String name) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Name cannot be null or empty.");
}
this.name = name;
}
public Builder color(String color) {
if (color == null || color.trim().isEmpty()) {
throw new IllegalArgumentException("Color cannot be null or empty.");
}
this.color = color;
return this;
}
public Builder age(int age) {
if (age < 0 || age > 30) {
throw new IllegalArgumentException("Age must be between 0 and 30.");
}
this.age = age;
return this;
}
public Builder weight(double weight) {
if (weight < 0 || weight > 15) {
throw new IllegalArgumentException("Weight must be between 0 and 15 kg.");
}
this.weight = weight;
return this;
}
public Builder isFluffy(boolean isFluffy) {
this.isFluffy = isFluffy;
return this;
}
public Builder meowVolume(int meowVolume) {
if (meowVolume < 0 || meowVolume > 10) {
throw new IllegalArgumentException("Meow volume must be between 0 and 10.");
}
this.meowVolume = meowVolume;
return this;
}
public Builder addFavoriteFood(String food) {
if (food == null || food.trim().isEmpty()) {
throw new IllegalArgumentException("Food cannot be null or empty.");
}
this.favoriteFoods.add(food);
return this;
}
public Cat build() {
return new Cat(this);
}
}
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
", color='" + color + '\'' +
", age=" + age +
", weight=" + weight +
", isFluffy=" + isFluffy +
", meowVolume=" + meowVolume +
", favoriteFoods=" + (favoriteFoods.size() > 5 ? favoriteFoods.subList(0, 5) + "..." : favoriteFoods) +
'}';
}
}
Пару слов:
Все поля класса Cat
объявлены как final
, что гарантирует их неизменность после создания объекта. А внутренний статический класс Builder
содержит те же поля, что и Cat
, но с возможностью установки значений через методы.
Теперь создадим несколько разных котиков, придавая им уникальные параметры:
public class Main {
public static void main(String[] args) {
// Создаем кота с обязательными и несколькими опциональными параметрами
Cat vasiliy = new Cat.Builder("Vasiliy")
.color("White")
.age(3)
.weight(4.2)
.isFluffy(true)
.meowVolume(5)
.addFavoriteFood("Tuna")
.addFavoriteFood("Salmon")
.addFavoriteFood("Chicken")
.addFavoriteFood("Milk")
.addFavoriteFood("Cheese")
.addFavoriteFood("Fish")
.build();
// Создаем кота только с обязательным параметром
Cat murka = new Cat.Builder("Murka")
.weight(3.8)
.build();
// Создаем кота с несколькими параметрами
Cat barsik = new Cat.Builder("Barsik")
.color("Brown")
.age(5)
.isFluffy(false)
.addFavoriteFood("Beef")
.build();
System.out.println(vasiliy);
System.out.println(murka);
System.out.println(barsik);
}
}
Вывод:
Cat{name='Vasiliy', color='White', age=3, weight=4.2, isFluffy=true, meowVolume=5, favoriteFoods=[Tuna, Salmon, Chicken, Milk, Cheese...]}
Cat{name='Murka', color='Grey', age=0, weight=3.8, isFluffy=false, meowVolume=1, favoriteFoods=[]}
Cat{name='Barsik', color='Brown', age=5, weight=0.0, isFluffy=false, meowVolume=1, favoriteFoods=[Beef]}
В итоге мы получаем важную информацию:
Vasiliy: настоящий гурман! Белый, пушистый, возраст 3 года, громкое мяуканье и обожает тунцу и лосося.
Murka: небольшая, но крепкая. Весит 3.8 кг, остальные параметры по умолчанию.
Barsik: коричневый кот, не особо пушистый, весит 6.2 кг и любит курицу.
Таким образом, паттерн позволяет создавать объекты с различными конфигурациями, избегая перегруженных конструкторов.
Когда НЕ стоит использовать "Строитель"?
Паттерн «Строитель» — это мощный инструмент, но, как и все, он имеет свои границы.
-
Простые объекты с 2–3 параметрами: если объект имеет всего несколько полей, использование простого конструктора или сеттеров может быть более предпочтительным и менее избыточным.
Например: класс
Point
с полямиx
иy
. -
Слишком динамичные объекты: если объект предполагает частые изменения после создания, иммутабельность может стать препятствием. В таких случаях лучше использовать сеттеры или другие подходы.
Пример: класс
MutableConfig
, который постоянно изменяется во время работы приложения. Необходимость создания наследников: если нужно создавать множество наследуемых версий объекта с различными конфигурациями, паттерн «Строитель» может усложнить код. В таких случаях можно глянуть такой паттерн как «Абстрактная фабрика».
Заключение
Этот паттерн — отличный способ собрать сложные объекты без лишней путаницы. Пробуйте, внедряйте, и если есть вопросы — задавайте их в комментариях!
Приглашаю вас на страницу курса «Java Developer. Advanced», где вы сможете посмотреть записи прошедших вебинаров, а также зарегистрироваться на бесплатный вебинар по теме: «Знакомство с виртуальными потоками Java».
де
Комментарии (10)
arvgord
18.11.2024 05:57Раньше писал на java и теперь рад что перешёл kotlin, т.к. больше не приходится писать такой boilerpalte код. Вероятность ошибок снижается и код становится более читаемым.
Вот аналог вышеописанного Builder (исключая проверки) для класса Cat на Kotlin:
data class Cat( val name: String, val color: String = "Grey", val age: Int = 0, val weight: Double = 0.0, val isFluffy: Boolean = false, val meowVolume: Int = 1, val favoriteFoods: List<String> = emptyList() )
И вызовы этого класса разными параметрами:
val cat1 = Cat("Vasiliy") val cat2 = Cat(name = "Barsik", age = 2, favoriteFoods = listOf("Fish", "Chicken"))
Имя является обязательным параметром конструктора, остальные параметры если не будут указаны, то будет использовано значение по умолчанию. Также toString переопределяется автоматически т.к. это data класс. Мне кажется, что преимущество очевидно.
PrinceKorwin
18.11.2024 05:57исключая проверки
А если проверки будут нужны? Можете код на Котлине и для этого случая привести? Просто чтобы сравнить идентичные по функциональности вещи.
arvgord
18.11.2024 05:57Я вижу как минимум два варианта.
Использовать блок инициализации init, который будет вызываться при создании объекта и проверять свойства:
data class Cat( val name: String, val age: Int = 0 ) { init { require(name.isNotBlank()) { "Name cannot be null or empty." } require(age in 0..30) { "Age must be between 0 and 30." } } }
Второй вариант - использовать готовую библиотеку валидации Hibernate Validator:
data class Cat( @field:NotBlank(message = "Name cannot be null or empty.") val name: String, @field:Range(min = 0, max = 30, message = "Age must be between {min} and {max}.") val age: Int = 0 )
mr-garrick
18.11.2024 05:57Не понял для чего вся эта "шляпа", похоже на удаление гланд... сами знаете как. Потом кто-то будет говорить, что Java - это очень сложно, слишком много букв.
p-oleg
18.11.2024 05:57Barsik: коричневый кот, не особо пушистый, весит 6.2 кг и любит курицу.
Барсик не любит курочку. Барсик любит говядину. По крайней мере так следует из вашего кода.
Cat barsik = new Cat.Builder("Barsik")
...
.addFavoriteFood("Beef").build();
t3hk0d3
18.11.2024 05:57Интересно, а почему не record?
Ну и валидация в сеттерах билдера, а не хотябы внутри `.build()` - это дорога в спагетти ад.
Если надо провалидировать user-input - лучше пользоваться Bean Validation, чем писать чеки вручную непонятно где.public record Cat( @NotNull(message = "Name cannot be null") String name, @Min(value = 0, message = "Age should not be less than 0") @Max(value = 30, message = "Age should not be greater than 30") int age, ... ) { private Cat(Cat.Builder builder) { // ИМХО так лучше так не делать, а передавать все параметры в билдере честно // потому-что меньше кода -> лучше код // но это скорее вопрос вкуса ... } public static class Builder { ... public Cat build() { return validator.validate(new Cat(this)); // throws exception if validation failed } } }
(код сверху это не точный пример того как надо использовать 'jakarta.validation', просто пример упрощенный для наглядности)
Zoolander
А в этом языке сейчас нельзя в конструктор передать сразу дата объект с опциональными полями вместо того чтобы вызывать кучу методов?
Я просто интересуюсь просто я давно на джаве не пишу. Сижу на другом языке где у объектов позволены опциональные поля. Поэтому о патерне билдер давно не слышал потому что вместо вот этих методов можно просто в конструктор передать конфигурационный объект только с теми полями которые нужны.
PrinceKorwin
Можно, конечно. Также можно наделать кучу конструкторов для инициализации только нужных полей. А можно использовать Builder. Все зависит от задачи.
Билдер удобен и тем, что он даёт больше гарантий компиляции. При добавлении нового поля и изменения конструктора предыдущий код (использующий этот конструктор) перестанет компилироваться даже если это поле опционально. С билдером такой проблемы не будет.
Zoolander
Мой комментарий - не критика языка, а критика моего предыдущего комментария ))
простите, я забыл что в Java нет записи объектов в виде литералов ))
Эти литералы по сути и убили для меня паттерн Builder в языках, на которых я пишу
потому что можно писать вот так и не нужно париться с конструкторами
или в Java все-таки появились литералы и я давно не обновлял кодекс?
вообще опциональные поля и синтаксические проверки в JS/TS убили море паттернов, которые были просто компенсаторами языковых конструкций
Java хороша в миллионе мест, но боже, как же мне грустно, что в ней не хватает некоторых декларативных вещей, как эти литералы объектов
По сути литералы объектов - это удобный синтаксис для одноразовых объектов
PS: проверка типов на этапе компиляции в TypeScript есть, а я пишу только на нем, поэтому классический Builder я не видел уже со времен динозавров
PrinceKorwin
Понимаю о чем вы говорите. Кстати, у Builder есть еще одно не частое применение - это передача билдера как аргумента вызова. Удобно для переопребеления дефолтных значений. С конструктором такого не провернуть.