Files
Lab8-Prog/docs stuff/java_answers.md
T
2026-06-12 17:42:16 +03:00

1728 lines
65 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Ответы на экзаменационные вопросы по Java / Kotlin
> Все примеры кода взяты из реального учебного проекта — клиент-серверного приложения с GUI на JavaFX.
---
## Часть I: GUI, JavaFX, Локализация
---
## 1. Компоненты пользовательского интерфейса. Иерархия компонентов
### Что такое компонент?
**Компонент** (Component) — любой визуальный элемент GUI: кнопка, текстовое поле, таблица, окно. В Java/Kotlin GUI компоненты организованы в **иерархию наследования**.
### Иерархия в AWT/Swing
```
java.lang.Object
└── java.awt.Component ← базовый визуальный элемент
├── java.awt.Container ← может содержать другие компоненты
│ ├── java.awt.Window
│ │ ├── java.awt.Frame
│ │ └── java.awt.Dialog
│ └── javax.swing.JComponent ← база всех Swing-виджетов
│ ├── JPanel
│ ├── JButton
│ ├── JLabel
│ ├── JTextField
│ ├── JTable
│ └── ... (все J-компоненты)
└── java.awt.Button (AWT)
```
### Иерархия в JavaFX (используется в проекте)
В JavaFX нет AWT/Swing — другое дерево:
```
javafx.scene.Node ← базовый узел сцены
├── javafx.scene.Parent ← может содержать дочерние узлы
│ ├── javafx.scene.layout.Pane ← менеджер компоновки
│ │ ├── VBox, HBox, GridPane, StackPane, BorderPane...
│ └── javafx.scene.control.Control ← интерактивные элементы
│ ├── Button, Label, TextField, PasswordField
│ ├── ComboBox, CheckBox, ToggleButton
│ ├── TableView, MenuButton, MenuItem
│ └── ToolBar, SplitPane, Separator
└── javafx.scene.canvas.Canvas ← поверхность для рисования
```
### Концепция Stage → Scene → Node
```
Stage (окно ОС)
└── Scene (контент окна)
└── Node (корневой элемент — обычно Pane)
└── ... (дерево виджетов)
```
### Из проекта: Main.kt
```kotlin
class App : Application() {
override fun start(stage: Stage) {
AuthView(stage) { // Stage = окно ОС
val mainStage = Stage()
MainView(mainStage).show()
}.show()
}
}
```
### Из проекта: MainView.kt — дерево компонентов
```kotlin
private fun buildUI(): BorderPane {
val root = BorderPane() // корневой контейнер
root.top = buildToolbar() // ToolBar сверху
root.center = buildContent() // SplitPane посередине
root.bottom = buildCommandBar() // HBox снизу
return root
}
```
`SplitPane` внутри делится на `TableView` (список объектов) и `Canvas` (визуализация).
---
## 2. Базовые классы Component, Container, JComponent
### `java.awt.Component`
Самый базовый класс AWT. Описывает **любой визуальный элемент**: имеет позицию (x, y), размер, цвет, шрифт. Ключевые методы:
| Метод | Описание |
|---|---|
| `setVisible(boolean)` | показать / скрыть |
| `setEnabled(boolean)` | включить / выключить |
| `setSize(int, int)` | задать размер |
| `repaint()` | перерисовать |
| `addMouseListener(...)` | подписаться на мышь |
### `java.awt.Container`
Расширяет `Component`. Может **содержать другие компоненты** (дочерние). Ключевые методы:
| Метод | Описание |
|---|---|
| `add(Component)` | добавить дочерний компонент |
| `remove(Component)` | удалить |
| `setLayout(LayoutManager)` | назначить менеджер компоновки |
| `getComponents()` | получить массив дочерних |
### `javax.swing.JComponent`
Расширяет `Container`. База **всех Swing-виджетов** (кнопок, полей, таблиц). Добавляет:
- поддержку **двойной буферизации** (нет мерцания)
- механизм **бордеров** (`setBorder(...)`)
- поддержку **ToolTip** (`setToolTipText(...)`)
- систему **рисования через paintComponent(Graphics g)**
**Пример переопределения рисования в Swing:**
```java
class MyPanel extends JPanel {
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(Color.RED);
g.fillOval(10, 10, 100, 100);
}
}
```
### JavaFX-аналог в проекте: Canvas
В JavaFX рисование — через `Canvas` + `GraphicsContext`:
```kotlin
// CollectionCanvas.kt
class CollectionCanvas : Canvas() {
private val gc: GraphicsContext = graphicsContext2D // аналог Graphics
private fun drawItem(item: HumanBeingProperty, glowIntensity: Double) {
val px = worldToScreenX(item.coordinateX.value.toDouble())
val py = worldToScreenY(item.coordinateY.value.toDouble())
val radius = mapImpactSpeed(item.impactSpeed.value)
val color = ownerColor(item.ownerId.value)
gc.fill = color
gc.fillOval(px - radius, py - radius, radius * 2, radius * 2)
gc.stroke = Color.WHITE
gc.strokeOval(px - radius, py - radius, radius * 2, radius * 2)
}
}
```
---
## 3. Менеджеры компоновки
**Менеджер компоновки (Layout Manager)** — объект, который автоматически расставляет компоненты внутри контейнера. Без него пришлось бы вручную задавать координаты каждого элемента.
### Основные менеджеры в Swing (AWT)
| Менеджер | Поведение |
|---|---|
| `FlowLayout` | компоненты идут слева направо, как слова в тексте |
| `BorderLayout` | 5 зон: NORTH, SOUTH, EAST, WEST, CENTER |
| `GridLayout` | сетка одинаковых ячеек N×M |
| `GridBagLayout` | гибкая сетка с весами и отступами |
| `BoxLayout` | горизонтальная или вертикальная линия |
| `CardLayout` | стопка карточек, показывается одна |
| `null` | абсолютное позиционирование (setBounds) |
**Пример Swing:**
```java
JPanel panel = new JPanel(new BorderLayout());
panel.add(new JButton("Сверху"), BorderLayout.NORTH);
panel.add(new JTextField(), BorderLayout.CENTER);
panel.add(new JButton("Снизу"), BorderLayout.SOUTH);
```
### Менеджеры компоновки в JavaFX (используются в проекте)
JavaFX встраивает менеджер прямо в класс контейнера:
| Класс | Аналог Swing | Описание |
|---|---|---|
| `VBox` | BoxLayout вертикальный | дети вертикально |
| `HBox` | BoxLayout горизонтальный | дети горизонтально |
| `GridPane` | GridBagLayout | гибкая сетка по строкам/столбцам |
| `BorderPane` | BorderLayout | 5 зон: top, bottom, left, right, center |
| `StackPane` | LayeredPane | компоненты стопкой (z-порядок) |
| `SplitPane` | JSplitPane | делится на части с разделителем |
| `ToolBar` | JToolBar | панель инструментов |
### Из проекта: EditDialog.kt — GridPane для формы
```kotlin
private fun buildUI(): VBox {
val form = GridPane()
form.hgap = 8.0 // горизонтальный отступ между ячейками
form.vgap = 8.0 // вертикальный отступ
form.padding = Insets(16.0)
form.alignment = Pos.CENTER_LEFT
var row = 0
form.addRow(row, Label().also { AppLocale.bindLabel(it, "table.name") }, nameField)
row++
form.addRow(row, Label().also { AppLocale.bindLabel(it, "table.coordinates.x") }, coordXField)
row++
// ...
return VBox(8.0, form, buttonBar) // VBox: форма + кнопки
}
```
### Из проекта: MainView.kt — BorderPane + SplitPane
```kotlin
private fun buildUI(): BorderPane {
val root = BorderPane()
root.top = buildToolbar() // ToolBar: вверху
root.center = buildContent() // SplitPane: по центру
root.bottom = buildCommandBar() // HBox: внизу
return root
}
private fun buildContent(): Node {
val splitPane = SplitPane()
splitPane.orientation = Orientation.HORIZONTAL
splitPane.items.addAll(buildTablePane(), buildCanvasPane())
splitPane.setDividerPositions(0.55) // 55% левой части
return splitPane
}
```
---
## 4. Модель обработки событий. Класс-слушатель и класс-событие
### Суть модели событий
В Java GUI используется **паттерн Observer (наблюдатель)**:
1. **Источник события** (компонент) — кнопка, поле и т.д.
2. **Событие** (Event) — объект, описывающий что произошло.
3. **Слушатель** (Listener) — объект с методом-обработчиком, подписанный на источник.
### Класс-событие
Каждое событие — объект класса, наследующего `java.util.EventObject`:
```
java.util.EventObject
└── java.awt.AWTEvent
├── java.awt.event.ActionEvent (нажатие кнопки)
├── java.awt.event.MouseEvent (мышь)
├── java.awt.event.KeyEvent (клавиатура)
├── java.awt.event.WindowEvent (окно)
└── java.awt.event.ItemEvent (выбор в списке)
```
Событие содержит: источник (`getSource()`), тип события, дополнительные данные (координаты мыши, код клавиши и т.д.).
### Класс-слушатель (интерфейс Listener)
Каждый тип события — интерфейс с методом-обработчиком:
| Интерфейс | Методы | Событие |
|---|---|---|
| `ActionListener` | `actionPerformed(ActionEvent)` | клик кнопки |
| `MouseListener` | `mouseClicked/Pressed/Released/Entered/Exited` | мышь |
| `KeyListener` | `keyPressed/Released/Typed` | клавиши |
| `WindowListener` | `windowOpened/Closed/...` | окно |
| `ChangeListener` | `stateChanged(ChangeEvent)` | изменение состояния |
**Пример в Swing:**
```java
JButton btn = new JButton("Нажми");
btn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Кнопка нажата! Источник: " + e.getSource());
}
});
```
### JavaFX: EventHandler вместо Listener
В JavaFX используется generic-интерфейс `EventHandler<T extends Event>`:
```kotlin
button.setOnAction { event: ActionEvent ->
println("Нажата кнопка")
}
```
Иерархия событий JavaFX:
```
javafx.event.Event
├── javafx.event.ActionEvent (кнопки, меню)
├── javafx.scene.input.MouseEvent
├── javafx.scene.input.KeyEvent
└── javafx.collections.ListChangeListener.Change (коллекции)
```
### Из проекта: обработка кликов мыши в CollectionCanvas.kt
```kotlin
init {
// Добавляем слушатель событий мыши
addEventHandler(MouseEvent.MOUSE_CLICKED, this::onClick)
}
private fun onClick(event: MouseEvent) {
val mx = event.x // координаты клика
val my = event.y
val items = Session.collection.toList()
for (item in items) {
val px = worldToScreenX(item.coordinateX.value.toDouble())
val py = worldToScreenY(item.coordinateY.value.toDouble())
val r = mapImpactSpeed(item.impactSpeed.value)
val dx = mx - px
val dy = my - py
if (dx * dx + dy * dy <= (r + hitRadius) * (r + hitRadius)) {
showItemInfo(item) // показать информацию об объекте
return
}
}
}
```
### Из проекта: слушатель изменения коллекции в MainController.kt
```kotlin
// ListChangeListener — JavaFX-аналог Observer на ObservableList
private val collectionListener = ListChangeListener<HumanBeingProperty> { _ ->
reapplyFiltersAndSort() // пересчитать при любом изменении
}
fun start() {
Session.collection.addListener(collectionListener) // подписка
}
fun stop() {
Session.collection.removeListener(collectionListener) // отписка
}
```
### Из проекта: слушатель изменения текста в MainView.kt
```kotlin
val filterField = TextField()
filterField.textProperty().addListener { _, _, _ ->
// _ = observable, _ = oldValue, _ = newValue
applyFilters()
}
```
---
## 5. Технология JavaFX. Особенности архитектуры, отличия от AWT/Swing
### Ключевые отличия
| Характеристика | AWT | Swing | JavaFX |
|---|---|---|---|
| Год появления | 1995 | 1997 | 2008 (JavaFX 2.0 в 2011) |
| Рендеринг | нативные виджеты ОС | Java 2D (программный) | Hardware-ускорение (Prism) |
| UI-описание | Java-код | Java-код | FXML (XML) + Java/Kotlin |
| CSS-стилизация | нет | ограничено | полная поддержка CSS |
| Анимация | нет | Timer | Animation API (Timeline, AnimationTimer) |
| Связывание данных | нет | нет | Property/Binding API |
| Потокобезопасность | EDT (Event Dispatch Thread) | EDT | JavaFX Application Thread |
| 3D-поддержка | нет | нет | да (Scene3D) |
| WebView | нет | нет | да (встроенный браузер) |
### Архитектура JavaFX
```
Application
├── Stage (окно)
│ └── Scene
│ └── Scene Graph (дерево Node)
├── JavaFX Application Thread (поток UI)
└── Prism (рендеринг через GPU: OpenGL / DirectX / Metal)
```
**Scene Graph** — это граф объектов (`Node`). Каждый узел имеет трансформации, эффекты, CSS-класс. Это основное отличие от AWT/Swing.
### Property и Binding
JavaFX вводит **реактивное связывание**: `Property<T>` — обёртка над значением, которая уведомляет слушателей при изменении.
```kotlin
// HumanBeingProperty.kt — каждое поле объекта — Property
val name = SimpleStringProperty(humanBeing.name)
val realHero = SimpleBooleanProperty(humanBeing.realHero)
val coordinateX = SimpleObjectProperty<Long>(humanBeing.coordinates.x)
```
Это позволяет привязывать (bind) поля UI прямо к данным:
```kotlin
// TableColumn автоматически отображает значение Property
tableCol.cellValueFactory = Callback { features ->
SimpleObjectProperty(extractor(features.value))
}
```
### JavaFX Application Thread
Весь UI живёт в одном потоке — **JavaFX Application Thread**. Если нужно обновить UI из фонового потока — используют `Platform.runLater { }`.
### Из проекта: Platform.runLater в AuthController.kt
```kotlin
fun authenticate(..., onResult: (Boolean, String) -> Unit) {
scope.launch { // фоновый поток
val response = provider.auth(...) // сетевой запрос
// Обновление UI — только через Platform.runLater!
Platform.runLater { onResult(true, "") }
}
}
```
### Из проекта: AnimationTimer в CollectionCanvas.kt
```kotlin
private val renderTimer: AnimationTimer = object : AnimationTimer() {
override fun handle(now: Long) {
animationTime = now / 1_000_000_000.0 // время в секундах
render() // перерисовка каждый кадр (~60 FPS)
}
}
```
`AnimationTimer` — JavaFX-механизм игрового цикла, вызывающий `handle()` перед каждым кадром. Аналогов в Swing нет.
### Из проекта: ObservableList в Session.kt
```kotlin
object Session {
// ObservableList — специальный список JavaFX:
// уведомляет слушателей при добавлении/удалении элементов
val collection: ObservableList<HumanBeingProperty> = FXCollections.observableArrayList()
}
```
`TableView` напрямую привязан к `ObservableList` — при изменении коллекции таблица обновляется автоматически.
---
## 6. Интернационализация. Локализация. Хранение локализованных ресурсов
### Интернационализация (i18n) vs Локализация (l10n)
- **Интернационализация (i18n)** — проектирование программы так, чтобы её можно было адаптировать под разные языки/регионы *без* изменения кода.
- **Локализация (l10n)** — конкретная адаптация под конкретный язык/регион (перевод, форматы дат и чисел).
### Стандартный Java-подход: ResourceBundle + .properties файлы
**Структура файлов:**
```
src/main/resources/
messages.properties ← fallback (английский)
messages_ru.properties ← русский
messages_de.properties ← немецкий
messages_fr_FR.properties ← французский (Франция)
```
**Содержимое messages_ru.properties:**
```properties
auth.title=Вход / Регистрация
button.add=Добавить
error.not.owner=Вы не владелец этого объекта
```
**Использование в коде:**
```java
Locale locale = new Locale("ru");
ResourceBundle bundle = ResourceBundle.getBundle("messages", locale);
String title = bundle.getString("auth.title"); // → "Вход / Регистрация"
```
### Объект `Locale`
`java.util.Locale` описывает язык+регион:
```java
Locale ruLocale = new Locale("ru"); // русский
Locale enAuLocale = new Locale("en", "AU"); // английский (Австралия)
Locale.getDefault() // локаль системы
```
### Подход в проекте: Map-based AppLocale.kt
В проекте локализация реализована через **HashMap** прямо в коде (без .properties файлов). Строки хранятся в `Map<Locale, Map<String, String>>`:
```kotlin
object AppLocale {
val currentLocale: ObjectProperty<Locale> = SimpleObjectProperty(Locale("ru"))
private val strings: Map<Locale, Map<String, String>> = mapOf(
Locale("ru") to mapOf(
"auth.title" to "Вход / Регистрация",
"button.add" to "Добавить",
"error.not.owner" to "Вы не владелец этого объекта",
// ...
),
Locale("nl") to mapOf(
"auth.title" to "Inloggen / Registreren",
"button.add" to "Toevoegen",
"error.not.owner" to "U bent niet de eigenaar van dit object",
),
Locale("hu") to mapOf(
"auth.title" to "Bejelentkezés / Regisztráció",
// ...
),
Locale("en", "AU") to mapOf(
"auth.title" to "Sign In / Sign Up",
"auth.error.empty" to "Fill in all fields, mate",
// ...
),
)
fun get(key: String): String =
strings[currentLocale.value]?.get(key) ?: key
}
```
### Динамическое переключение языка
Ключевая особенность — **реактивная смена языка в реальном времени** через `Property` + `bind`:
```kotlin
// Привязка текста кнопки к ключу локализации
fun bind(property: Property<String>, key: String) {
property.value = get(key)
currentLocale.addListener { _, _, _ ->
property.value = get(key) // автообновление при смене языка
}
}
fun bindLabel(labeled: Labeled, key: String) {
bind(labeled.textProperty(), key)
}
```
В `MainView.kt` при смене локали:
```kotlin
localeCombo.setOnAction {
val selected = localeCombo.selectionModel.selectedItem ?: return@setOnAction
AppLocale.currentLocale.set(selected.locale) // → все bind()-ы обновятся
updateLocale()
}
```
---
## 7. Форматирование локализованных данных. NumberFormat, DateFormat, MessageFormat, ChoiceFormat
### `java.text.NumberFormat`
Форматирует числа в зависимости от локали:
```java
NumberFormat nf = NumberFormat.getInstance(Locale.GERMANY);
nf.format(1234567.89); // → "1.234.567,89" (точка — разделитель тысяч, запятая — дробная)
NumberFormat nf2 = NumberFormat.getInstance(Locale.US);
nf2.format(1234567.89); // → "1,234,567.89"
// Валюта
NumberFormat cf = NumberFormat.getCurrencyInstance(new Locale("ru"));
cf.format(1500.00); // → "1 500,00 ₽"
// Проценты
NumberFormat pf = NumberFormat.getPercentInstance(Locale.US);
pf.format(0.75); // → "75%"
```
### `java.text.DateFormat` / `SimpleDateFormat`
Форматирует даты по локали:
```java
DateFormat df = DateFormat.getDateInstance(DateFormat.FULL, new Locale("ru"));
df.format(new Date()); // → "пятница, 12 июня 2026 г."
DateFormat dfEn = DateFormat.getDateInstance(DateFormat.SHORT, Locale.US);
dfEn.format(new Date()); // → "6/12/26"
// SimpleDateFormat — задаём паттерн вручную
SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy HH:mm", new Locale("ru"));
sdf.format(new Date()); // → "12.06.2026 16:00"
```
Паттерны: `d` = день, `M` = месяц, `y` = год, `H` = час (24ч), `m` = минута, `s` = секунда.
### `java.text.MessageFormat`
Форматирует строки с параметрами (шаблоны с `{0}`, `{1}`):
```java
String template = "Пользователь {0} добавил {1} объект(ов)";
MessageFormat mf = new MessageFormat(template, new Locale("ru"));
String result = mf.format(new Object[]{"Alice", 3});
// → "Пользователь Alice добавил 3 объект(ов)"
```
Можно задавать форматирование прямо в шаблоне:
```java
// {1,number,#.##} — число с 2 знаками после запятой
String tpl = "Скорость: {0,number,#.##} м/с";
MessageFormat.format(tpl, 123.456789); // → "Скорость: 123.46 м/с"
// {1,date,short} — короткая дата
String tpl2 = "Создан: {0,date,short}";
MessageFormat.format(tpl2, new Date()); // → "Создан: 12.06.26"
```
### `java.text.ChoiceFormat`
Выбирает строку в зависимости от числового значения (для правильного склонения):
```java
// Паттерн: "0#нет файлов|1#один файл|2#два файла|5#пять файлов"
// Числа задают границы диапазонов
double[] limits = {0, 1, 2, 5};
String[] formats = {"нет файлов", "один файл", "два файла", "много файлов"};
ChoiceFormat cf = new ChoiceFormat(limits, formats);
cf.format(0); // → "нет файлов"
cf.format(1); // → "один файл"
cf.format(3); // → "два файла"
cf.format(10); // → "много файлов"
```
`ChoiceFormat` часто используют внутри `MessageFormat`:
```java
String pattern = "У вас {0,choice,0#нет сообщений|1#одно сообщение|2<{0} сообщения}";
MessageFormat.format(pattern, 2); // → "У вас 2 сообщения"
```
### В проекте: хранение локализованных дат
В `HumanBeingProperty.kt` поле даты — `LocalDate`. При отображении в таблице оно конвертируется в строку через `toString()`. В продакшн-коде лучше использовать `DateTimeFormatter`:
```kotlin
val formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
.withLocale(AppLocale.currentLocale.value)
creationDate.value.format(formatter) // → "12.06.26" для ru, "6/12/26" для en_US
```
---
## Часть II: Многопоточность и JDBC
---
## 1. Многопоточность. Класс Thread, интерфейс Runnable, модификатор synchronized
### Поток (Thread)
Поток — независимый поток выполнения кода в рамках одного JVM-процесса. Все потоки делят общую кучу (heap), но имеют свой стек.
### Создание потока: два способа
**Способ 1: наследование от Thread**
```java
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Выполняется в потоке: " + Thread.currentThread().getName());
}
}
MyThread t = new MyThread();
t.start(); // запуск (не run()!)
```
**Способ 2: реализация Runnable (предпочтительно)**
```java
Runnable task = () -> System.out.println("Работаю в потоке");
Thread t = new Thread(task);
t.start();
```
`Runnable` — функциональный интерфейс с одним методом `void run()`. Предпочтителен, т.к. не ограничивает наследование.
### Жизненный цикл потока
```
NEW → RUNNABLE → (BLOCKED/WAITING/TIMED_WAITING) → TERMINATED
```
### Модификатор `synchronized`
Проблема: при одновременном доступе нескольких потоков к общим данным возникают **состояния гонки** (race conditions).
`synchronized` гарантирует, что только один поток в момент времени выполняет защищённый блок. Работает через **монитор объекта** (встроенный мьютекс).
```java
// 1. Синхронизированный метод (монитор = this)
public synchronized void increment() {
counter++;
}
// 2. Синхронизированный блок (явное указание монитора)
public void increment() {
synchronized (this) {
counter++;
}
}
// 3. Статический synchronized (монитор = Class объект)
public static synchronized void staticMethod() { ... }
```
### Из проекта: CollectionManager.kt — синхронизация коллекции
```kotlin
class CollectionManager {
// Collections.synchronizedSortedSet — потокобезопасная обёртка
private val collection: MutableSet<HumanBeing> =
Collections.synchronizedSortedSet(TreeSet())
fun update(id: UUID, updated: HumanBeing): Boolean {
synchronized(collection) { // явный synchronized-блок
val old = getById(id) ?: return false
collection.remove(old)
val replaced = updated.copy(id = old.id, creationDate = old.creationDate)
return collection.add(replaced)
}
}
fun getAll(): TreeSet<HumanBeing> {
synchronized(collection) {
return TreeSet(collection) // снимок коллекции
}
}
}
```
### Из проекта: CacheInvalidator.kt — фоновый поток
```kotlin
class CacheInvalidator(
private val dataSource: DataSource,
private val onInvalidated: () -> Unit
) {
@Volatile
private var running = false
private var thread: Thread? = null
fun start() {
running = true
thread = Thread { // создание потока через Runnable (лямбда)
while (running) {
// слушаем PostgreSQL NOTIFY
Thread.sleep(500) // пауза 500мс между проверками
}
}.also { it.isDaemon = true } // демон-поток: не мешает остановке JVM
.apply {
name = "pg-notify-listener"
start() // запуск
}
}
fun stop() {
running = false
thread?.interrupt() // прерываем сон через interrupt()
}
}
```
---
## 2. Методы wait(), notify(), Lock и Condition
### wait() и notify() из класса Object
Эти методы работают **только внутри synchronized-блока**. Они реализуют схему "ожидание условия":
- `wait()` — текущий поток **освобождает монитор** и засыпает, пока другой поток не вызовет `notify()`.
- `notify()` — будит **один** ожидающий поток.
- `notifyAll()` — будит **все** ожидающие потоки.
**Классический пример: очередь производитель-потребитель**
```java
class Buffer {
private final Queue<Integer> queue = new LinkedList<>();
private final int capacity = 10;
public synchronized void produce(int item) throws InterruptedException {
while (queue.size() == capacity) {
wait(); // ждём, пока не освободится место
}
queue.add(item);
notify(); // будим потребителя
}
public synchronized int consume() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // ждём, пока не появится элемент
}
int item = queue.poll();
notify(); // будим производителя
return item;
}
}
```
### Интерфейс `Lock` (java.util.concurrent.locks)
`Lock` — более гибкая альтернатива `synchronized`. Основная реализация — `ReentrantLock`.
| Возможность | synchronized | ReentrantLock |
|---|---|---|
| Явное получение/освобождение | нет | `lock()` / `unlock()` |
| Попытка без блокировки | нет | `tryLock()` |
| Попытка с таймаутом | нет | `tryLock(time, unit)` |
| Прерываемое ожидание | нет | `lockInterruptibly()` |
| Справедливость (FIFO) | нет | `new ReentrantLock(true)` |
```java
Lock lock = new ReentrantLock();
try {
lock.lock(); // захват монитора
// критическая секция
} finally {
lock.unlock(); // ВСЕГДА в finally!
}
// tryLock — не блокируется
if (lock.tryLock()) {
try { /* работа */ } finally { lock.unlock(); }
} else {
System.out.println("Ресурс занят");
}
```
### Интерфейс `Condition`
`Condition` — аналог `wait()/notify()` для `Lock`. Создаётся из объекта `Lock`:
```java
Lock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();
// Производитель:
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await(); // ждём "не полон"
}
queue.add(item);
notEmpty.signal(); // сигналим "не пуст"
} finally { lock.unlock(); }
// Потребитель:
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // ждём "не пуст"
}
int item = queue.poll();
notFull.signal(); // сигналим "не полон"
} finally { lock.unlock(); }
```
---
## 3. Классы-синхронизаторы из пакета java.util.concurrent
`java.util.concurrent` предоставляет готовые синхронизаторы для типовых сценариев.
### Основные синхронизаторы
| Класс | Назначение |
|---|---|
| `Semaphore` | ограничение числа одновременных доступов |
| `CountDownLatch` | ожидание выполнения N задач |
| `CyclicBarrier` | синхронизация N потоков в точке |
| `Exchanger` | обмен объектами между двумя потоками |
| `Phaser` | гибкий многофазный барьер |
### `Semaphore` — контролируем число одновременных доступов
```java
// Не более 3 потоков одновременно могут работать с ресурсом
Semaphore semaphore = new Semaphore(3);
void accessResource() throws InterruptedException {
semaphore.acquire(); // захватить "разрешение"
try {
// работа с ограниченным ресурсом
} finally {
semaphore.release(); // вернуть "разрешение"
}
}
```
### `CountDownLatch` — ждём завершения N задач
```java
CountDownLatch latch = new CountDownLatch(3); // ждём 3 задачи
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// выполняем задачу
latch.countDown(); // уменьшаем счётчик
}).start();
}
latch.await(); // главный поток ждёт, пока счётчик не = 0
System.out.println("Все 3 задачи выполнены!");
```
### `CyclicBarrier` — все потоки ждут друг друга в точке
```java
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("Все потоки достигли барьера!");
});
Runnable task = () -> {
// часть работы...
barrier.await(); // ждём остальных
// продолжаем...
};
```
### В проекте: готовые пулы потоков как синхронизированное решение
В `NetworkManager.kt` сервера используются пулы потоков вместо ручной синхронизации:
```kotlin
private val readerPool = Executors.newFixedThreadPool(4) // 4 потока-читателя
private val processingPool = ForkJoinPool( // пул для обработки
Runtime.getRuntime().availableProcessors()
)
private val senderPool = ForkJoinPool( // пул для отправки
Runtime.getRuntime().availableProcessors()
)
```
---
## 4. Модификатор volatile. Атомарные типы данных и операции
### Проблема видимости (visibility)
Каждый поток может кешировать значения переменных в **регистрах CPU или L1/L2 кеше**. Изменение переменной одним потоком может быть невидимо другим.
### `volatile`
`volatile` гарантирует:
1. **Видимость**: запись всегда идёт в main memory, чтение — всегда из main memory (без кеша).
2. **Запрет переупорядочивания** (happens-before на запись/чтение).
```java
volatile boolean running = true; // без volatile второй поток может не увидеть false
// Поток 1:
running = false;
// Поток 2:
while (running) {
// без volatile этот цикл может никогда не закончиться!
}
```
**Ограничение**: `volatile` НЕ делает операцию `i++` атомарной, т.к. это три операции: read → increment → write.
### Из проекта: `@Volatile` в CacheInvalidator.kt
```kotlin
@Volatile // Kotlin-аналог volatile
private var running = false
fun start() {
running = true
thread = Thread {
while (running) { // читаем актуальное значение каждый раз
try {
// ...
Thread.sleep(500)
} catch (e: Exception) {
if (!running) break // проверяем после пробуждения
}
}
}
}
fun stop() {
running = false // запись видна другому потоку сразу
thread?.interrupt()
}
```
### Атомарные типы: `java.util.concurrent.atomic`
Атомарные типы позволяют делать составные операции (read-modify-write) **атомарно** без `synchronized`. Реализованы через CPU-инструкции **CAS (Compare-And-Swap)**.
| Класс | Описание |
|---|---|
| `AtomicInteger` | атомарный `int` |
| `AtomicLong` | атомарный `long` |
| `AtomicBoolean` | атомарный `boolean` |
| `AtomicReference<V>` | атомарная ссылка |
| `AtomicIntegerArray` | атомарный массив int |
```java
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // атомарный i++, возвращает новое
counter.getAndIncrement(); // атомарный i++, возвращает старое
counter.addAndGet(5); // атомарный i += 5
counter.compareAndSet(5, 10); // если == 5, то установить 10 (CAS)
```
### Из проекта: AtomicInteger в GatewayManager.kt
```kotlin
// Атомарный счётчик для round-robin балансировки нагрузки
private val roundRobinCounter = AtomicInteger(0)
private fun pickBackend(): BackendServer? {
val alive = backends.filter { it.isAlive }
if (alive.isEmpty()) return null
// getAndIncrement() — атомарно: read + increment + write
return alive[roundRobinCounter.getAndIncrement() % alive.size]
}
```
---
## 5. Коллекции из пакета java.util.concurrent
Обычные коллекции (`ArrayList`, `HashMap`, `TreeSet`) **не потокобезопасны**. `Collections.synchronizedXxx()` делает их потокобезопасными, но с глобальной блокировкой (плохая производительность).
`java.util.concurrent` предоставляет высокопроизводительные потокобезопасные коллекции.
### Основные коллекции
| Класс | Описание |
|---|---|
| `ConcurrentHashMap<K,V>` | HashMap с сегментной блокировкой (высокая пропускная способность) |
| `CopyOnWriteArrayList<T>` | список: запись = копирование, чтение без блокировки |
| `CopyOnWriteArraySet<T>` | то же, но Set |
| `ConcurrentLinkedQueue<T>` | неблокирующая очередь (CAS) |
| `ConcurrentLinkedDeque<T>` | неблокирующая двусторонняя очередь |
| `LinkedBlockingQueue<T>` | блокирующая очередь (производитель-потребитель) |
| `PriorityBlockingQueue<T>` | блокирующая очередь с приоритетом |
| `ArrayBlockingQueue<T>` | ограниченная блокирующая очередь |
### `ConcurrentHashMap`
```java
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1);
map.putIfAbsent("key", 2); // атомарная операция
map.computeIfAbsent("key", k -> 42); // атомарное вычисление
```
### `CopyOnWriteArrayList`
При каждой записи создаётся **копия массива**. Читатели не блокируются. Оптимален для редких записей и частых чтений.
```java
List<String> list = new CopyOnWriteArrayList<>();
list.add("a"); // копирует массив, добавляет, заменяет ссылку
// Итерация безопасна даже при параллельных записях:
for (String s : list) { /* ... */ }
```
### `LinkedBlockingQueue` — производитель-потребитель
```java
BlockingQueue<Task> queue = new LinkedBlockingQueue<>(100);
// Производитель:
queue.put(task); // блокируется если очередь полна
// Потребитель:
Task t = queue.take(); // блокируется если очередь пуста
```
### Из проекта: CopyOnWriteArrayList в GatewayManager.kt
```kotlin
// CopyOnWriteArrayList: список серверов читается часто (при каждом запросе),
// пишется редко (при обнаружении/удалении серверов)
private val backends = CopyOnWriteArrayList<BackendServer>()
// Безопасное чтение без блокировки:
private fun pickBackend(): BackendServer? {
val alive = backends.filter { it.isAlive } // итерация без блокировки
// ...
}
// Запись (создаёт копию):
backends.addAllAbsent(newBackends)
backends.removeAll { ... }
```
---
## 6. Интерфейсы Executor, ExecutorService, Callable, Future
### Проблема прямого создания потоков
Создавать `new Thread()` для каждой задачи — дорого. Лучше **переиспользовать потоки** через пулы.
### `Executor`
Самый простой интерфейс: принимает `Runnable` на выполнение:
```java
interface Executor {
void execute(Runnable command);
}
```
### `ExecutorService`
Расширяет `Executor`. Добавляет управление жизненным циклом и возможность получать результаты:
```java
ExecutorService es = Executors.newFixedThreadPool(4);
// submit Runnable (нет результата):
es.submit(() -> System.out.println("задача"));
// submit Callable (с результатом):
Future<Integer> future = es.submit(() -> 2 + 2);
int result = future.get(); // блокирует до получения результата
// Завершение:
es.shutdown(); // ждёт завершения задач
es.shutdownNow(); // прерывает запущенные задачи
```
### `Callable<V>`
Как `Runnable`, но **возвращает результат** и **может бросить checked-исключения**:
```java
interface Callable<V> {
V call() throws Exception;
}
Callable<String> task = () -> {
// долгая операция
return "результат";
};
```
### `Future<V>`
Представляет **будущий результат** асинхронной задачи:
```java
Future<String> future = executorService.submit(task);
future.isDone(); // завершена ли задача?
future.isCancelled(); // отменена?
future.cancel(true); // попытка отмены (true = прервать поток)
future.get(); // блокирует до готовности
future.get(5, TimeUnit.SECONDS); // с таймаутом
```
### Фабричные методы `Executors`
```java
Executors.newFixedThreadPool(4) // пул из 4 потоков
Executors.newCachedThreadPool() // динамический пул (рождаются по требованию)
Executors.newSingleThreadExecutor() // один поток, очередь задач
Executors.newScheduledThreadPool(2) // для отложенных/периодических задач
```
### Из проекта: ExecutorService в NetworkManager.kt
```kotlin
// Сервер использует несколько пулов для изоляции задач:
private val readerPool = Executors.newFixedThreadPool(4) // читают сокет
private val processingPool = ForkJoinPool(...) // обрабатывают запросы
private val senderPool = ForkJoinPool(...) // отправляют ответы
fun start() {
repeat(4) {
readerPool.submit(::readLoop) // запуск 4 задач чтения
}
}
fun stop() {
socket.close()
readerPool.shutdownNow() // прерываем чтение
processingPool.shutdown() // ждём обработки
senderPool.shutdown() // ждём отправки
}
```
### Из проекта: ScheduledExecutorService в GatewayManager.kt
```kotlin
private val healthPool = Executors.newScheduledThreadPool(2)
private fun startHealthChecks() {
// Запускать проверку каждые 5 секунд
healthPool.scheduleAtFixedRate(
{ backends.forEach { probeServer(it) } },
healthCheckIntervalMs, // начальная задержка
healthCheckIntervalMs, // интервал
TimeUnit.MILLISECONDS
)
}
```
---
## 7. Пулы потоков
**Пул потоков** — набор заранее созданных потоков, которые **переиспользуются** для выполнения задач.
### Зачем нужны пулы
- Создание потока — дорогая операция (~1мс, ~1МБ памяти на стек).
- Бесконтрольное создание потоков перегрузит систему.
- Пул: потоки создаются один раз, задачи ставятся в очередь.
### Типы пулов
| Метод `Executors` | Поведение |
|---|---|
| `newFixedThreadPool(n)` | ровно n потоков; очередь задач не ограничена |
| `newCachedThreadPool()` | потоки создаются при необходимости, удаляются через 60с простоя |
| `newSingleThreadExecutor()` | 1 поток, задачи выполняются последовательно |
| `newScheduledThreadPool(n)` | n потоков, поддержка задержек и периодических задач |
| `ForkJoinPool(n)` | work-stealing: поток может "украсть" задачу у другого |
### `ForkJoinPool` и рекурсивные задачи
Оптимален для **рекурсивных задач** (divide & conquer). Использует **work-stealing**: незанятый поток берёт задачу из очереди другого потока.
```java
// RecursiveTask — возвращает результат
class SumTask extends RecursiveTask<Long> {
@Override
protected Long compute() {
if (размер_маленький) {
return посчитать_напрямую();
}
SumTask left = new SumTask(левая_половина);
SumTask right = new SumTask(правая_половина);
left.fork(); // запустить левую асинхронно
return right.compute() + left.join(); // правую здесь + ждать левую
}
}
ForkJoinPool pool = new ForkJoinPool();
long result = pool.invoke(new SumTask(data));
```
### Из проекта: ForkJoinPool для обработки запросов (NetworkManager.kt)
```kotlin
// RecursiveTask для обработки запроса
inner class ProcessRequestTask(
private val json: String,
private val address: InetAddress,
private val clientPort: Int
) : RecursiveTask<Unit>() {
override fun compute() {
val response = invoker.execute(request) // выполнить команду
senderPool.submit(SendResponseTask(response, address, clientPort)) // отправить
}
}
// RecursiveAction для отправки (нет возвращаемого значения)
inner class SendResponseTask(...) : RecursiveAction() {
override fun compute() {
val bytes = AppJson.encodeToString(...).toByteArray()
val packet = DatagramPacket(bytes, bytes.size, address, clientPort)
synchronized(socket) { socket.send(packet) }
}
}
```
Аналогично устроен `GatewayManager.kt` с `ProxyTask` и `SendTask`.
---
## 8. JDBC. Порядок взаимодействия с базой данных. DriverManager. Connection
### Что такое JDBC
**JDBC (Java Database Connectivity)** — стандартный API для работы с реляционными БД из Java. Предоставляет единый интерфейс независимо от типа БД (PostgreSQL, MySQL, SQLite...).
### Порядок взаимодействия с БД
```
1. Загрузить драйвер
2. Получить соединение (Connection) через DriverManager
3. Создать Statement или PreparedStatement
4. Выполнить запрос
5. Обработать ResultSet
6. Закрыть ресурсы (ResultSet → Statement → Connection)
```
### `DriverManager` — получение соединения
```java
// Строка подключения: jdbc:<субпротокол>://<хост>:<порт>/<база>
String url = "jdbc:postgresql://localhost:5432/mydb";
String user = "postgres";
String pass = "secret";
Connection conn = DriverManager.getConnection(url, user, pass);
```
`DriverManager` автоматически находит нужный драйвер (если он в classpath). При подключении к PostgreSQL нужен артефакт `org.postgresql:postgresql`.
### `Connection` — соединение с БД
`java.sql.Connection` — главный объект, представляющий открытое соединение.
```java
// Управление транзакциями:
conn.setAutoCommit(false); // отключить авто-коммит
// ... выполнить запросы ...
conn.commit(); // подтвердить транзакцию
conn.rollback(); // откатить при ошибке
// Закрытие:
conn.close(); // ОБЯЗАТЕЛЬНО закрывать!
```
Лучшая практика — try-with-resources:
```java
try (Connection conn = DriverManager.getConnection(url, user, pass)) {
// работа с conn
} // автоматически закрывается
```
### Connection Pool: HikariCP в проекте
В проекте используется **HikariCP** — пул соединений. Соединения дорого создавать, поэтому их переиспользуют:
```kotlin
// DatabaseManager.kt
fun start() {
val config = HikariConfig().apply {
jdbcUrl = System.getenv("DB_URL") ?: "jdbc:postgresql://localhost:5432/prog7_db"
driverClassName = "org.postgresql.Driver"
username = System.getenv("DB_USER") ?: error("DB_USER not set")
password = System.getenv("DB_PASS") ?: error("DB_PASS not set")
maximumPoolSize = 11 // максимум 11 соединений в пуле
isAutoCommit = false
transactionIsolation = "TRANSACTION_REPEATABLE_READ"
}
dataSource = HikariDataSource(config)
Database.connect(dataSource) // Exposed ORM использует этот пул
}
```
---
## 9. Интерфейсы Statement, PreparedStatement, ResultSet, RowSet
### `Statement` — простой запрос
Выполняет SQL-запрос без параметров. Уязвим к **SQL-инъекциям** — никогда не используйте для запросов с пользовательским вводом!
```java
Statement stmt = conn.createStatement();
// DML (изменение данных):
int affected = stmt.executeUpdate("DELETE FROM users WHERE id = 1");
// DQL (выборка):
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
// DDL (CREATE, DROP, ALTER):
stmt.execute("CREATE TABLE test (id INT)");
stmt.close();
```
### `PreparedStatement` — параметризованный запрос
Компилируется один раз, выполняется много раз с разными параметрами. Защищает от SQL-инъекций.
```java
PreparedStatement pstmt = conn.prepareStatement(
"INSERT INTO users (username, password) VALUES (?, ?)"
);
pstmt.setString(1, "alice"); // параметр 1
pstmt.setString(2, "hashed"); // параметр 2
pstmt.executeUpdate(); // выполнить
// Переиспользование:
pstmt.setString(1, "bob");
pstmt.setString(2, "hash2");
pstmt.executeUpdate();
pstmt.close();
```
**Batch-выполнение:**
```java
PreparedStatement pstmt = conn.prepareStatement("INSERT INTO t VALUES (?)");
for (int i = 0; i < 1000; i++) {
pstmt.setInt(1, i);
pstmt.addBatch(); // добавить в пакет
}
pstmt.executeBatch(); // выполнить весь пакет одним запросом к БД
```
### `ResultSet` — результат запроса
Курсор для итерации по строкам результата:
```java
PreparedStatement pstmt = conn.prepareStatement(
"SELECT id, name, speed FROM humanBeing WHERE owner_id = ?"
);
pstmt.setString(1, ownerId.toString());
ResultSet rs = pstmt.executeQuery();
while (rs.next()) { // перейти к следующей строке
UUID id = UUID.fromString(rs.getString("id"));
String name = rs.getString("name");
double speed = rs.getDouble("speed");
System.out.println(id + ": " + name + " speed=" + speed);
}
rs.close();
```
Методы получения значений: `getString(col)`, `getInt(col)`, `getDouble(col)`, `getBoolean(col)`, `getDate(col)`, `getObject(col)`.
### `RowSet` — автономный ResultSet
`RowSet` — расширение `ResultSet`, которое:
- может существовать **без открытого соединения** (`CachedRowSet`)
- поддерживает **сериализацию** и **передачу по сети**
- сам управляет соединением
```java
// CachedRowSet — загружает данные в память, соединение закрывается
CachedRowSet crs = RowSetProvider.newFactory().createCachedRowSet();
crs.setUrl("jdbc:postgresql://localhost:5432/db");
crs.setUsername("user");
crs.setPassword("pass");
crs.setCommand("SELECT * FROM users");
crs.execute(); // загрузить данные (соединение закроется)
while (crs.next()) {
System.out.println(crs.getString("username"));
}
// Соединение к БД уже закрыто, данные в памяти
```
### В проекте: Exposed ORM поверх JDBC (Repository.kt)
Проект использует **Exposed** (Kotlin ORM) как обёртку над JDBC. Exposed генерирует `PreparedStatement` автоматически:
```kotlin
// Repository.kt — под капотом это PreparedStatement
fun login(username: String, password: String): Pair<String, String>? {
var id: String? = null
transaction {
// Exposed генерирует: SELECT id FROM "user" WHERE username=? AND password=?
id = UserTable.select(UserTable.id)
.where { (UserTable.username eq username) and (UserTable.password eq password) }
.singleOrNull()
?.get(UserTable.id)?.toString()
}
// ...
}
fun write(item: HumanBeing): UUID? {
transaction {
// INSERT INTO coordinates (x, y) VALUES (?, ?)
val cords = CoordinatesTable.insertAndGetId {
it[x] = item.coordinates.x
it[y] = item.coordinates.y
}
// INSERT INTO humanBeing (...) VALUES (...)
humanId = HumanBeingTable.insertAndGetId {
it[owner_id] = item.ownerId
it[name] = item.name
it[coordinates] = cords
// ...
}.value
exec("NOTIFY cache_changed") // PostgreSQL NOTIFY
}
}
```
---
## 10. Шаблоны проектирования
**Шаблон проектирования (Design Pattern)** — проверенное решение типовой проблемы в проектировании ПО. Делятся на три группы:
- **Порождающие** (Creational) — создание объектов
- **Структурные** (Structural) — организация классов
- **Поведенческие** (Behavioral) — взаимодействие объектов
---
### Команда (Command) — поведенческий
**Проблема**: нужно инкапсулировать запрос как объект, поддерживать отмену операций, логировать команды.
**Решение**: каждое действие — отдельный класс, реализующий интерфейс `Command`.
```kotlin
// Интерфейс команды (Command.kt)
interface Command {
val name: String
val description: String
val args: List<CommandArgType>
val type: CommandType
fun execute(args: List<String>, humanBeing: HumanBeing? = null, userId: String = ""): Response
}
```
```kotlin
// Конкретная команда (AddCommand.kt)
class AddCommand(
private val manager: CollectionManager,
private val repository: Repository
) : Command {
override val name = "add"
override val description = "Добавить объект в коллекцию"
override val type = CommandType.MODEL
override val args = listOf(CommandArgType.NONE)
override fun execute(args: List<String>, humanBeing: HumanBeing?, userId: String): Response {
val id = repository.write(humanBeing!!) ?: return Response(false, "Ошибка БД")
val saved = humanBeing.copy(id = id)
manager.add(saved)
return Response(true, "Объект добавлен: $id")
}
}
```
```kotlin
// Инвокер (CommandInvoker.kt) — хранит и запускает команды
class CommandInvoker(private val jwtManager: JWTManager) {
private val commands = mutableMapOf<String, Command>()
private val history = ArrayDeque<String>()
fun register(command: Command) {
commands[command.name] = command
}
fun execute(request: Request): Response {
val command = commands[request.commandName]
?: return Response(false, "Неизвестная команда")
addToHistory(request.commandName)
return command.execute(request.args, request.humanBeing, userId)
}
}
```
Сервер регистрирует все команды в `Main.kt`:
```kotlin
fun registerServerCommands(invoker: CommandInvoker, manager: CollectionManager, repository: Repository) {
listOf(
InfoCommand(manager),
ShowCommand(manager),
AddCommand(manager, repository),
UpdateCommand(manager, repository),
RemoveByIdCommand(manager, repository),
// ...
).forEach { invoker.register(it) }
}
```
---
### Observer (Наблюдатель) — поведенческий
**Проблема**: объект должен уведомлять другие объекты об изменениях, не зная, кто именно слушает.
В JavaFX это реализовано через `ObservableList` и `ListChangeListener`:
```kotlin
// Session.kt — Observable (субъект)
val collection: ObservableList<HumanBeingProperty> = FXCollections.observableArrayList()
// CollectionCanvas.kt — Observer (наблюдатель)
Session.collection.addListener { _: ListChangeListener.Change<out HumanBeingProperty>? ->
scheduleRender() // перерисовать при любом изменении коллекции
}
// MainController.kt — ещё один Observer
private val collectionListener = ListChangeListener<HumanBeingProperty> { _ ->
reapplyFiltersAndSort()
}
fun start() {
Session.collection.addListener(collectionListener)
}
```
---
### Singleton — порождающий
**Проблема**: объект должен существовать в единственном экземпляре.
В проекте это `Session` и `AppLocale` через Kotlin-объекты (`object`):
```kotlin
// Session.kt — единственный экземпляр сессии
object Session {
var client: ClientProvider? = null
var username: String = ""
val collection: ObservableList<HumanBeingProperty> = FXCollections.observableArrayList()
}
// AppLocale.kt — единственный менеджер локализации
object AppLocale {
val currentLocale: ObjectProperty<Locale> = SimpleObjectProperty(Locale("ru"))
fun get(key: String): String = strings[currentLocale.value]?.get(key) ?: key
}
```
---
### MVC (Model-View-Controller) — архитектурный
Проект использует паттерн **MVC**:
| Слой | Классы в проекте | Описание |
|---|---|---|
| **Model** | `HumanBeing`, `Session`, `CollectionManager` | данные и бизнес-логика |
| **View** | `MainView`, `AuthView`, `EditDialog`, `CollectionCanvas` | UI, отображение |
| **Controller** | `MainController`, `AuthController` | связь View и Model |
```kotlin
// View (MainView.kt) создаёт Controller и вызывает его методы:
class MainView(private val stage: Stage) {
private val controller = MainController()
private fun onAdd() {
val dialog = EditDialog(stage.scene.window)
val humanBeing = dialog.showAndWait() ?: return
controller.executeCommand(name = "add", humanBeing = humanBeing) { success, message ->
if (success) controller.requestRefresh() else showError(message)
}
}
}
// Controller (MainController.kt) координирует логику:
class MainController {
fun executeCommand(name: String, ..., callback: (Boolean, String) -> Unit) {
scope.launch { // фоновый поток
val response = Session.client?.execute(name, args, humanBeing)
Platform.runLater { callback(success, message) } // обновление View
}
}
}
```
---
### Strategy (Стратегия) — поведенческий
**Проблема**: нужно иметь несколько алгоритмов и переключаться между ними в runtime.
В проекте реализована через `Comparator` при сортировке:
```kotlin
// MainView.kt — динамическое создание стратегии сортировки
private fun toggleSort(key: String) {
val comparator: Comparator<HumanBeingProperty> = Comparator { a, b ->
val va = extractSortValue(a, key)
val vb = extractSortValue(b, key)
if (sortAscending) va.compareTo(vb) else vb.compareTo(va)
}
controller.updateComparators(listOf(comparator))
}
// StreamUtils.kt — применение стратегии
fun applyFiltersAndSort(..., comparators: List<Comparator<HumanBeingProperty>>): ObservableList<...> {
if (comparators.isNotEmpty()) {
val combined = comparators.reduce { a, b -> a.thenComparing(b) }
stream = stream.sorted(combined) // применяем выбранную стратегию
}
}
```
---
### Proxy — структурный
**Проблема**: нужен объект-посредник для контроля доступа к другому объекту.
В проекте — `GatewayManager` выступает прокси-сервером:
```kotlin
// GatewayManager.kt — принимает запросы клиентов и перенаправляет на backend
inner class ProxyTask(...) : RecursiveTask<Unit>() {
override fun compute() {
val backend = pickBackend() // выбрать живой сервер (round-robin)
// Прозрачно пересылаем запрос на backend:
DatagramSocket().use { bs ->
bs.send(DatagramPacket(data, data.size, InetAddress.getByName(backend.host), backend.port))
bs.receive(respPacket)
senderPool.submit(SendTask(respPacket.data.copyOf(respPacket.length), clientAddress, clientPort))
}
}
}
```