Введение
В процессе разработки часто возникает необходимость в параметризации сложного процесса, которая сопровождается использованием не менее сложных моделей данных.
Инициализация подобных моделей через ломбоковский билдер может привести к некорректной конфигурации объекта, особенно если разработчик недостаточно хорошо знаком со спецификой процесса.
Можно пойти по пути использования конструктора, да это надежно, но бывает неудобно либо из-за большого количества параметров, либо из за их чрезмерной сложности.
Золотая середина между гибкостью и безопасностью – это Stage Builder. Этот подход позволяет:
-
Четко разделить процесс построения объекта на этапы;
-
Гарантировать последовательное заполнение всех обязательных полей;
-
Работать с необязательными параметрами и предусматривать их отсутствие;
-
Упростить поддержку кода, делая процесс конфигурации более интуитивным.
Несколько подходов к реализации подобного билдера представлены в статье "Next-level Java 8 staged builders", здесь же я расскажу как развил эту идею.
1. Optional stage
В оригинальном подходе все этапы считаются обязательными. Однако часто требуется добавить опциональные этапы, которые могут быть пропущены.
Пример реализации:
@Getter
@AllArgsConstructor(access = PRIVATE)
@RequiredArgsConstructor(access = PRIVATE)
public class Model {
private final String requiredFieldFirst;
private final String requiredFieldSecond;
private String optionalField;
public static RequireFieldFirst<RequireFieldSecond<OptionalStage>> builder() {
return requiredFieldFirst -> requiredFieldSecond -> new OptionalStage(requiredFieldFirst,
requiredFieldSecond);
}
public static class StageBuilder {
@FunctionalInterface
public interface RequireFieldFirst<T> {
T requiredFieldFirst(String requiredFieldFirst);
}
@FunctionalInterface
public interface RequireFieldSecond<T> {
T requiredFieldSecond(String requiredFieldSecond);
}
@FunctionalInterface
public interface OptionalField<T> {
T optionalField(String optionalField);
}
@AllArgsConstructor(access = PRIVATE)
@RequiredArgsConstructor(access = PRIVATE)
public static class FinalStage {
protected final String requiredFieldFirst;
protected final String requiredFieldSecond;
private String optionalField;
public Model build() {
return new Model(requiredFieldFirst, requiredFieldSecond, optionalField);
}
}
public static class OptionalStage extends FinalStage implements OptionalField<FinalStage> {
private OptionalStage(String requiredFieldFirst, String requiredFieldSecond) {
super(requiredFieldFirst, requiredFieldSecond);
}
@Override
public FinalStage optionalField(String optionalField) {
return new FinalStage(requiredFieldFirst, requiredFieldSecond, optionalField);
}
}
}
}
Благодаря тому, что этапы - дженерики, порядок инициализации полей можно описать в статичном методе по созданию билдера.
public static RequireFieldFirst<RequireFieldSecond<OptionalBuilder>> builder() {
return requiredFieldFirst -> requiredFieldSecond -> new OptionalBuilder(requiredFieldFirst,
requiredFieldSecond);
}
За счет этого при создании объекта порядок прибит гвоздями.
Опциональность достигается благодаря классу OptionalStage.
public static class OptionalStage extends FinalStage implements OptionalField<FinalStage> {
private OptionalStage(String requiredFieldFirst, String requiredFieldSecond) {
super(requiredFieldFirst, requiredFieldSecond);
}
@Override
public FinalStage optionalField(String optionalField) {
return new FinalStage(requiredFieldFirst, requiredFieldSecond, optionalField);
}
}
@AllArgsConstructor(access = PRIVATE)
@RequiredArgsConstructor(access = PRIVATE)
public static class FinalStage {
protected final String requiredFieldFirst;
protected final String requiredFieldSecond;
private String optionalField;
public Model build() {
return new Model(requiredFieldFirst, requiredFieldSecond, optionalField);
}
}
Он наследует FinalStage и имплементирует OptionalField, за счет чего в процессе билда появляется развилка:
Пример использования:
Model build = Model.builder()
.requiredFieldFirst("first")
.requiredFieldSecond("second")
.build();
Model build = Model.builder()
.requiredFieldFirst("first")
.requiredFieldSecond("second")
.optionalField("optional")
.build();
2. Map builder
Чтобы показать преимущества билдера при создании мап, примера уровня hello world может не хватить, поэтому рассмотрим код из моего проекта. Это небольшой кусок параметризации стратегии по работе с criteria api. В следующей статье опишу целиком. Получилось на основании JpaSpecificationExecutor изобрести абстрактную стратегию построения списков с поиском, сортировкой, фильтрацией и пагинацией.
Пример реализации:
@Getter
@RequiredArgsConstructor(access = PRIVATE)
public class SearchCriteriaConfig {
private final Map<String, Function<From<?, ?>, List<Path<?>>>> searchConfigMap;
public static TableName<SearchColumn<FinalStage>> builder() {
return tableName -> columnName -> new FinalStage(tableName, new HashMap<>(
Map.of(tableName, new ArrayList<>(List.of(columnName)))));
}
public static class SearchRuleStageBuilder {
@FunctionalInterface
public interface TableName<T> {
T tableName(String tableName);
}
@FunctionalInterface
public interface SearchColumn<T> {
T searchInColumn(String searchColumn);
}
@AllArgsConstructor(access = PRIVATE)
public static class FinalStage implements SearchColumn<FinalStage>, TableName<SearchColumn<FinalStage>> {
private String tableName;
private Map<String, List<String>> tableColumnMap;
public SearchCriteriaConfig build() {
Map<String, Function<From<?, ?>, List<Path<?>>>> tableConfigMap =
tableColumnMap.entrySet()
.stream()
.collect(toMap(Map.Entry::getKey,
entry -> from -> entry.getValue()
.stream()
.map(from::get)
.collect(toList())));
return new SearchCriteriaConfig(tableConfigMap);
}
@Override
public FinalStage searchInColumn(String searchColumn) {
List<String> columnNames = tableColumnMap.get(tableName);
columnNames.add(searchColumn);
return new FinalStage(tableName, tableColumnMap);
}
@Override
public SearchColumn<FinalStage> tableName(String tableName) {
return searchColumn -> {
this.tableName = tableName;
if (tableColumnMap.containsKey(tableName)) {
List<String> searchParams = tableColumnMap.get(tableName);
searchParams.add(searchColumn);
} else {
tableColumnMap.put(tableName, new ArrayList<>(List.of(searchColumn)));
}
return new FinalStage(tableName, tableColumnMap);
};
}
}
}
}
Немного контекста. В данном классе содержится мапа, которая описывает в каких колонках в таблице необходимо будет произвести поиск переданного с клиента значения.
Map<String, Function<From<?, ?>, List<Path<?>>>> searchConfigMap;
//Наименование таблицы против функции извлечения путей до колонок из таблицы
Вся магия происходит тут:
FinalStage implements SearchColumn<FinalStage>, TableName<SearchColumn<FinalStage>>
За счет такой имплементации, после завершения порядка этапов, описанного в статичном методе, появляется развилка: добавить еще одну колонку, добавить новую таблицу или произвести билд.
Статический метод с описанием порядка:
public static TableName<SearchColumn<FinalStage>> builder() {
return tableName -> columnName -> new FinalStage(tableName, new HashMap<>(
Map.of(tableName, new ArrayList<>(List.of(columnName)))));
}
Развилка:
И вишенка на торте метод build, где мы можем инкапсулировать всю логику.
public SearchCriteriaConfig build() {
Map<String, Function<From<?, ?>, List<Path<?>>>> tableConfigMap =
tableColumnMap.entrySet()
.stream()
.collect(toMap(Map.Entry::getKey,
entry -> from -> entry.getValue()
.stream()
.map(from::get)
.collect(toList())));
return new SearchCriteriaConfig(tableConfigMap);
}
На выходе получаем удобный интерфейс для создания сложного объекта.
SearchCriteriaConfig config = SearchCriteriaConfig.builder()
.tableName("client")
.searchInColumn("name")
.tableName("client_contact")
.searchInColumn("email")
.build();
Заключение
Staged builder — мощный инструмент, который помогает обеспечить строгую последовательность при построении объектов и предоставляет интуитивный интерфейс.
Автор: petr-ananev