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(), + "После удаления задачи история должна быть пустой"); + } +}