Fluent API.md
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Struchkov Mark 2024-10-04 01:19:00 +03:00
parent ce9f2d25e7
commit 46e423c2db
No known key found for this signature in database
GPG Key ID: A3F0AC3F0FA52F3C
8 changed files with 709 additions and 6 deletions

View File

@ -0,0 +1,318 @@
---
aliases:
tags:
- maturity/🌱
date: 2024-10-04
zero-link:
parents:
linked:
---
Builder Pattern — это [[порождающий паттерн проектирования]], который используется для пошагового создания сложных объектов. Этот паттерн особенно полезен, когда объект может иметь множество конфигураций или параметров, которые делают его создание через конструкторы неудобным или даже невозможным.
Основные концепции
- **Разделение построения и представления**. Билдер позволяет отделить логику создания объекта от его конечной структуры. Это делает код более чистым и поддерживаемым.
- **Пошаговая сборка**. Позволяет добавлять параметры или части объекта последовательно, при этом сам процесс создания может контролироваться и меняться независимо от основной логики объекта.
- **Иммутабельность**. Паттерн билдер часто применяется для создания неизменяемых объектов. После сборки объекта его нельзя изменить, что улучшает предсказуемость и безопасность.
## Пример применения
Рассмотрим создание объекта `Car`, у которого много настроек, таких как тип двигателя, количество дверей, цвет и т.д.
Без использования паттерна «Билдер» мы можем столкнуться с такой проблемой: необходимо создавать различные конструкторы, что ухудшает читаемость и поддержку кода. Паттерн «Билдер» помогает избежать этого, при этом используя [[Fluent API|fluent API]] стиль — подход, при котором методы возвращают сам объект билдера, позволяя вызывать их цепочкой. Это делает код более выразительным и легким для чтения.
```java
public class Car {
private String engine;
private int doors;
private String color;
private Car(CarBuilder builder) {
this.engine = builder.engine;
this.doors = builder.doors;
this.color = builder.color;
}
public static class CarBuilder {
private String engine;
private int doors;
private String color;
public CarBuilder setEngine(String engine) {
this.engine = engine;
return this;
}
public CarBuilder setDoors(int doors) {
this.doors = doors;
return this;
}
public CarBuilder setColor(String color) {
this.color = color;
return this;
}
public Car build() {
return new Car(this);
}
}
}
```
Использование паттерна:
```java
Car car = new Car.CarBuilder()
.setEngine("V8")
.setDoors(4)
.setColor("Red")
.build();
System.out.println(car);
```
**Преимущества**
1. **Чистый код**. Конфигурация объектов становится ясной и понятной, даже если у объекта множество параметров.
2. **Гибкость в создании объектов**. Можно не указывать все параметры сразу, а добавлять их по мере необходимости, что делает процесс сборки более гибким.
3. **Поддержка иммутабельности**. Объекты могут быть неизменяемыми после создания, так как параметры устанавливаются только в процессе сборки.
4. **Минимизация перегрузок конструкторов**. Это позволяет избежать множества конструкторов для различных комбинаций параметров.
**Недостатки**
- **Усложнение кода**. Добавление класса-билдера может увеличить объем кода, особенно если объект не настолько сложен, чтобы оправдать использование паттерна.
- **Многословность**. Если объект требует только нескольких параметров, то использование билдера может казаться излишним и создавать ненужную многословность.
**Когда применять?**
- Когда объект имеет множество конфигураций и параметров.
- Когда нужен гибкий процесс создания объектов с возможностью пошагового добавления параметров.
- Когда объект должен быть неизменяемым, и после сборки его состояние не должно меняться.
## Продвинутый билдер
### Обязательные поля
Одной из распространенных проблем является ==отсутствие явного указания на обязательные поля== при использовании билдера. Если объект имеет поля, которые обязательны для заполнения (например, идентификатор или имя), но их установка не контролируется билдером, это может привести к созданию некорректных или невалидных объектов.
**Решение**:
- Обязательные поля можно передавать через конструктор билдера, чтобы их указание было обязательным. Остальные параметры можно указывать через цепочку методов.
Пример:
```java
public class Car {
private final String engine; // Обязательное поле
private final String model; // Обязательное поле
private int doors; // Необязательное поле
private String color; // Необязательное поле
// Приватный конструктор для сборки объекта через билдер
private Car(CarBuilder builder) {
this.engine = builder.engine;
this.model = builder.model;
this.doors = builder.doors;
this.color = builder.color;
}
// Статический метод для создания билдера с обязательными полями
public static CarBuilder builder(String engine, String model) {
return new CarBuilder(engine, model);
}
public static class CarBuilder {
private final String engine; // Обязательное поле
private final String model; // Обязательное поле
private int doors = 4; // Значение по умолчанию
private String color = "Black"; // Значение по умолчанию
// Конструктор билдера с обязательными полями
public CarBuilder(String engine, String model) {
if (engine == null || engine.isEmpty()) {
throw new IllegalArgumentException("Engine is required");
}
if (model == null || model.isEmpty()) {
throw new IllegalArgumentException("Model is required");
}
this.engine = engine;
this.model = model;
}
// Методы для установки необязательных полей
public CarBuilder setDoors(int doors) {
this.doors = doors;
return this;
}
public CarBuilder setColor(String color) {
this.color = color;
return this;
}
// Метод для сборки объекта Car
public Car build() {
return new Car(this);
}
}
}
```
Теперь обязательные поля передаются при создании билдера:
```java
Car car = Car.builder("V8", "Sedan") // Передача обязательных полей через статический метод
.setDoors(2) // Опциональные поля
.setColor("Red")
.build();
```
### Валидация создания объекта
Ещё одна частая проблема заключается в том, что во время процесса построения ==не проверяются ограничения на совместимость полей.== Например, не всегда проверяется корректность значений или логика взаимодействия между параметрами, что может привести к созданию некорректного объекта.
**Решение**:
Добавляйте в билдер логику валидации и проверки состояния перед созданием объекта. Это позволит убедиться, что все параметры совместимы и объект корректен.
Пример:
```java
public Car build() {
if (doors < 2 || doors > 6) {
throw new IllegalArgumentException("Invalid number of doors");
}
return new Car(this);
}
```
### Многократный вызов методов
Когда методы билдера вызываются несколько раз, каждый вызов может перезаписывать значение параметра, что может остаться незамеченным или вызвать непредсказуемое поведение.
Предположим, что у нас есть билдер для создания объекта `Car`, и метод для установки количества дверей (`setDoors`) был вызван дважды:
```java
Car car = new Car.CarBuilder("V8")
.setDoors(4)
.setDoors(2) // Этот вызов перезапишет предыдущее значение
.setColor("Red")
.build();
```
В результате объект car будет создан с двумя дверями, хотя программист мог ожидать, что будет 4 двери (из-за первого вызова). Такая ситуация особенно распространена, когда объект конфигурируется динамически, или когда несколько разработчиков работают с билдером, не зная всех деталей.
#### Возможные решения проблемы
**Введение проверок на повторный вызов.** Чтобы избежать многократных вызовов одного и того же метода, можно добавить логику проверки, которая будет отслеживать, был ли метод уже вызван ранее. Если метод вызывается повторно, можно выбросить исключение или проигнорировать повторный вызов.
```java
public static class CarBuilder {
private String engine;
private int doors;
private String color;
private boolean doorsSet = false; // Флаг, указывающий на то, что метод setDoors уже был вызван
public CarBuilder(String engine) {
this.engine = engine;
}
public CarBuilder setDoors(int doors) {
if (doorsSet) {
throw new IllegalStateException("Doors can only be set once");
}
this.doors = doors;
doorsSet = true;
return this;
}
public CarBuilder setColor(String color) {
this.color = color;
return this;
}
public Car build() {
return new Car(this);
}
}
```
**Логирование перезаписи параметров.** Если необходимо разрешить многократные вызовы методов, но при этом важно отслеживать перезапись параметров, можно добавлять логирование, чтобы было видно, когда параметр перезаписывается новым значением.
```java
public CarBuilder setDoors(int doors) {
if (this.doors != 0) {
System.out.println("Warning: Doors value is being overwritten from " + this.doors + " to " + doors);
}
this.doors = doors;
return this;
}
```
**Использование** [[Fluent API#Step building|Fluent API Step building]]. Позволит конфигурировать объект в определенной последовательности.
```java
public class Car {
private final String engine; // Обязательное поле
private final int doors; // Обязательное поле
private final String color; // Обязательное поле
// Приватный конструктор для сборки через пошаговую сборку
private Car(String engine, int doors, String color) {
this.engine = engine;
this.doors = doors;
this.color = color;
}
// Интерфейс для первого шага: выбор двигателя
public interface EngineStep {
DoorsStep setEngine(String engine);
}
// Интерфейс для второго шага: выбор дверей
public interface DoorsStep {
ColorStep setDoors(int doors);
}
// Интерфейс для третьего шага: выбор цвета
public interface ColorStep {
BuildStep setColor(String color);
}
// Интерфейс для финального шага: завершение сборки
public interface BuildStep {
Car build();
}
// Класс, который реализует пошаговую сборку
public static class Builder implements EngineStep, DoorsStep, ColorStep, BuildStep {
private String engine;
private int doors;
private String color;
@Override
public DoorsStep setEngine(String engine) {
this.engine = engine;
return this;
}
@Override
public ColorStep setDoors(int doors) {
this.doors = doors;
return this;
}
@Override
public BuildStep setColor(String color) {
this.color = color;
return this;
}
@Override
public Car build() {
return new Car(engine, doors, color);
}
}
// Метод для запуска пошаговой сборки
public static EngineStep builder() {
return new Builder();
}
}
```
***
## Мета информация
**Область**:: [[../../meta/zero/00 Разработка|00 Разработка]]
**Родитель**:: [[Порождающий паттерн проектирования]]
**Источник**::
**Создана**:: [[2024-10-04]]
**Автор**::
### Дополнительные материалы
-
### Дочерние заметки
<!-- QueryToSerialize: LIST FROM [[]] WHERE contains(Родитель, this.file.link) or contains(parents, this.file.link) -->

View File

@ -9,7 +9,7 @@ zero-link:
- "[[../../meta/zero/00 Архитектура ПО|00 Архитектура ПО]]"
parents:
- "[[Inversion of Control]]"
- "[[Паттерн программирования]]"
- "[[Паттерн проектирования]]"
linked:
---
**Dependency Injection (DI)** — это паттерн проектирования, который используется для реализации принципа [[Inversion of Control]] (IoC). DI позволяет передавать зависимости объектам извне, вместо того чтобы объекты сами создавали их. Это ослабляет связь между компонентами системы, что делает код более гибким и удобным для поддержки.
@ -60,7 +60,7 @@ class Car {
***
## Мета информация
**Область**:: [[../../meta/zero/00 Архитектура ПО|00 Архитектура ПО]]
**Родитель**:: [[Inversion of Control]], [[Паттерн программирования]]
**Родитель**:: [[Inversion of Control]], [[Паттерн проектирования]]
**Источник**::
**Автор**::
**Создана**:: [[2023-10-06]]

View File

@ -0,0 +1,356 @@
---
aliases:
tags:
- maturity/🌱
date: 2024-10-04
zero-link:
parents:
linked:
---
**Fluent API** — это стиль проектирования API, в котором ==методы возвращают объект, к которому они принадлежат==, позволяя вызывать методы цепочкой (chaining).
**Основные концепции**
- **Method Chaining**. Fluent API позволяет вызывать методы один за другим, что уменьшает количество промежуточных переменных и улучшает читаемость.
- **Самоописывающийся код**. Использование цепочки методов делает код более понятным и логичным, приближая его к естественному языку.
**Где встречается?**
- Фреймворки с реактивным подходом.
- Java Stream
- **Библиотеки для работы с базами данных**. Такие фреймворки, как JPA или Hibernate, используют Fluent API для создания запросов. Например, запросы могут выглядеть так
```java
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Car> query = builder.createQuery(Car.class);
query.select(query.from(Car.class))
.where(builder.equal(root.get("color"), "Red"));
```
- **Настройка объектов**. Fluent API часто используется в [[../../../../garden/ru/dev/architecture/Builder Pattern|Builder Pattern]], где объект строится поэтапно через цепочку методов.
- Конфигурация. Например Spring Security, Kafka Streams
- **Фреймворки для тестирования**. Например, в JUnit или AssertJ можно строить цепочки утверждений:
Fluent API часто используется для построения специфических языков (DSL), которые имитируют человеческий язык и делают код максимально самоописательным.
Пример императивного кода
```java
Instant start = Instant.now();
Duration timeout = Duration.ofSeconds(10);
do {
Thread.sleep(200);
var entity = repo.get("id");
if ("EXPECTED".equals(entity.status)) {
return;
} while (Instant.now().isBefore(start.plus(timeout)));
throw new AssertionError("Status was not updated to EXPECTED");
```
И аналогичный в стиле Fluent API
```java
Awaitility.await("Entity status should be updated to EXPECTED")
.atMost(Duration.ofSeconds(10))
.pollDelay(Duration.ofMillis(200))
.until(() -> "EXPECTED".equals(repo.get("id").status));
```
## Приемы и подходы
### Method chaining
**Method chaining** — это техника, при которой методы возвращают текущий объект (обычно через `this`), позволяя вызывать несколько методов последовательно в одной строке.
```java {7, 12}
public class Car {
private String engine;
private int doors;
public Car setEngine(String engine) {
this.engine = engine;
return this; // Возвращаем текущий объект
}
public Car setDoors(int doors) {
this.doors = doors;
return this;
}
}
```
### Смена контекста
#### С помощью method chaining
Представим, что мы настраиваем серверное приложение с несколькими аспектами: базовая настройка, настройка безопасности, логирования и т.д. Здесь каждый вызов метода переключает нас на новый “контекст”, где мы продолжаем настраивать приложение, но в рамках другой области (например, с безопасности переключаемся на логирование).
```java
public class ServerConfig {
public ServerConfig http() {
System.out.println("HTTP basic configuration");
return this; // Возвращаем тот же объект для продолжения цепочки
}
public ServerConfig security() {
System.out.println("Security configuration");
return this; // Переключение на контекст безопасности
}
public ServerConfig authorizeRequests() {
System.out.println("Authorization configuration");
return this; // Переключение на настройку авторизации запросов
}
public ServerConfig requestMatchers(String pattern) {
System.out.println("Configuring request matchers for: " + pattern);
return this; // Продолжение работы в контексте авторизации
}
public ServerConfig csrf() {
System.out.println("CSRF protection disabled");
return this; // Переключение на настройку защиты CSRF
}
public ServerConfig exceptionHandling() {
System.out.println("Exception handling configuration");
return this; // Переключение на обработку исключений
}
public Server build() {
System.out.println("Server is configured and built");
return new Server();
}
}
class Server {
// Имитация запущенного сервера
}
```
```java
Server server = new ServerConfig()
.http() // Контекст базовой настройки HTTP
.security() // Переключение на контекст безопасности
.authorizeRequests() // Настройка авторизации запросов
.requestMatchers("/") // Настройка доступа для главной страницы
.requestMatchers("/api") // Настройка доступа к API
.csrf() // Отключение CSRF
.exceptionHandling() // Настройка обработки исключений
.build(); // Завершаем конфигурацию и запускаем сервер
```
#### С помощью лямбда-выражений
```java
public class ServerConfig {
public ServerConfig http(Consumer<HttpConfig> httpConfig) {
System.out.println("Entering HTTP configuration context");
httpConfig.accept(new HttpConfig());
return this; // Возвращаем тот же объект для дальнейшей конфигурации
}
public ServerConfig security(Consumer<SecurityConfig> securityConfig) {
System.out.println("Entering Security configuration context");
securityConfig.accept(new SecurityConfig());
return this; // Переключение на контекст безопасности
}
public ServerConfig logging(Consumer<LoggingConfig> loggingConfig) {
System.out.println("Entering Logging configuration context");
loggingConfig.accept(new LoggingConfig());
return this; // Переключение на контекст логирования
}
public Server build() {
System.out.println("Server is configured and built");
return new Server(); // Финальный этап — запуск сервера
}
// Вложенные классы конфигураций для разных контекстов
public static class HttpConfig {
public HttpConfig enableHttp2() {
System.out.println("HTTP/2 enabled");
return this;
}
public HttpConfig port(int port) {
System.out.println("Server will listen on port: " + port);
return this;
}
}
public static class SecurityConfig {
public SecurityConfig enableTLS() {
System.out.println("TLS enabled");
return this;
}
public SecurityConfig authorizeRequests(Consumer<RequestAuthorization> authorizationConfig) {
System.out.println("Authorizing requests...");
authorizationConfig.accept(new RequestAuthorization());
return this;
}
}
public static class LoggingConfig {
public LoggingConfig level(String level) {
System.out.println("Logging level set to: " + level);
return this;
}
}
public static class RequestAuthorization {
public RequestAuthorization permitAll() {
System.out.println("All requests are permitted");
return this;
}
public RequestAuthorization authenticated() {
System.out.println("Authenticated requests only");
return this;
}
}
}
```
```java
Server server = new ServerConfig()
.http(http -> http.enableHttp2().port(8080)) // Настройка HTTP с использованием лямбда-выражения
.security(security -> security
.enableTLS() // Настройка безопасности
.authorizeRequests(auth -> auth.authenticated())) // Смена контекста внутри лямбды
.logging(log -> log.level("INFO")) // Настройка логирования с помощью лямбда
.build(); // Финальная сборка сервера
```
### Step building
Позволяет организовать процесс создания объектов или выполнения операций через строго упорядоченные шаги. Хотя этот подход часто используется в [[../../../../garden/ru/dev/architecture/Builder Pattern|Builder Pattern]], он применим и в других контекстах, например, при вызове API, конфигурации сложных процессов, построении запросов и даже в рабочих процессах (workflow).
**Основные концепции**
- **Упорядоченные шаги**. Процесс выполнения операции или создания объекта разделен на несколько этапов (шагов), которые должны выполняться в определённой последовательности. Каждый шаг может представлять собой настройку, изменение состояния или выполнение отдельной операции.
- **Контроль обязательных шагов**. Пошаговая сборка гарантирует, что определенные важные шаги не будут пропущены. Это особенно полезно для процессов, где важно соблюдение последовательности действий или конфигурации обязательных параметров.
**Примеры применения пошаговой сборки**
**Построение SQL-запросов**
```java
public interface SelectStep {
FromStep select(String... columns);
}
public interface FromStep {
WhereStep from(String table);
}
public interface WhereStep {
OrderByStep where(String condition);
}
public interface OrderByStep {
BuildStep orderBy(String column);
}
public interface BuildStep {
String build();
}
public class SqlQueryBuilder implements SelectStep, FromStep, WhereStep, OrderByStep, BuildStep {
private String query;
@Override
public FromStep select(String... columns) {
query = "SELECT " + String.join(", ", columns);
return this;
}
@Override
public WhereStep from(String table) {
query += " FROM " + table;
return this;
}
@Override
public OrderByStep where(String condition) {
query += " WHERE " + condition;
return this;
}
@Override
public BuildStep orderBy(String column) {
query += " ORDER BY " + column;
return this;
}
@Override
public String build() {
return query;
}
public static SelectStep start() {
return new SqlQueryBuilder();
}
}
```
### Самообобщение
Когда мы используем наследование для создания подклассов, возникает проблема, что методы Fluent API могут возвращать не подкласс, а базовый класс, разрывая цепочку вызовов. **Самообобщение** решает эту проблему, позволяя методам возвращать правильный тип подкласса.
**Пример проблемы без самообобщения**. Допустим, у нас есть базовый класс с цепочкой методов, и мы хотим унаследовать этот класс.
```java
class BaseBuilder {
public BaseBuilder setName(String name) {
System.out.println("Name set to: " + name);
return this;
}
}
class AdvancedBuilder extends BaseBuilder {
public AdvancedBuilder setFeature(String feature) {
System.out.println("Feature set to: " + feature);
return this;
}
}
public class Main {
public static void main(String[] args) {
AdvancedBuilder builder = new AdvancedBuilder();
builder.setName("MyObject")
.setFeature("AdvancedFeature"); // Ошибка: возвращается BaseBuilder
}
}
```
В этом примере `setName()` возвращает тип `BaseBuilder`, поэтому попытка вызвать `setFeature()` на результат этого вызова приведет к ошибке. Метод `setFeature()` будет недоступен.
**Решение с использованием самообобщения (Self-type Generics)**. Мы можем решить эту проблему, используя самообобщение с помощью обобщений (generics). Это позволит методам возвращать **самый специфичный тип**.
```java
class BaseBuilder<T extends BaseBuilder<T>> {
public T setName(String name) {
System.out.println("Name set to: " + name);
return (T) this; // Возвращаем текущий объект с типом T
}
}
class AdvancedBuilder extends BaseBuilder<AdvancedBuilder> {
public AdvancedBuilder setFeature(String feature) {
System.out.println("Feature set to: " + feature);
return this; // Возвращаем текущий объект с типом AdvancedBuilder
}
}
public class Main {
public static void main(String[] args) {
AdvancedBuilder builder = new AdvancedBuilder();
builder.setName("MyObject")
.setFeature("AdvancedFeature"); // Теперь работает правильно
}
}
```
***
## Мета информация
**Область**:: [[../../../../garden/ru/meta/zero/00 Java разработка|00 Java разработка]]
**Родитель**::
**Источник**::
**Создана**:: [[2024-10-04]]
**Автор**::
### Дополнительные материалы
-
### Дочерние заметки
<!-- QueryToSerialize: LIST FROM [[]] WHERE contains(Родитель, this.file.link) or contains(parents, this.file.link) -->

View File

@ -25,7 +25,7 @@ linked:
<!-- QueryToSerialize: LIST FROM [[]] WHERE contains(Родитель, this.file.link) or contains(parents, this.file.link) -->
<!-- SerializedQuery: LIST FROM [[]] WHERE contains(Родитель, this.file.link) or contains(parents, this.file.link) -->
- [[Inversion of Control]]
- [[Паттерн программирования]]
- [[Один клиент — один поток]]
- [[Много клиентов — один поток]]
- [[Один клиент — один поток]]
- [[Паттерн проектирования]]
<!-- SerializedQuery END -->

View File

@ -11,6 +11,8 @@ parents:
linked:
---
- [[Dependency Injection]]
- [[Порождающий паттерн проектирования]]
- [[Builder Pattern|Builder Pattern]]
***
## Мета информация
@ -25,4 +27,5 @@ linked:
<!-- QueryToSerialize: LIST FROM [[]] WHERE contains(Родитель, this.file.link) or contains(parents, this.file.link) -->
<!-- SerializedQuery: LIST FROM [[]] WHERE contains(Родитель, this.file.link) or contains(parents, this.file.link) -->
- [[Dependency Injection]]
- [[Порождающий паттерн проектирования]]
<!-- SerializedQuery END -->

View File

@ -0,0 +1,26 @@
---
aliases:
tags:
- maturity/🌱
date: 2024-10-04
zero-link:
parents:
linked:
---
- [[Builder Pattern]]
***
## Мета информация
**Область**:: [[../../meta/zero/00 Разработка|00 Разработка]]
**Родитель**:: [[Паттерн проектирования]]
**Источник**::
**Создана**:: [[2024-10-04]]
**Автор**::
### Дополнительные материалы
-
### Дочерние заметки
<!-- QueryToSerialize: LIST FROM [[]] WHERE contains(Родитель, this.file.link) or contains(parents, this.file.link) -->
<!-- SerializedQuery: LIST FROM [[]] WHERE contains(Родитель, this.file.link) or contains(parents, this.file.link) -->
- [[Builder Pattern]]
<!-- SerializedQuery END -->

View File

@ -6,7 +6,7 @@ zero-link:
---
- [[../../dev/architecture/Архитектурный слой|Архитектурный слой]]
- [[../../dev/architecture/Архитектурная концепция|Архитектурная концепция]]
- [[../../dev/architecture/Паттерн программирования|Паттерн программирования]]
- [[../../dev/architecture/Паттерн проектирования|Паттерн проектирования]]
Архитектурные ошибки и проблемы:
- [[../../dev/architecture/Протекание абстракций|Протекание абстракций]]

View File

@ -94,7 +94,7 @@ NIO посылает события Netty их преобразовывает в
-
***
## Мета информация
**Область**::
**Область**:: [[../00 Источники|00 Источники]]
**Родитель**::
**Источник**::
**Создана**:: [[2024-10-02]]