diff --git a/.gitignore b/.gitignore
index 28cb4f5..4a7cd1b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,43 +1,40 @@
-### IntelliJ IDEA ###
-# Игнорируем всё содержимое .idea — это папка настроек среды разработки
-# Оставляем только нужные файлы, чтобы IDE могла корректно открыть проект
-.idea/
-!.idea/misc.xml # Уровень JDK, базовые настройки проекта
-!.idea/modules.xml # Структура модулей (если нет Maven/Gradle)
-!.idea/libraries/ # Подключённые библиотеки, например JUnit
-!.idea/vcs.xml # Настройки системы контроля версий (Git)
+# ===== Build output / compiled =====
+out/
+bin/
+build/
+target/
+*.class
+
+# ===== App data created by FileBackedTaskManager =====
+tasks.csv
-# Игнорируем файлы модулей IntelliJ (.iml) — не нужны в репозитории
+# ===== IntelliJ IDEA =====
+.idea/
+!.idea/misc.xml
+!.idea/modules.xml
+!.idea/vcs.xml
+# !.idea/libraries/ # надо включать только осознанно
*.iml
-### Build output ###
-# Исключаем папки, которые содержат скомпилированные классы и артефакты
-out/ # Папка вывода сборки IntelliJ
-bin/ # Папка вывода Eclipse/ручной сборки
-target/ # Папка сборки Maven (если появится)
+# ===== VS Code =====
+.vscode/
-### OS ###
-# Системные файлы macOS, не должны попадать в репозиторий
+# ===== OS junk =====
.DS_Store
+Thumbs.db
-### VS Code ###
-# Конфигурации Visual Studio Code (если кто-то откроет проект там)
-.vscode/
-
-### Eclipse ###
-# Игнорируем все файлы и папки, связанные с Eclipse IDE
-.apt_generated # Автоматически сгенерированные исходники
-.classpath # Файл конфигурации путей классов
-.factorypath # Конфигурация аннотаций
-.project # Основной файл проекта Eclipse
-.settings # Папка с настройками проекта
-.springBeans # Конфиги Spring Beans (если используется)
-.sts4-cache # Кэш Spring Tool Suite (STS)
+# ===== Eclipse =====
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
-### NetBeans ###
-# Всё, что создаёт NetBeans IDE
-nbproject/private/ # Личные настройки проекта
-nbbuild/ # Папка сборки NetBeans
-dist/ # Артефакты сборки (JAR и т.д.)
-nbdist/ # Расширенная папка вывода
-.nb-gradle/ # Кэш Gradle от NetBeans
+# ===== NetBeans =====
+nbproject/private/
+nbbuild/
+dist/
+nbdist/
+.nb-gradle/
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 7bc07ec..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,10 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
-# Editor-based HTTP Client requests
-/httpRequests/
-# Environment-dependent path to Maven home directory
-/mavenHomeManager.xml
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
diff --git a/.idea/libraries/junit_jupiter.xml b/.idea/libraries/junit_jupiter.xml
deleted file mode 100644
index 0725eb8..0000000
--- a/.idea/libraries/junit_jupiter.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 89ee753..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index e54d3ba..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/java-sprint4-hw.iml b/java-sprint4-hw.iml
deleted file mode 100644
index 547dd47..0000000
--- a/java-sprint4-hw.iml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Main.java b/src/Main.java
index 771028c..f830167 100644
--- a/src/Main.java
+++ b/src/Main.java
@@ -2,24 +2,37 @@
import manager.TaskManager;
import model.*;
-
-// Демонстрация базовой работы с менеджером задач:
-//добавление задач, получение и история просмотров.
-
+// Демонстрация базовой работы с менеджером задач
public class Main {
public static void main(String[] args) {
TaskManager manager = Managers.getDefault();
// === Добавление задач ===
- int id1 = manager.addNewTask(new Task("Задача 1", "Описание задачи", Status.NEW));
- int id2 = manager.addNewEpic(new Epic("Эпик 1", "Описание эпика"));
- int id3 = manager.addNewSubtask(new Subtask("Подзадача 1", "Описание подзадачи", id2));
+ int id1 = manager.addNewTask(new Task("Задача 1", "Описание задачи", Status.NEW));
+ int epicId = manager.addNewEpic(new Epic("Эпик 1", "Описание эпика"));
+ int subId = manager.addNewSubtask(new Subtask("Подзадача 1", "Описание подзадачи", epicId));
+ manager.addNewSubtask(new Subtask("Подзадача 2", "Ещё одна", epicId));
+
+ // === Используем возвращаемые списки (убираем жёлтые лампы) TODO: это просто для себя подчеркиваю ===
+ int tasksCount = manager.getTasks().size();
+ int epicsCount = manager.getEpics().size();
+ int subtasksCount = manager.getSubtasks().size();
+ int epicSubCount = manager.getEpicSubtasks(epicId).size();
+ System.out.printf(
+ "Всего: tasks=%d, epics=%d, subtasks=%d; у эпика %d подзадач=%d%n",
+ tasksCount, epicsCount, subtasksCount, epicId, epicSubCount
+ );
+
+ // === Получение задач (для истории просмотров) — без пустых if ===
+ boolean viewedTask1 = manager.getTask(id1) != null;
+ boolean viewedEpic = manager.getEpic(epicId) != null;
+ boolean viewedSub = manager.getSubtask(subId) != null;
+ boolean viewedTask2 = manager.getTask(id1) != null;
+
+ // просто используем значения, чтобы инспекция была довольна
+ System.out.printf("Просмотры: t1=%b, epic=%b, sub=%b, t1-again=%b%n",
+ viewedTask1, viewedEpic, viewedSub, viewedTask2);
- // === Получение задач (для истории просмотров) ===
- manager.getTask(id1);
- manager.getEpic(id2);
- manager.getSubtask(id3);
- manager.getTask(id1); // повторное обращение к задаче
// === Вывод истории просмотров ===
System.out.println("=== История просмотров ===");
@@ -34,14 +47,12 @@ public static void main(String[] args) {
}
}
- // понятный тип задачи
private static String getTypeName(Task task) {
if (task instanceof Epic) return "Эпик";
if (task instanceof Subtask) return "Подзадача";
return "Задача";
}
- // Перевод статуса на русский
private static String getStatusName(Status status) {
return switch (status) {
case NEW -> "Новая";
diff --git a/src/manager/FileBackedTaskManager.java b/src/manager/FileBackedTaskManager.java
new file mode 100644
index 0000000..f6c23d1
--- /dev/null
+++ b/src/manager/FileBackedTaskManager.java
@@ -0,0 +1,232 @@
+package manager;
+
+import model.*;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Менеджер с автоматическим сохранением в CSV-файл.
+ * Наследуем InMemoryTaskManager и добавляем автосохранение.
+ */
+public class FileBackedTaskManager extends InMemoryTaskManager {
+
+ private final File file;
+
+ public FileBackedTaskManager(File file) {
+ super();
+ this.file = file;
+ }
+
+ /* ───────────── фабрика ───────────── */
+
+ public static FileBackedTaskManager loadFromFile(File file) {
+ FileBackedTaskManager m = new FileBackedTaskManager(file);
+ m.restore();
+ return m;
+ }
+
+ /* ───────────── сохранение ───────────── */
+
+ /** Сохраняет все задачи в CSV: id,type,name,status,description,epic */
+ private void save() {
+ try (BufferedWriter w = Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8)) {
+ w.write("id,type,name,status,description,epic");
+ w.newLine();
+
+ // порядок не критичен, но читается приятнее
+ for (Task t : getTasks()) {
+ w.write(t.toCsvRow());
+ w.newLine();
+ }
+ for (Epic e : getEpics()) {
+ w.write(e.toCsvRow());
+ w.newLine();
+ }
+ for (Subtask s : getSubtasks()) {
+ w.write(s.toCsvRow());
+ w.newLine();
+ }
+ } catch (IOException ex) {
+ throw new ManagerSaveException("Не удалось сохранить файл", ex);
+ }
+ }
+
+ /* ───────────── восстановление ───────────── */
+
+ /**
+ * Читает CSV и восстанавливает состояние.
+ * ВАЖНО: не перебиваем зафиксированные в файле ID.
+ * Поэтому используем прямые put*-методы из базового класса и выставляем nextId.
+ */
+ private void restore() {
+ if (!file.exists()) {
+ return;
+ }
+
+ List epics = new ArrayList<>();
+ List tasks = new ArrayList<>();
+ List subtasks = new ArrayList<>();
+
+ try (BufferedReader r = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) {
+ String header = r.readLine(); // заголовок
+ if (header == null) {
+ return;
+ }
+ String line;
+ while ((line = r.readLine()) != null) {
+ if (line.isBlank()) {
+ continue;
+ }
+ Task t = fromCsv(line);
+ switch (t.getType()) {
+ case EPIC -> epics.add((Epic) t);
+ case TASK -> tasks.add(t);
+ case SUBTASK -> subtasks.add((Subtask) t);
+ }
+ }
+ } catch (IOException ex) {
+ throw new ManagerSaveException("Не удалось прочитать файл", ex);
+ }
+
+ // Важно: сначала эпики, затем задачи, затем подзадачи
+ for (Epic e : epics) {
+ super.putEpicPreserveId(e);
+ }
+ for (Task t : tasks) {
+ super.putTaskPreserveId(t);
+ }
+ for (Subtask s : subtasks) {
+ super.putSubtaskPreserveId(s);
+ }
+ // ---> СДВИГАЕМ nextId TODO:так же для себя делал,убрал второстепенные замечания!
+ int maxId = 0;
+ for (Task t : tasks) maxId = Math.max(maxId, t.getId());
+ for (Epic e : epics) maxId = Math.max(maxId, e.getId());
+ for (Subtask s : subtasks) maxId = Math.max(maxId, s.getId());
+
+ super.setNextIdAfterRestore(maxId + 1);
+ }
+
+
+ /* ───────────── CSV утилиты ───────────── */
+
+ private static Task fromCsv(String csv) {
+ String[] p = csv.split(",", -1);
+
+ int id = Integer.parseInt(p[0]);
+ TaskType type = TaskType.valueOf(p[1]);
+ String name = p[2];
+ Status status = Status.valueOf(p[3]);
+ String description = p[4];
+
+ switch (type) {
+ case TASK: {
+ Task t = new Task(name, description, status);
+ t.setId(id);
+ return t;
+ }
+ case EPIC: {
+ Epic e = new Epic(name, description);
+ e.setId(id);
+ e.setStatus(status);
+ return e;
+ }
+ case SUBTASK: {
+ int epicId = Integer.parseInt(p[5]);
+ Subtask s = new Subtask(name, description, epicId);
+ s.setId(id);
+ s.setStatus(status);
+ return s;
+ }
+ default:
+ throw new IllegalStateException("Неизвестный тип: " + type);
+ }
+ }
+
+ /* ───────────── переопределения с автоматическим сохранением ───────────── */
+
+ @Override
+ public int addNewTask(Task t) {
+ int id = super.addNewTask(t);
+ save();
+ return id;
+ }
+
+ @Override
+ public int addNewEpic(Epic e) {
+ int id = super.addNewEpic(e);
+ save();
+ return id;
+ }
+
+ @Override
+ public int addNewSubtask(Subtask s) {
+ int id = super.addNewSubtask(s);
+ save();
+ return id;
+ }
+
+ @Override
+ public void updateTask(Task t) {
+ super.updateTask(t);
+ save();
+ }
+
+ @Override
+ public void updateEpic(Epic e) {
+ super.updateEpic(e);
+ save();
+ }
+
+ @Override
+ public void updateSubtask(Subtask s) {
+ super.updateSubtask(s);
+ save();
+ }
+
+ @Override
+ public void removeTask(int id) {
+ super.removeTask(id);
+ save();
+ }
+
+ @Override
+ public void removeEpic(int id) {
+ super.removeEpic(id);
+ save();
+ }
+
+ @Override
+ public void removeSubtask(int id) {
+ super.removeSubtask(id);
+ save();
+ }
+
+ // Если в интерфейсе есть метод clear, раскомментировать. Это я себе на будущее!
+ /*TODO(clear): @Override
+ public void clear() {
+ super.clear();
+ save();
+ } */
+
+ /* ───────────── demo ───────────── */
+
+ public static void main(String[] args) {
+ FileBackedTaskManager m = new FileBackedTaskManager(new File("tasks.csv"));
+
+ Epic epic = new Epic("Спринт-7", "Файл-менеджер");
+ m.addNewEpic(epic);
+ m.addNewSubtask(new Subtask("save()", "реализовать", epic.getId()));
+ m.addNewTask(new Task("Читать ТЗ", "вникнуть", Status.IN_PROGRESS));
+
+ FileBackedTaskManager restored = FileBackedTaskManager.loadFromFile(new File("tasks.csv"));
+ System.out.println("♻ восстановлено задач: " + restored.getTasks().size());
+ }
+}
diff --git a/src/manager/HistoryManager.java b/src/manager/HistoryManager.java
index 084b9c5..c163693 100644
--- a/src/manager/HistoryManager.java
+++ b/src/manager/HistoryManager.java
@@ -1,9 +1,10 @@
package manager;
-//Интерфейс менеджера истории просмотров задач.
+
import model.Task;
import java.util.List;
public interface HistoryManager {
- void add(Task task);
- List getHistory();
-}
\ No newline at end of file
+ void add(Task task); // записать просмотр
+ void remove(int id); // удалить по id (нужно при удалении задач)
+ List getHistory();// вернуть историю в порядке просмотра
+}
diff --git a/src/manager/InMemoryHistoryManager.java b/src/manager/InMemoryHistoryManager.java
index d45efee..9f97a16 100644
--- a/src/manager/InMemoryHistoryManager.java
+++ b/src/manager/InMemoryHistoryManager.java
@@ -2,22 +2,99 @@
import model.Task;
import java.util.*;
-//Хранит максимум 10 последних задач. При превышении лимита
- // самая старая задача удаляется.
+
+/** HistoryManager на базе двойного связанного списка + HashMap */
public class InMemoryHistoryManager implements HistoryManager {
- private static final int MAX_HISTORY = 10;
- private final Deque history = new ArrayDeque<>();
+
+ /* ───── узел списка ───── */
+ private static class Node {
+ Task data;
+ Node prev;
+ Node next;
+
+ Node(Node prev, Task data, Node next) {
+ this.prev = prev;
+ this.data = data;
+ this.next = next;
+ }
+ }
+
+ /* ───── поля ───── */
+ private final Map index = new HashMap<>();
+ private Node head;
+ private Node tail;
+
+ /* ───── вспомогательные ───── */
+
+ /** Добавляем просмотр в хвост */
+ private void linkLast(Task task) {
+ Node oldTail = tail;
+ Node newNode = new Node(oldTail, task, null); // n → newNode
+ tail = newNode;
+
+ if (oldTail == null) {
+ head = newNode; // фигурные скобки
+ } else {
+ oldTail.next = newNode; // фигурные скобки
+ }
+ }
+
+ /** Удаляем произвольный узел */
+ private void removeNode(Node target) {
+ if (target == null) {
+ return;
+ }
+
+ Node prev = target.prev;
+ Node next = target.next;
+
+ if (prev != null) {
+ prev.next = next;
+ } else {
+ head = next; // фигурные скобки
+ }
+
+ if (next != null) {
+ next.prev = prev;
+ } else {
+ tail = prev; // фигурные скобки
+ }
+ }
+
+ /** Выгружаем историю списком */
+ private List getTasks() {
+ List list = new ArrayList<>();
+ for (Node current = head; current != null; current = current.next) {
+ list.add(current.data);
+ }
+ return list;
+ }
+
+ /* ───── HistoryManager API ───── */
@Override
public void add(Task task) {
- history.addLast(task);
- if (history.size() > MAX_HISTORY) {
- history.pollFirst();
+ if (task == null) {
+ return; // фигурные скобки
}
+
+ /* если id уже есть — убираем старый узел */
+ Node duplicate = index.remove(task.getId());
+ removeNode(duplicate);
+
+ /* вносим новый просмотр */
+ linkLast(task);
+ index.put(task.getId(), tail);
}
-//Возвращает список просмотренных задач (в порядке просмотра).
+
+ @Override
+ public void remove(int id) {
+ Node node = index.remove(id);
+ removeNode(node);
+ }
+
@Override
public List getHistory() {
- return new ArrayList<>(history);
+ return getTasks();
}
-}
\ No newline at end of file
+}
diff --git a/src/manager/InMemoryTaskManager.java b/src/manager/InMemoryTaskManager.java
index b299777..8dc000e 100644
--- a/src/manager/InMemoryTaskManager.java
+++ b/src/manager/InMemoryTaskManager.java
@@ -1,23 +1,30 @@
package manager;
import model.*;
+
import java.util.*;
-// InMemoryTaskManager — реализация интерфейса TaskManager,
-// хранящая задачи, эпики и подзадачи в оперативной памяти.
-// Поддерживает создание, обновление, удаление и получение задач всех типов,
-// а также отслеживает историю просмотров через HistoryManager.
+/**
+ * InMemoryTaskManager хранит задачи в памяти и ведет историю.
+ */
public class InMemoryTaskManager implements TaskManager {
- private final Map tasks = new HashMap<>();
- private final Map epics = new HashMap<>();
- private final Map subtasks = new HashMap<>();
+
+ /* ---------- хранилища ---------- */
+ protected final Map tasks = new HashMap<>();
+ protected final Map epics = new HashMap<>();
+ protected final Map subtasks = new HashMap<>();
+
+ /* ---------- история ---------- */
private final HistoryManager historyManager = Managers.getDefaultHistory();
- private int nextId = 1;
+
+ /* ---------- генератор ID ---------- */
+ protected int nextId = 1;
private int generateId() {
return nextId++;
}
+ /* ---------- создание ---------- */
@Override
public int addNewTask(Task task) {
task.setId(generateId());
@@ -38,13 +45,13 @@ public int addNewSubtask(Subtask subtask) {
if (epic == null) {
throw new IllegalArgumentException("Эпик не найден");
}
- int id = generateId();
- subtask.setId(id);
- subtasks.put(id, subtask);
- epic.addSubtaskId(id);
- return id;
+ subtask.setId(generateId());
+ subtasks.put(subtask.getId(), subtask);
+ epic.addSubtaskId(subtask.getId());
+ return subtask.getId();
}
+ /* ---------- обновление ---------- */
@Override
public void updateTask(Task task) {
if (tasks.containsKey(task.getId())) {
@@ -60,59 +67,72 @@ public void updateEpic(Epic epic) {
}
@Override
- public void updateSubtask(Subtask subtask) {
- if (subtasks.containsKey(subtask.getId())) {
- subtasks.put(subtask.getId(), subtask);
+ public void updateSubtask(Subtask s) {
+ if (subtasks.containsKey(s.getId())) {
+ subtasks.put(s.getId(), s);
}
}
+ /* ---------- удаление ---------- */
@Override
public void removeTask(int id) {
tasks.remove(id);
+ historyManager.remove(id);
}
@Override
public void removeEpic(int id) {
Epic epic = epics.remove(id);
if (epic != null) {
- for (int subId : epic.getSubtaskIds()) {
- subtasks.remove(subId);
+ for (int sid : epic.getSubtaskIds()) {
+ subtasks.remove(sid);
+ historyManager.remove(sid);
}
+ historyManager.remove(id);
}
}
@Override
public void removeSubtask(int id) {
- Subtask subtask = subtasks.remove(id);
- if (subtask != null) {
- Epic epic = epics.get(subtask.getEpicId());
+ Subtask s = subtasks.remove(id);
+ if (s != null) {
+ Epic epic = epics.get(s.getEpicId());
if (epic != null) {
epic.getSubtaskIds().remove((Integer) id);
}
}
+ historyManager.remove(id);
}
+ /* ---------- получение + история ---------- */
@Override
public Task getTask(int id) {
Task t = tasks.get(id);
- if (t != null) historyManager.add(t);
+ if (t != null) {
+ historyManager.add(t);
+ }
return t;
}
@Override
public Epic getEpic(int id) {
Epic e = epics.get(id);
- if (e != null) historyManager.add(e);
+ if (e != null) {
+ historyManager.add(e);
+ }
return e;
}
@Override
public Subtask getSubtask(int id) {
Subtask s = subtasks.get(id);
- if (s != null) historyManager.add(s);
+ if (s != null) {
+ historyManager.add(s);
+ }
return s;
}
+ /* ---------- списки ---------- */
@Override
public List getTasks() {
return new ArrayList<>(tasks.values());
@@ -135,7 +155,9 @@ public List getEpicSubtasks(int epicId) {
if (epic != null) {
for (int id : epic.getSubtaskIds()) {
Subtask s = subtasks.get(id);
- if (s != null) result.add(s);
+ if (s != null) {
+ result.add(s);
+ }
}
}
return result;
@@ -145,4 +167,38 @@ public List getEpicSubtasks(int epicId) {
public List getHistory() {
return historyManager.getHistory();
}
+
+ /* ---------- защищённые хуки для восстановления из файла ---------- */
+
+ /** Кладем задачу с уже заданным id (не трогаем историю, TODO: без увеличения nextId). */
+ protected void putTaskPreserveId(Task task) {
+ tasks.put(task.getId(), task);
+ bumpNextId(task.getId());
+ }
+
+ /** Кладем эпик с уже заданным id. */
+ protected void putEpicPreserveId(Epic epic) {
+ epics.put(epic.getId(), epic);
+ bumpNextId(epic.getId());
+ }
+
+ /** Кладем Subtask с уже заданным id и привязываем к эпику. */
+ protected void putSubtaskPreserveId(Subtask subtask) {
+ subtasks.put(subtask.getId(), subtask);
+ Epic epic = epics.get(subtask.getEpicId());
+ if (epic != null) {
+ epic.addSubtaskId(subtask.getId());
+ }
+ bumpNextId(subtask.getId());
+ }
+ private void bumpNextId(int usedId) {
+ if (usedId >= nextId) {
+ nextId = usedId + 1;
+ }
+ }
+
+ /** Вызывается после восстановления, чтобы новые id шли дальше. */
+ protected void setNextIdAfterRestore(int nextId) {
+ this.nextId = Math.max(this.nextId, nextId);
+ }
}
diff --git a/src/manager/ManagerSaveException.java b/src/manager/ManagerSaveException.java
new file mode 100644
index 0000000..2e38139
--- /dev/null
+++ b/src/manager/ManagerSaveException.java
@@ -0,0 +1,10 @@
+package manager;
+
+/**
+ * Наша обёртка над IOException, чтобы не менять сигнатуры интерфейса.
+ */
+public class ManagerSaveException extends RuntimeException {
+ public ManagerSaveException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
\ No newline at end of file
diff --git a/src/manager/TaskManager.java b/src/manager/TaskManager.java
index 6f31646..6c0c8c3 100644
--- a/src/manager/TaskManager.java
+++ b/src/manager/TaskManager.java
@@ -1,35 +1,99 @@
package manager;
-import model.*;
+import model.Epic;
+import model.Subtask;
+import model.Task;
+
import java.util.List;
-// Интерфейс менеджера задач.
-// Определяет базовые методы для управления обычными задачами, эпиками и подзадачами.
-// Также предоставляет методы для получения истории просмотров.
+
+/**
+ * Интерфейс менеджера задач.
+ * Определяет операции для создания, изменения, удаления и получения
+ * задач всех типов (Task, Epic, Subtask), а также получения истории просмотров.
+ */
public interface TaskManager {
- //Создание задач всех типов
+ /* ===================== Создание ===================== */
+
+ /**
+ * Добавить обычную задачу.
+ * @param task задача
+ * @return присвоенный идентификатор
+ */
int addNewTask(Task task);
+
+ /**
+ * Добавить эпик.
+ * @param epic эпик
+ * @return присвоенный идентификатор
+ */
int addNewEpic(Epic epic);
+
+ /**
+ * Добавить подзадачу.
+ * @param subtask подзадача
+ * @return присвоенный идентификатор
+ */
int addNewSubtask(Subtask subtask);
- //Обновление задач всех типов
+ /* ===================== Обновление ===================== */
+
+ /**
+ * Обновить обычную задачу (по её id внутри объекта).
+ * Если задачи с таким id нет — обновление игнорируется.
+ */
void updateTask(Task task);
+
+ /**
+ * Обновить эпик (по его id внутри объекта).
+ * Если эпика с таким id нет — обновление игнорируется.
+ */
void updateEpic(Epic epic);
+
+ /**
+ * Обновить подзадачу (по её id внутри объекта).
+ * Если подзадачи с таким id нет — обновление игнорируется.
+ */
void updateSubtask(Subtask subtask);
- //Удаление задач всех типов
+ /* ===================== Удаление ===================== */
+
+ /** Удалить обычную задачу по id. */
void removeTask(int id);
+
+ /** Удалить эпик по id (его подзадачи тоже удаляются). */
void removeEpic(int id);
+
+ /** Удалить подзадачу по id. */
void removeSubtask(int id);
+ /* ===================== Получение (одна) ===================== */
+
+ /** Получить задачу по id (добавляется в историю просмотров). */
+
Task getTask(int id);
+ /** Получить эпик по id (добавляется в историю просмотров). */
Epic getEpic(int id);
+
+ /** Получить подзадачу по id (добавляется в историю просмотров). */
Subtask getSubtask(int id);
+ /* ===================== Получение (списки) ===================== */
+
+ /** Все обычные задачи. */
List getTasks();
+
+ /** Все эпики. */
List getEpics();
+
+ /** Все подзадачи. */
List getSubtasks();
+
+ /** Подзадачи конкретного эпика. */
List getEpicSubtasks(int epicId);
+ /* ===================== История ===================== */
+
+ /** Последние просмотренные задачи (до 10, без дублей). */
List getHistory();
-}
\ No newline at end of file
+}
diff --git a/src/model/Epic.java b/src/model/Epic.java
index 7ea6494..4eecf5f 100644
--- a/src/model/Epic.java
+++ b/src/model/Epic.java
@@ -2,28 +2,30 @@
import java.util.ArrayList;
import java.util.List;
-//Класс Epic представляет эпик — задачу, содержащую список подзадач.
-// * Наследуется от Task и содержит список идентификаторов всех подзадач.
+
+/** Эпик — задача, содержащая подзадачи. */
public class Epic extends Task {
private final List subtaskIds = new ArrayList<>();
public Epic(String title, String description) {
super(title, description, Status.NEW);
}
-//Защищает subtaskIds от внешнего изменения — инкапсуляция.
-//Возвращает копию списка идентификаторов подзадач.
- // * Это нужно для соблюдения принципа инкапсуляции —
- // * чтобы внешний код не мог напрямую изменить внутренний список.
+
+ @Override
+ public TaskType getType() {
+ return TaskType.EPIC;
+ }
+
+ /** Возвращает копию списка идентификаторов подзадач. */
public List getSubtaskIds() {
- return new ArrayList<>(subtaskIds); // ✅ ВОТ ТАК инкапсуляция соблюдена
+ return new ArrayList<>(subtaskIds);
}
-//Добавляет идентификатор подзадачи к эпику.
-//* Проверяет, что эпик не добавляет сам себя как подзадачу.
+ /** Добавляет id подзадачи (эпик не может ссылаться сам на себя). */
public void addSubtaskId(int id) {
if (id == this.id) {
- throw new IllegalArgumentException("Эпик не может быть собственным сабтаском");
+ throw new IllegalArgumentException("Эпик не может быть собственным Subtask");
}
subtaskIds.add(id);
}
-}
\ No newline at end of file
+}
diff --git a/src/model/Subtask.java b/src/model/Subtask.java
index eb110c6..df813ed 100644
--- a/src/model/Subtask.java
+++ b/src/model/Subtask.java
@@ -11,7 +11,24 @@ public Subtask(String title, String description, int epicId) {
this.epicId = epicId;
}
+ @Override
+ public TaskType getType() {
+ return TaskType.SUBTASK;
+ }
+
public int getEpicId() {
return epicId;
}
-}
\ No newline at end of file
+
+ @Override
+ public String toCsvRow() {
+ return String.join(",",
+ String.valueOf(id),
+ getType().name(),
+ title,
+ status.name(),
+ description,
+ String.valueOf(epicId)
+ );
+ }
+}
diff --git a/src/model/Task.java b/src/model/Task.java
index 078802d..fcf9174 100644
--- a/src/model/Task.java
+++ b/src/model/Task.java
@@ -2,6 +2,7 @@
import java.util.Objects;
+/** Базовая задача. */
public class Task {
protected String title;
protected String description;
@@ -14,18 +15,60 @@ public Task(String title, String description, Status status) {
this.status = status;
}
- public int getId() { return id; }
- public void setId(int id) { this.id = id; }
- public String getTitle() { return title; }
- public Status getStatus() { return status; }
- public void setStatus(Status status) { this.status = status; }
+ /** Тип задачи. */
+ public TaskType getType() {
+ return TaskType.TASK;
+ }
+
+ /* ========= геттеры/сеттеры ========= */
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public Status getStatus() {
+ return status;
+ }
+
+ public void setStatus(Status status) {
+ this.status = status;
+ }
+
+ /* ========= CSV-представление (для FileBackedTaskManager) ========= */
+ public String toCsvRow() {
+ // у обычной задачи поле epic пустое
+ return String.join(",",
+ String.valueOf(getId()),
+ getType().name(),
+ getTitle(),
+ getStatus().name(),
+ getDescription(),
+ ""
+ );
+ }
@Override
public boolean equals(Object o) {
- if (this == o) return true;
- if (!(o instanceof Task)) return false;
- Task task = (Task) o;
- return id == task.id;
+ if (this == o) {
+ return true;
+ }
+ // pattern matching TODO: — убирает предупреждение «Variable 'task' can be replaced with pattern variable»
+ if (!(o instanceof Task other)) {
+ return false;
+ }
+ return id == other.id;
}
@Override
@@ -41,4 +84,4 @@ public String toString() {
", status=" + status +
'}';
}
-}
\ No newline at end of file
+}
diff --git a/src/model/TaskType.java b/src/model/TaskType.java
new file mode 100644
index 0000000..fbdbeb1
--- /dev/null
+++ b/src/model/TaskType.java
@@ -0,0 +1,8 @@
+package model;
+
+/** Тип задачи: нужен для CSV сериализация. */
+public enum TaskType {
+ TASK,
+ EPIC,
+ SUBTASK
+}
diff --git a/src/test/java/manager/InMemoryTaskManagerTest.java b/src/test/java/manager/InMemoryTaskManagerTest.java
deleted file mode 100644
index aebf951..0000000
--- a/src/test/java/manager/InMemoryTaskManagerTest.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package test.java.manager;
-
-import manager.Managers;
-import manager.TaskManager;
-import model.*;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-// Юнит-тесты для {InMemoryTaskManager}.
-// Проверяются добавление задач, история просмотров и корректная обработка ошибок.
-
-class InMemoryTaskManagerTest {
- private TaskManager manager;
-//Создаёт новый экземпляр менеджера перед каждым тестом.
- @BeforeEach
- void setup() {
- manager = Managers.getDefault();
- }
-//Проверяет, что добавленная задача возвращается корректно по ID.
- @Test
- void shouldAddAndReturnTask() {
- Task task = new Task("Test task", "Desc", Status.NEW);
- int id = manager.addNewTask(task);
- Task returned = manager.getTask(id);
-
- assertNotNull(returned);
- assertEquals(task.getTitle(), returned.getTitle());
- }
-//Проверяет, что история просмотров сохраняет порядок и допускает повторы.
- @Test
- void shouldStoreHistoryCorrectly() {
- int id1 = manager.addNewTask(new Task("T1", "", Status.NEW));
- int id2 = manager.addNewTask(new Task("T2", "", Status.NEW));
-
- manager.getTask(id1);
- manager.getTask(id2);
- manager.getTask(id1);
-
- List history = manager.getHistory();
- assertEquals(3, history.size());
- assertEquals("T1", history.get(2).getTitle());
- }
-//Проверяет, что при попытке привязать подзадачу к несуществующему эпику будет выброшено исключение.
- @Test
- void shouldThrowIfSubtaskReferencesMissingEpic() {
- Subtask subtask = new Subtask("Ошибка", "Нет эпика", 999); // несуществующий epicId
- assertThrows(IllegalArgumentException.class, () -> manager.addNewSubtask(subtask));
- }
-
-//Проверяет, что история просмотров не превышает 10 элементов.
- @Test
- void historyShouldNotExceedTenEntries() {
- for (int i = 0; i < 12; i++) {
- int id = manager.addNewTask(new Task("T" + i, "", Status.NEW));
- manager.getTask(id);
- }
-
- List history = manager.getHistory();
- assertEquals(10, history.size(), "История не должна превышать 10 элементов");
- }
-}
diff --git a/src/test/manager/HistoryManagerTest.java b/src/test/manager/HistoryManagerTest.java
new file mode 100644
index 0000000..928c3a3
--- /dev/null
+++ b/src/test/manager/HistoryManagerTest.java
@@ -0,0 +1,60 @@
+package manager;
+
+
+import model.Task;
+import model.Status;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Unit-тесты самого HistoryManager
+ */
+class HistoryManagerTest {
+
+ private HistoryManager hm;
+
+ private Task t1;
+ private Task t2;
+ private Task t3;
+
+ @BeforeEach
+ void setUp() {
+ hm = Managers.getDefaultHistory(); // или new InMemoryHistoryManager()
+
+ // id задаём вручную, чтобы HistoryManager.remove(id) работал корректно
+ t1 = new Task("T-1", "description1", Status.NEW); t1.setId(1);
+ t2 = new Task("T-2", "description2", Status.NEW); t2.setId(2);
+ t3 = new Task("T-3", "description3", Status.NEW); t3.setId(3);
+ }
+
+ /** Add(): без дубликатов, последний просмотр переносится в конец */
+ @Test
+ void add_movesTaskToTail_withoutDuplicates() {
+ hm.add(t1);
+ hm.add(t2);
+ hm.add(t3);
+ hm.add(t2); // повторный просмотр t2
+
+ List history = hm.getHistory();
+ assertEquals(List.of(t1, t3, t2), history,
+ "Повторный просмотр должен перемещать задачу в конец истории без дублирования");
+ }
+
+ /** Remove(): удаляет узел из середины за O(1) */
+ @Test
+ void remove_deletesNodeFromAnyPosition() {
+ hm.add(t1);
+ hm.add(t2);
+ hm.add(t3);
+
+ hm.remove(2); // удаляем t2 (из середины списка)
+
+ List history = hm.getHistory();
+ assertEquals(List.of(t1, t3), history,
+ "После удаления задачи из середины истории в списке должны остаться t1 и t3");
+ }
+}
diff --git a/src/test/manager/InMemoryTaskManagerTest.java b/src/test/manager/InMemoryTaskManagerTest.java
new file mode 100644
index 0000000..6841ce0
--- /dev/null
+++ b/src/test/manager/InMemoryTaskManagerTest.java
@@ -0,0 +1,64 @@
+package manager;
+
+import model.Status;
+import model.Task;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+
+/**
+ * Unit-тесты InMemoryTaskManager + HistoryManager.
+ */
+class InMemoryTaskManagerTest {
+
+ private TaskManager tm;
+
+ @BeforeEach
+ void setUp() {
+ tm = Managers.getDefault(); // InMemoryTaskManager
+ }
+
+ /* -------- 1. Дубликаты не сохраняются -------- */
+ @Test
+ void addDuplicates_keepsOnlyLastView() {
+ int id = tm.addNewTask(new Task("T", "d", Status.NEW));
+
+ tm.getTask(id);
+ tm.getTask(id);
+ tm.getTask(id);
+
+ List history = tm.getHistory();
+ assertEquals(1, history.size(),
+ "В истории должен остаться единственный просмотр");
+ assertEquals(id, history.get(0).getId());//TODO: если именно нужно от Java 21 и более то нужно заменить на
+ // assertEquals(id, history.getFirst().getId());
+ }
+
+ /* -------- 2. История может быть > 10 -------- */
+ @Test
+ void historyCanGrowMoreThanTen() {
+ for (int i = 0; i < 20; i++) {
+ int id = tm.addNewTask(new Task("task-" + i, "", Status.NEW));
+ tm.getTask(id);
+ }
+ assertEquals(20, tm.getHistory().size(),
+ "История должна содержать все 20 просмотров");
+ }
+
+ /* -------- 3. Удаление чистит историю -------- */
+ @Test
+ void deletingTask_removesItFromHistory() {
+ int id = tm.addNewTask(new Task("X", "", Status.NEW));
+ tm.getTask(id);
+
+ tm.removeTask(id);
+
+ assertTrue(tm.getHistory().isEmpty(),
+ "После удаления задачи записи о ней в истории быть не должно");
+ }
+}
diff --git a/src/test/manager/TaskManagerHistoryIntegrationTest.java b/src/test/manager/TaskManagerHistoryIntegrationTest.java
new file mode 100644
index 0000000..26e42b4
--- /dev/null
+++ b/src/test/manager/TaskManagerHistoryIntegrationTest.java
@@ -0,0 +1,36 @@
+package manager;
+
+
+import model.Task;
+import model.Status;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Интеграционный тест: TaskManager ↔ HistoryManager
+ */
+class TaskManagerHistoryIntegrationTest {
+
+ private TaskManager tm;
+
+ @BeforeEach
+ void setUp() {
+ tm = Managers.getDefault(); // new InMemoryTaskManager()
+ }
+
+ /** Удаление задачи очищает историю */
+ @Test
+ void deletingTask_removesItFromHistory() {
+ int id = tm.addNewTask(new Task("Task-1", "description", Status.NEW));
+
+ tm.getTask(id); // помещаем в историю
+ assertEquals(1, tm.getHistory().size(),
+ "После просмотра история должна содержать одну запись");
+
+ tm.removeTask(id); // удаляем задачу
+ assertTrue(tm.getHistory().isEmpty(),
+ "После удаления задачи история должна быть пустой");
+ }
+}