Введение
В процессе разработки часто возникает необходимость в параметризации сложного процесса, которая сопровождается использованием не менее сложных моделей данных.
Инициализация подобных моделей через ломбоковский билдер может привести к некорректной конфигурации объекта, особенно если разработчик недостаточно хорошо знаком со спецификой процесса.
Можно пойти по пути использования конструктора, да это надежно, но бывает неудобно либо из-за большого количества параметров, либо из за их чрезмерной сложности.
Золотая середина между гибкостью и безопасностью – это 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 — мощный инструмент, который помогает обеспечить строгую последовательность при построении объектов и предоставляет интуитивный интерфейс.
ссылка на оригинал статьи https://habr.com/ru/articles/863446/
Добавить комментарий