diff --git a/dev/architecture/Builder Pattern.md b/dev/architecture/Builder Pattern.md new file mode 100644 index 00000000..cfe67741 --- /dev/null +++ b/dev/architecture/Builder Pattern.md @@ -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]] +**Автор**:: +### Дополнительные материалы +- + +### Дочерние заметки + + diff --git a/dev/architecture/Dependency Injection.md b/dev/architecture/Dependency Injection.md index 04fd285e..d6e37358 100644 --- a/dev/architecture/Dependency Injection.md +++ b/dev/architecture/Dependency Injection.md @@ -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]] diff --git a/dev/architecture/Fluent API.md b/dev/architecture/Fluent API.md new file mode 100644 index 00000000..e656a019 --- /dev/null +++ b/dev/architecture/Fluent API.md @@ -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 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) { + System.out.println("Entering HTTP configuration context"); + httpConfig.accept(new HttpConfig()); + return this; // Возвращаем тот же объект для дальнейшей конфигурации + } + + public ServerConfig security(Consumer securityConfig) { + System.out.println("Entering Security configuration context"); + securityConfig.accept(new SecurityConfig()); + return this; // Переключение на контекст безопасности + } + + public ServerConfig logging(Consumer 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 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> { + public T setName(String name) { + System.out.println("Name set to: " + name); + return (T) this; // Возвращаем текущий объект с типом T + } +} + +class AdvancedBuilder extends BaseBuilder { + 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]] +**Автор**:: +### Дополнительные материалы +- + +### Дочерние заметки + + diff --git a/dev/architecture/Архитектурная концепция.md b/dev/architecture/Архитектурная концепция.md index 7666b69f..8551a6e9 100644 --- a/dev/architecture/Архитектурная концепция.md +++ b/dev/architecture/Архитектурная концепция.md @@ -25,7 +25,7 @@ linked: - [[Inversion of Control]] -- [[Паттерн программирования]] -- [[Один клиент — один поток]] - [[Много клиентов — один поток]] +- [[Один клиент — один поток]] +- [[Паттерн проектирования]] diff --git a/dev/architecture/Паттерн программирования.md b/dev/architecture/Паттерн проектирования.md similarity index 83% rename from dev/architecture/Паттерн программирования.md rename to dev/architecture/Паттерн проектирования.md index 940471e4..02d1b838 100644 --- a/dev/architecture/Паттерн программирования.md +++ b/dev/architecture/Паттерн проектирования.md @@ -11,6 +11,8 @@ parents: linked: --- - [[Dependency Injection]] +- [[Порождающий паттерн проектирования]] + - [[Builder Pattern|Builder Pattern]] *** ## Мета информация @@ -25,4 +27,5 @@ linked: - [[Dependency Injection]] +- [[Порождающий паттерн проектирования]] diff --git a/dev/architecture/Порождающий паттерн проектирования.md b/dev/architecture/Порождающий паттерн проектирования.md new file mode 100644 index 00000000..2ab4fe3f --- /dev/null +++ b/dev/architecture/Порождающий паттерн проектирования.md @@ -0,0 +1,26 @@ +--- +aliases: +tags: + - maturity/🌱 +date: 2024-10-04 +zero-link: +parents: +linked: +--- +- [[Builder Pattern]] +*** +## Мета информация +**Область**:: [[../../meta/zero/00 Разработка|00 Разработка]] +**Родитель**:: [[Паттерн проектирования]] +**Источник**:: +**Создана**:: [[2024-10-04]] +**Автор**:: +### Дополнительные материалы +- + +### Дочерние заметки + + +- [[Builder Pattern]] + + diff --git a/meta/zero/00 Архитектура ПО.md b/meta/zero/00 Архитектура ПО.md index 54f8d911..ed239942 100644 --- a/meta/zero/00 Архитектура ПО.md +++ b/meta/zero/00 Архитектура ПО.md @@ -6,7 +6,7 @@ zero-link: --- - [[../../dev/architecture/Архитектурный слой|Архитектурный слой]] - [[../../dev/architecture/Архитектурная концепция|Архитектурная концепция]] - - [[../../dev/architecture/Паттерн программирования|Паттерн программирования]] + - [[../../dev/architecture/Паттерн проектирования|Паттерн проектирования]] Архитектурные ошибки и проблемы: - [[../../dev/architecture/Протекание абстракций|Протекание абстракций]] diff --git a/source/lecture/Доклад. Могут ли Virtual threads заменить Webflux.md b/source/lecture/Доклад. Могут ли Virtual threads заменить Webflux.md index c63d4b4e..9846d109 100644 --- a/source/lecture/Доклад. Могут ли Virtual threads заменить Webflux.md +++ b/source/lecture/Доклад. Могут ли Virtual threads заменить Webflux.md @@ -94,7 +94,7 @@ NIO посылает события Netty их преобразовывает в - *** ## Мета информация -**Область**:: +**Область**:: [[../00 Источники|00 Источники]] **Родитель**:: **Источник**:: **Создана**:: [[2024-10-02]]