From 8988ac0b01ca584fbf51f2d382118ef45fed6c39 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Tue, 17 Mar 2026 21:14:05 +0300 Subject: [PATCH 01/20] Refactor history manager: add remove method and linked list implementation --- src/model/Epic.java | 2 +- src/service/HistoryManager.java | 2 +- src/service/InMemoryHistoryManager.java | 94 ++++++++++++++++++++++--- 3 files changed, 88 insertions(+), 10 deletions(-) diff --git a/src/model/Epic.java b/src/model/Epic.java index e5c4dcd..00edfd5 100644 --- a/src/model/Epic.java +++ b/src/model/Epic.java @@ -10,7 +10,7 @@ public Epic(String name, String description) { } public ArrayList getSubtaskIds() { - return new ArrayList<>(subtaskIds);; + return new ArrayList<>(subtaskIds); } public void setSubtaskIds(ArrayList subtaskIds) { diff --git a/src/service/HistoryManager.java b/src/service/HistoryManager.java index 532225d..59af633 100644 --- a/src/service/HistoryManager.java +++ b/src/service/HistoryManager.java @@ -5,6 +5,6 @@ public interface HistoryManager { void add(Task task); - + void remove(int id); List getHistory(); } diff --git a/src/service/InMemoryHistoryManager.java b/src/service/InMemoryHistoryManager.java index 4a40100..de282ed 100644 --- a/src/service/InMemoryHistoryManager.java +++ b/src/service/InMemoryHistoryManager.java @@ -3,27 +3,105 @@ import model.Task; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; public class InMemoryHistoryManager implements HistoryManager { - private static final int HISTORY_LIMIT = 10; - // Список для хранения истории просмотров задач - private final List history = new ArrayList<>(); + // Двусвязный список с самой задачей и ссылками на соседей + private static class Node { + Task task; + Node prev; + Node next; + + Node(Task task) { + this.task = task; + } + } + + // Голова и хвост двусвязного списка + private Node head; + private Node tail; + + // В HashMap ключ = id задачи, значение = узел в списке + private final HashMap historyMap = new HashMap<>(); + + // Добавление задачи в конец двусвязного списка + private void linkLast(Task task) { + Node newNode = new Node(task); + if (tail == null) { + // Если список пустой, то полностью новый узел - и голова, и хвост + head = newNode; + tail = newNode; + } else { + // Иначе подвешивание к хвосту + tail.next = newNode; + newNode.prev = tail; + tail = newNode; + } + // Сохранение узла в индексе + historyMap.put(task.getId(), newNode); + } + + // Удаление конкретного узла из списка + private void removeNode(Node node) { + if (node == null) return; + + Node prevNode = node.prev; + Node nextNode = node.next; + + if (prevNode != null) { + prevNode.next = nextNode; // левый сосед смотрит на правого + } else { + head = nextNode; // удаление головы и новая голова - это следующий + } + + if (nextNode != null) { + nextNode.prev = prevNode; // правый сосед теперь смотрит на левого + } else { + tail = prevNode; // удаление хвоста и новый хвост - это предыдущий + } + + // Обнуление ссылок удалённого узла (помощь Garbage Collector) + node.prev = null; + node.next = null; + } + + // Сбор задач всех из двусвязного списка в ArrayList + private List getTasks() { + List result = new ArrayList<>(); + Node current = head; + while (current != null) { + result.add(current.task); + current = current.next; + } + return result; + } // Добавление в историю @Override public void add(Task task) { - history.add(task); - // Ограничение в 10 элементов - if (history.size() > HISTORY_LIMIT) { - history.remove(0); + if (task == null) return; + + // Если задача уже в истории, значит удаление старого просмотра + if (historyMap.containsKey(task.getId())) { + removeNode(historyMap.get(task.getId())); } + + // Добавление в конец списка и обновление индекса + linkLast(task); + } + + // Удаление задачи из истории по id + @Override + public void remove(int id) { + Node node = historyMap.remove(id); + removeNode(node); } // Возвращение списка истории @Override public List getHistory() { - return new ArrayList<>(history); + return getTasks(); } } From a7f1b0906412410f5a7e8acc307f5c35dc998326 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Tue, 17 Mar 2026 21:19:47 +0300 Subject: [PATCH 02/20] Fix task manager: remove tasks from history on delete --- src/service/InMemoryTaskManager.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/service/InMemoryTaskManager.java b/src/service/InMemoryTaskManager.java index 584d125..49f6fc9 100644 --- a/src/service/InMemoryTaskManager.java +++ b/src/service/InMemoryTaskManager.java @@ -197,6 +197,7 @@ public void updateSubtask(Subtask subtask) { @Override public void deleteTask(int id) { tasks.remove(id); + historyManager.remove(id); } @Override @@ -206,8 +207,10 @@ public void deleteEpic(int id) { // Удаление подзадач, связанных с этим Эпиком for (Integer subtaskId : epic.getSubtaskIds()) { subtasks.remove(subtaskId); + historyManager.remove(subtaskId); } } + historyManager.remove(id); } @Override @@ -221,6 +224,7 @@ public void deleteSubtask(int id) { updateEpicStatus(epic); } } + historyManager.remove(id); } // Получение списка всех Подзадач для определённого Эпика From 6de48cfb33fd79c1bd91ec900645206fd8ab742f Mon Sep 17 00:00:00 2001 From: Ksenia Date: Tue, 17 Mar 2026 21:28:13 +0300 Subject: [PATCH 03/20] Add history manager tests: duplicates, unlimited size, remove from start/middle/end --- test/service/InMemoryHistoryManagerTest.java | 89 ++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/test/service/InMemoryHistoryManagerTest.java b/test/service/InMemoryHistoryManagerTest.java index 4a45cd6..b0027b6 100644 --- a/test/service/InMemoryHistoryManagerTest.java +++ b/test/service/InMemoryHistoryManagerTest.java @@ -38,4 +38,93 @@ void add() { assertEquals(task.getStatus(), history.get(0).getStatus(), "Статус задачи в истории не совпадает"); assertEquals(task.getId(), history.get(0).getId(), "ID задачи в истории не совпадает"); } + + // При повторном добавлении задачи в историю дубликат не создастся + @Test + void addShouldNotCreateDuplicates() { + Task task = new Task("Task", "Description", Status.NEW); + task.setId(1); + + historyManager.add(task); + historyManager.add(task); // добавляем второй раз + historyManager.add(task); // и третий + + assertEquals(1, historyManager.getHistory().size(), "История не должна содержать дубликаты"); + } + + // История не должна ограничиваться 10 элементами + @Test + void historyShouldBeUnlimited() { + for (int i = 1; i <= 15; i++) { + Task task = new Task("Task " + i, "Description", Status.NEW); + task.setId(i); + historyManager.add(task); + } + + assertEquals(15, historyManager.getHistory().size(), "История должна хранить более 10 элементов"); + } + + // Удаление из начала истории + @Test + void removeShouldDeleteFromBeginning() { + Task task1 = new Task("Task1", "Desc", Status.NEW); + task1.setId(1); + Task task2 = new Task("Task2", "Desc", Status.NEW); + task2.setId(2); + Task task3 = new Task("Task3", "Desc", Status.NEW); + task3.setId(3); + + historyManager.add(task1); + historyManager.add(task2); + historyManager.add(task3); + + historyManager.remove(1); // удаление первой - головы + + List history = historyManager.getHistory(); + assertEquals(2, history.size(), "В истории должно остаться 2 задачи"); + assertEquals(2, history.get(0).getId(), "Первой должна быть task2"); + } + + // Удаление из середины истории + @Test + void removeShouldDeleteFromMiddle() { + Task task1 = new Task("Task1", "Desc", Status.NEW); + task1.setId(1); + Task task2 = new Task("Task2", "Desc", Status.NEW); + task2.setId(2); + Task task3 = new Task("Task3", "Desc", Status.NEW); + task3.setId(3); + + historyManager.add(task1); + historyManager.add(task2); + historyManager.add(task3); + + historyManager.remove(2); // удаление из середины + + List history = historyManager.getHistory(); + assertEquals(2, history.size(), "В истории должно остаться 2 задачи"); + assertEquals(1, history.get(0).getId(), "Первой должна быть task1"); + assertEquals(3, history.get(1).getId(), "Второй должна быть task3"); + } + + // Удаление из конца истории + @Test + void removeShouldDeleteFromEnd() { + Task task1 = new Task("Task1", "Desc", Status.NEW); + task1.setId(1); + Task task2 = new Task("Task2", "Desc", Status.NEW); + task2.setId(2); + Task task3 = new Task("Task3", "Desc", Status.NEW); + task3.setId(3); + + historyManager.add(task1); + historyManager.add(task2); + historyManager.add(task3); + + historyManager.remove(3); // удаление последней - хвоста + + List history = historyManager.getHistory(); + assertEquals(2, history.size(), "В истории должно остаться 2 задачи"); + assertEquals(2, history.get(1).getId(), "Последней должна быть task2"); + } } \ No newline at end of file From 324ee81bc39e1a1fd98cf351178d3ab37fda7f2b Mon Sep 17 00:00:00 2001 From: Ksenia Date: Tue, 17 Mar 2026 21:36:24 +0300 Subject: [PATCH 04/20] Add task manager tests: history cleanup on delete task/epic/subtask --- test/service/InMemoryTaskManagerTest.java | 58 ++++++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/test/service/InMemoryTaskManagerTest.java b/test/service/InMemoryTaskManagerTest.java index 9c6beac..74e00c6 100644 --- a/test/service/InMemoryTaskManagerTest.java +++ b/test/service/InMemoryTaskManagerTest.java @@ -80,7 +80,8 @@ void taskWithGeneratedIdShouldNotConflict() { taskManager.createTask(task); - assertNotEquals(999, task.getId(), "Менеджер должен игнорировать заданный вручную ID и генерировать уникальный"); + assertNotEquals(999, task.getId(), + "Менеджер должен игнорировать заданный вручную ID и генерировать уникальный"); Task savedTask = taskManager.getTask(task.getId()); assertNotNull(savedTask, "Задача должна быть найдена по сгенерированному ID"); @@ -105,7 +106,60 @@ void taskShouldBeUnchangedAfterAddingToManager() { // Проверяем assertEquals(expectedName, savedTask.getName(), "Имя задачи изменилось при сохранении"); - assertEquals(expectedDescription, savedTask.getDescription(), "Описание задачи изменилось при сохранении"); + assertEquals(expectedDescription, savedTask.getDescription(), + "Описание задачи изменилось при сохранении"); assertEquals(expectedStatus, savedTask.getStatus(), "Статус задачи изменился при сохранении"); } + + // Если удалена задача, то она должна исчезнуть из истории + @Test + void deletedTaskShouldBeRemovedFromHistory() { + Task task = new Task("Task", "Description", Status.NEW); + taskManager.createTask(task); + + taskManager.getTask(task.getId()); + assertEquals(1, taskManager.getHistory().size(), "Задача должна быть в истории"); + + taskManager.deleteTask(task.getId()); // удаляем + assertEquals(0, taskManager.getHistory().size(), + "Удалённая задача не должна оставаться в истории"); + } + + // При удалении эпика из истории удаляется и сам эпик, и все его подзадачи + @Test + void deletedEpicShouldBeRemovedFromHistoryWithSubtasks() { + Epic epic = new Epic("Epic", "Description"); + taskManager.createEpic(epic); + + Subtask subtask1 = new Subtask("Sub1", "Desc", Status.NEW, epic.getId()); + Subtask subtask2 = new Subtask("Sub2", "Desc", Status.NEW, epic.getId()); + taskManager.createSubtask(subtask1); + taskManager.createSubtask(subtask2); + + // Просматриваем всё — всё попадает в историю + taskManager.getEpic(epic.getId()); + taskManager.getSubtask(subtask1.getId()); + taskManager.getSubtask(subtask2.getId()); + assertEquals(3, taskManager.getHistory().size(), "В истории должно быть 3 элемента"); + + taskManager.deleteEpic(epic.getId()); // удаляем эпик + assertEquals(0, taskManager.getHistory().size(), + "После удаления эпика история должна быть пустой"); + } + + // Если удалена подзадача, то её id не должен оставаться внутри эпика + @Test + void deletedSubtaskShouldBeRemovedFromEpic() { + Epic epic = new Epic("Epic", "Description"); + taskManager.createEpic(epic); + + Subtask subtask = new Subtask("Sub", "Desc", Status.NEW, epic.getId()); + taskManager.createSubtask(subtask); + + int subtaskId = subtask.getId(); + taskManager.deleteSubtask(subtaskId); + + assertEquals(0, taskManager.getEpicSubtasks(epic.getId()).size(), + "После удаления подзадачи эпик не должен содержать её id"); + } } \ No newline at end of file From 3fd4883d84f9ec8797be4dea2c491594a72800f2 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Tue, 17 Mar 2026 21:58:16 +0300 Subject: [PATCH 05/20] Fix getTask returns copy to prevent external mutation, add setter test --- src/service/InMemoryTaskManager.java | 12 ++++++++---- test/service/InMemoryTaskManagerTest.java | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/service/InMemoryTaskManager.java b/src/service/InMemoryTaskManager.java index 49f6fc9..88512b0 100644 --- a/src/service/InMemoryTaskManager.java +++ b/src/service/InMemoryTaskManager.java @@ -68,10 +68,14 @@ public void clearSubtasks() { @Override public Task getTask(int id) { Task task = tasks.get(id); - if (task != null) { - historyManager.add(task); - } - return task; + if (task == null) return null; + + historyManager.add(task); + + Task copy = new Task(task.getName(), task.getDescription(), task.getStatus()); + copy.setId(task.getId()); + + return copy; } @Override diff --git a/test/service/InMemoryTaskManagerTest.java b/test/service/InMemoryTaskManagerTest.java index 74e00c6..1b4cf64 100644 --- a/test/service/InMemoryTaskManagerTest.java +++ b/test/service/InMemoryTaskManagerTest.java @@ -162,4 +162,20 @@ void deletedSubtaskShouldBeRemovedFromEpic() { assertEquals(0, taskManager.getEpicSubtasks(epic.getId()).size(), "После удаления подзадачи эпик не должен содержать её id"); } + + // Изменение задачи через сеттер не должно влиять на данные внутри менеджера + @Test + void taskShouldNotChangeInManagerAfterSetterCall() { + Task task = new Task("Оригинальное имя", "Описание", Status.NEW); + taskManager.createTask(task); + + // Получаем задачу и меняем через сеттер + Task savedTask = taskManager.getTask(task.getId()); + savedTask.setName("Изменённое имя"); + + // Проверяем что в менеджере данные не изменились + assertEquals("Оригинальное имя", + taskManager.getTask(task.getId()).getName(), + "Сеттер не должен менять данные задачи внутри менеджера"); + } } \ No newline at end of file From 05af7be80f2f397ecba7c00e0ee75d676f5bbc46 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Tue, 17 Mar 2026 22:48:14 +0300 Subject: [PATCH 06/20] fix: clear methods now remove tasks from history; --- src/service/InMemoryTaskManager.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/service/InMemoryTaskManager.java b/src/service/InMemoryTaskManager.java index 88512b0..9ac53cd 100644 --- a/src/service/InMemoryTaskManager.java +++ b/src/service/InMemoryTaskManager.java @@ -45,18 +45,37 @@ public ArrayList getSubtasks() { // Удаление всех задач для каждого из типов задач(Задача/Эпик/Подзадача) @Override public void clearTasks() { + // Удаление каждой задачи из истории перед очисткой хранилища + for (int id : tasks.keySet()) { + historyManager.remove(id); + } tasks.clear(); } @Override public void clearEpics() { + // Сначала удаление всех подзадач из истории + for (int id : subtasks.keySet()) { + historyManager.remove(id); + } + // Затем самих эпиков + for (int id : epics.keySet()) { + historyManager.remove(id); + } + epics.clear(); subtasks.clear(); } @Override public void clearSubtasks() { + // Удаление каждой подзадачи из истории + for (int id : subtasks.keySet()) { + historyManager.remove(id); + } + subtasks.clear(); + // Чистка Эпиков, если подзадач больше нет for (Epic epic : epics.values()) { epic.clearSubtaskIds(); From b8faf3fbca47d6883de3fb9480a9d32e2b79d054 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Tue, 17 Mar 2026 23:00:15 +0300 Subject: [PATCH 07/20] test & fix: add clear history tests; fix getSubtask/getEpic to return copies --- src/service/InMemoryTaskManager.java | 31 ++++++++---- test/service/InMemoryTaskManagerTest.java | 58 ++++++++++++++++++++++- 2 files changed, 79 insertions(+), 10 deletions(-) diff --git a/src/service/InMemoryTaskManager.java b/src/service/InMemoryTaskManager.java index 9ac53cd..642d1d5 100644 --- a/src/service/InMemoryTaskManager.java +++ b/src/service/InMemoryTaskManager.java @@ -100,19 +100,34 @@ public Task getTask(int id) { @Override public Epic getEpic(int id) { Epic epic = epics.get(id); - if (epic != null) { - historyManager.add(epic); - } - return epic; + if (epic == null) return null; + + historyManager.add(epic); + + Epic copy = new Epic(epic.getName(), epic.getDescription()); + copy.setId(epic.getId()); + copy.setStatus(epic.getStatus()); + copy.setSubtaskIds(epic.getSubtaskIds()); + + return copy; } @Override public Subtask getSubtask(int id) { Subtask subtask = subtasks.get(id); - if (subtask != null) { - historyManager.add(subtask); - } - return subtask; + if (subtask == null) return null; + + historyManager.add(subtask); + + Subtask copy = new Subtask( + subtask.getName(), + subtask.getDescription(), + subtask.getStatus(), + subtask.getEpicId() + ); + copy.setId(subtask.getId()); + + return copy; } // Возвращение списка истории diff --git a/test/service/InMemoryTaskManagerTest.java b/test/service/InMemoryTaskManagerTest.java index 1b4cf64..db09cd4 100644 --- a/test/service/InMemoryTaskManagerTest.java +++ b/test/service/InMemoryTaskManagerTest.java @@ -169,13 +169,67 @@ void taskShouldNotChangeInManagerAfterSetterCall() { Task task = new Task("Оригинальное имя", "Описание", Status.NEW); taskManager.createTask(task); - // Получаем задачу и меняем через сеттер Task savedTask = taskManager.getTask(task.getId()); savedTask.setName("Изменённое имя"); - // Проверяем что в менеджере данные не изменились assertEquals("Оригинальное имя", taskManager.getTask(task.getId()).getName(), "Сеттер не должен менять данные задачи внутри менеджера"); } + + // clearTasks() должен удалять задачи из истории + @Test + void clearTasksShouldRemoveTasksFromHistory() { + Task task1 = new Task("Task1", "Desc", Status.NEW); + Task task2 = new Task("Task2", "Desc", Status.NEW); + taskManager.createTask(task1); + taskManager.createTask(task2); + + taskManager.getTask(task1.getId()); + taskManager.getTask(task2.getId()); + assertEquals(2, taskManager.getHistory().size(), + "Перед очисткой в истории должно быть 2 задачи"); + + taskManager.clearTasks(); + + assertEquals(0, taskManager.getHistory().size(), + "После clearTasks история должна быть пустой"); + } + + // clearEpics() должен удалять эпики и их подзадачи из истории + @Test + void clearEpicsShouldRemoveEpicsAndSubtasksFromHistory() { + Epic epic = new Epic("Epic", "Desc"); + taskManager.createEpic(epic); + + Subtask subtask = new Subtask("Sub", "Desc", Status.NEW, epic.getId()); + taskManager.createSubtask(subtask); + + taskManager.getEpic(epic.getId()); + taskManager.getSubtask(subtask.getId()); + assertEquals(2, taskManager.getHistory().size(), + "Перед очисткой в истории должно быть 2 элемента"); + + taskManager.clearEpics(); + + assertEquals(0, taskManager.getHistory().size(), + "После clearEpics история должна быть пустой"); + } + + // Изменение подзадачи через сеттер не должно влиять на данные внутри менеджера + @Test + void subtaskShouldNotChangeInManagerAfterSetterCall() { + Epic epic = new Epic("Epic", "Desc"); + taskManager.createEpic(epic); + + Subtask subtask = new Subtask("Оригинальное имя", "Описание", Status.NEW, epic.getId()); + taskManager.createSubtask(subtask); + + Subtask savedSubtask = taskManager.getSubtask(subtask.getId()); + savedSubtask.setName("Изменённое имя"); + + assertEquals("Оригинальное имя", + taskManager.getSubtask(subtask.getId()).getName(), + "Сеттер не должен менять данные подзадачи внутри менеджера"); + } } \ No newline at end of file From 6d35f35972f5a1e6e28545f9604ffe0af39080cf Mon Sep 17 00:00:00 2001 From: Ksenia Date: Tue, 17 Mar 2026 23:07:13 +0300 Subject: [PATCH 08/20] refactor: update Main to match sprint 6 scenario --- src/Main.java | 58 ++++++++++++++++++++------------------------------- 1 file changed, 23 insertions(+), 35 deletions(-) diff --git a/src/Main.java b/src/Main.java index ccfa2bb..23e9d56 100644 --- a/src/Main.java +++ b/src/Main.java @@ -24,22 +24,21 @@ public static void main(String[] args) { manager.createTask(task1); manager.createTask(task2); - // Создание Эпика с двумя Подзадачами - Epic epic1 = new Epic("Эпик 1", "Эпик с 2 подзадачами"); + // Создание Эпика с тремя подзадачами + Epic epic1 = new Epic("Эпик 1", "Эпик с 3 подзадачами"); manager.createEpic(epic1); Subtask subtask1 = new Subtask("Подзадача 1", "Подзадача Эпика 1", Status.NEW, epic1.getId()); Subtask subtask2 = new Subtask("Подзадача 2", "Подзадача Эпика 1", Status.NEW, epic1.getId()); + Subtask subtask3 = new Subtask("Подзадача 3", "Подзадача Эпика 1", Status.NEW, epic1.getId()); manager.createSubtask(subtask1); manager.createSubtask(subtask2); + manager.createSubtask(subtask3); - // Создание Эпика с одной подзадачей - Epic epic2 = new Epic("Эпик 2", "Эпик с 1 подзадачей"); + // Создание Эпика без подзадач + Epic epic2 = new Epic("Эпик 2", "Эпик без подзадач"); manager.createEpic(epic2); - Subtask subtask3 = new Subtask("Подзадача 1", "Подзадача Эпика 2", Status.NEW, epic2.getId()); - manager.createSubtask(subtask3); - // Печать начального состояния списков Эпиков, Задач и Подзадач System.out.println("Задачи: " + manager.getTasks()); System.out.println(); @@ -50,15 +49,6 @@ public static void main(String[] args) { // Разделительная строка printDelimiter(); - // Изменение статуса Задачи 2 - task2.setStatus(Status.IN_PROGRESS); - manager.updateTask(task2); - System.out.println("Задача 2: в процессе"); - System.out.println("Статус задачи 2: " + manager.getTask(task2.getId()).getStatus()); - - // Разделительная строка - printDelimiter(); - // Изменение статуса Подзадач в Эпике 1 subtask1.setStatus(Status.DONE); subtask2.setStatus(Status.IN_PROGRESS); @@ -88,39 +78,37 @@ public static void main(String[] args) { // Разделительная строка printDelimiter(); - // Заполнение истории просмотра - System.out.println("Запрашиваем задачи, чтобы заполнить историю."); - - // много запросов, чтобы превысить лимит 10 + // Запрашивание задачи несколько раз в разном порядке + System.out.println("Запрашиваем задачи в разном порядке."); manager.getTask(task1.getId()); - manager.getTask(task2.getId()); manager.getEpic(epic1.getId()); manager.getSubtask(subtask1.getId()); + manager.getTask(task2.getId()); manager.getSubtask(subtask2.getId()); manager.getEpic(epic2.getId()); manager.getSubtask(subtask3.getId()); - // Повторные запросы (дубли) - manager.getTask(task1.getId()); - manager.getEpic(epic1.getId()); + // Повторные запросы = проверке, что дублей нет + manager.getTask(task1.getId()); // если task1 уже был = перемещение в конец + manager.getSubtask(subtask1.getId()); // если subtask1 уже был = перемещение в конец - // Еще запросы, чтобы вытеснить старые и проверить, выполняется ли лимит в 10 задач - manager.getTask(task2.getId()); - manager.getSubtask(subtask1.getId()); - - // Проверка истории - System.out.println("Проверка истории (10 элементов):"); + System.out.println("История (без повторов):"); printAllTasks(manager); - // Разделительная строка printDelimiter(); - // Удаление задачи и эпика - System.out.println("Удаляем задачу 1 и эпик 1"); + // Удаление задачи из истории + System.out.println("Удаляем задачу 1"); manager.deleteTask(task1.getId()); - manager.deleteEpic(epic1.getId()); + System.out.println("История после удаления задачи 1:"); + printAllTasks(manager); - System.out.println("Состояние после удаления:"); + printDelimiter(); + + // Удаление эпика с тремя подзадачами + System.out.println("Удаляем эпик 1 (с тремя подзадачами)"); + manager.deleteEpic(epic1.getId()); + System.out.println("История после удаления эпика 1 и его подзадач:"); printAllTasks(manager); } From a8ad16684cefa1f3d33f93dab885225406d07352 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Wed, 18 Mar 2026 16:57:16 +0300 Subject: [PATCH 09/20] fix(test): fix subtaskCannotBeEpic - properly verify setId guard against id == epicId --- test/model/SubtaskTest.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/model/SubtaskTest.java b/test/model/SubtaskTest.java index da8fd5b..64e6ed0 100644 --- a/test/model/SubtaskTest.java +++ b/test/model/SubtaskTest.java @@ -24,9 +24,12 @@ public void subtasksShouldBeEqualIfIdEquals(){ public void subtaskCannotBeEpic() { Subtask subtask = new Subtask("Subtask", "Description", Status.NEW, 3); - subtask.setId(3); + subtask.setId(5); + Assertions.assertEquals(5, subtask.getId(), "Корректный id должен установиться"); - Assertions.assertNotEquals(3, subtask.getId(), "Подзадача не должна позволять устанавливать ID, равный EpicID"); + subtask.setId(3); + Assertions.assertEquals(5, subtask.getId(), + "Подзадача не должна позволять устанавливать ID, равный EpicID"); } From 5a66e4dbb3068847519a487326e34df1a8fa99a2 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Wed, 18 Mar 2026 17:01:02 +0300 Subject: [PATCH 10/20] test: add clearSubtasks history removal test --- test/model/SubtaskTest.java | 3 --- test/service/InMemoryTaskManagerTest.java | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/test/model/SubtaskTest.java b/test/model/SubtaskTest.java index 64e6ed0..aedddc0 100644 --- a/test/model/SubtaskTest.java +++ b/test/model/SubtaskTest.java @@ -31,7 +31,4 @@ public void subtaskCannotBeEpic() { Assertions.assertEquals(5, subtask.getId(), "Подзадача не должна позволять устанавливать ID, равный EpicID"); } - - - } \ No newline at end of file diff --git a/test/service/InMemoryTaskManagerTest.java b/test/service/InMemoryTaskManagerTest.java index db09cd4..624a296 100644 --- a/test/service/InMemoryTaskManagerTest.java +++ b/test/service/InMemoryTaskManagerTest.java @@ -232,4 +232,23 @@ void subtaskShouldNotChangeInManagerAfterSetterCall() { taskManager.getSubtask(subtask.getId()).getName(), "Сеттер не должен менять данные подзадачи внутри менеджера"); } + + // clearSubtasks() должен удалять подзадачи из истории + @Test + void clearSubtasksShouldRemoveSubtasksFromHistory() { + Epic epic = new Epic("Epic", "Desc"); + taskManager.createEpic(epic); + + Subtask subtask = new Subtask("Sub", "Desc", Status.NEW, epic.getId()); + taskManager.createSubtask(subtask); + + taskManager.getSubtask(subtask.getId()); // добавление в историю + assertEquals(1, taskManager.getHistory().size(), + "Перед очисткой в истории должна быть 1 подзадача"); + + taskManager.clearSubtasks(); + + assertEquals(0, taskManager.getHistory().size(), + "После clearSubtasks история должна быть пустой"); + } } \ No newline at end of file From 8c7af0f6beaf6ba7b7ff80a7392a756bd6b9c7a5 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Wed, 18 Mar 2026 17:03:01 +0300 Subject: [PATCH 11/20] test: add epic immutability test after adding to manager --- test/service/InMemoryTaskManagerTest.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/service/InMemoryTaskManagerTest.java b/test/service/InMemoryTaskManagerTest.java index 624a296..8a43bf2 100644 --- a/test/service/InMemoryTaskManagerTest.java +++ b/test/service/InMemoryTaskManagerTest.java @@ -251,4 +251,21 @@ void clearSubtasksShouldRemoveSubtasksFromHistory() { assertEquals(0, taskManager.getHistory().size(), "После clearSubtasks история должна быть пустой"); } + + // Проверка неизменности эпика по всем полям при добавлении в менеджер + @Test + void epicShouldBeUnchangedAfterAddingToManager() { + String expectedName = "Оригинальный эпик"; + String expectedDescription = "Описание эпика"; + + Epic epic = new Epic(expectedName, expectedDescription); + taskManager.createEpic(epic); + + Epic savedEpic = taskManager.getEpic(epic.getId()); + + assertEquals(expectedName, savedEpic.getName(), + "Имя эпика изменилось при сохранении"); + assertEquals(expectedDescription, savedEpic.getDescription(), + "Описание эпика изменилось при сохранении"); + } } \ No newline at end of file From 50c10edfee7e61e7ef7eb312a0778ae32075c320 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Wed, 18 Mar 2026 17:07:28 +0300 Subject: [PATCH 12/20] refactor: rename local variable in getEpicSubtasks to avoid shadowing class field --- src/service/InMemoryTaskManager.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/service/InMemoryTaskManager.java b/src/service/InMemoryTaskManager.java index 642d1d5..1c4e95a 100644 --- a/src/service/InMemoryTaskManager.java +++ b/src/service/InMemoryTaskManager.java @@ -266,15 +266,14 @@ public void deleteSubtask(int id) { } // Получение списка всех Подзадач для определённого Эпика - @Override public ArrayList getEpicSubtasks(int epicId) { - ArrayList tasks = new ArrayList<>(); + ArrayList result = new ArrayList<>(); Epic epic = epics.get(epicId); if (epic != null) { for (int subtaskId : epic.getSubtaskIds()) { - tasks.add(subtasks.get(subtaskId)); + result.add(subtasks.get(subtaskId)); } } - return tasks; + return result; } } From 68be48a0c1f98c181a3b04e842bf4af7abb4d839 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Wed, 18 Mar 2026 18:38:12 +0300 Subject: [PATCH 13/20] feat: implement history with LinkedList and HashMap --- src/service/InMemoryHistoryManager.java | 22 +++++++------------ src/service/Node.java | 14 ++++++++++++ test/service/InMemoryHistoryManagerTest.java | 23 ++++++++++++++++++++ 3 files changed, 45 insertions(+), 14 deletions(-) create mode 100644 src/service/Node.java diff --git a/src/service/InMemoryHistoryManager.java b/src/service/InMemoryHistoryManager.java index de282ed..5c556b8 100644 --- a/src/service/InMemoryHistoryManager.java +++ b/src/service/InMemoryHistoryManager.java @@ -8,17 +8,6 @@ public class InMemoryHistoryManager implements HistoryManager { - // Двусвязный список с самой задачей и ссылками на соседей - private static class Node { - Task task; - Node prev; - Node next; - - Node(Task task) { - this.task = task; - } - } - // Голова и хвост двусвязного списка private Node head; private Node tail; @@ -83,13 +72,18 @@ private List getTasks() { public void add(Task task) { if (task == null) return; - // Если задача уже в истории, значит удаление старого просмотра + // Если задача уже в истории = удаление старого просмотра if (historyMap.containsKey(task.getId())) { removeNode(historyMap.get(task.getId())); + historyMap.remove(task.getId()); // убирание из индекса } - // Добавление в конец списка и обновление индекса - linkLast(task); + // Создание копии, чтобы история хранила снимок состояния на момент просмотра + Task copy = new Task(task.getName(), task.getDescription(), task.getStatus()); + copy.setId(task.getId()); + + // Добавление копии в конец списка + linkLast(copy); } // Удаление задачи из истории по id diff --git a/src/service/Node.java b/src/service/Node.java new file mode 100644 index 0000000..f2a00ad --- /dev/null +++ b/src/service/Node.java @@ -0,0 +1,14 @@ +package service; + +import model.Task; + +// Двусвязный список с самой задачей и ссылками на соседей +public class Node { + Task task; + Node prev; + Node next; + + public Node(Task task) { + this.task = task; + } +} diff --git a/test/service/InMemoryHistoryManagerTest.java b/test/service/InMemoryHistoryManagerTest.java index b0027b6..51c9043 100644 --- a/test/service/InMemoryHistoryManagerTest.java +++ b/test/service/InMemoryHistoryManagerTest.java @@ -127,4 +127,27 @@ void removeShouldDeleteFromEnd() { assertEquals(2, history.size(), "В истории должно остаться 2 задачи"); assertEquals(2, history.get(1).getId(), "Последней должна быть task2"); } + + // Повторный просмотр задачи должен переместить её в конец истории + @Test + void repeatedViewShouldMoveTaskToEnd() { + Task task1 = new Task("Task1", "Desc", Status.NEW); + task1.setId(1); + Task task2 = new Task("Task2", "Desc", Status.NEW); + task2.setId(2); + Task task3 = new Task("Task3", "Desc", Status.NEW); + task3.setId(3); + + historyManager.add(task1); + historyManager.add(task2); + historyManager.add(task3); + historyManager.add(task1); // task1 повторно — должна уйти в конец + + List history = historyManager.getHistory(); + + assertEquals(3, history.size(), "Дубликатов быть не должно"); + assertEquals(2, history.get(0).getId(), "Первой должна быть task2"); + assertEquals(3, history.get(1).getId(), "Второй должна быть task3"); + assertEquals(1, history.get(2).getId(), "Последней должна быть task1"); + } } \ No newline at end of file From 01612b6cb31371eab2d6fa0e655deb4b36a05595 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Wed, 18 Mar 2026 19:10:51 +0300 Subject: [PATCH 14/20] refactor: encapsulate Node fields, optimize history add method --- src/service/InMemoryHistoryManager.java | 32 +++++++++++-------------- src/service/Node.java | 14 ++++++++--- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/service/InMemoryHistoryManager.java b/src/service/InMemoryHistoryManager.java index 5c556b8..df4f6ae 100644 --- a/src/service/InMemoryHistoryManager.java +++ b/src/service/InMemoryHistoryManager.java @@ -24,8 +24,8 @@ private void linkLast(Task task) { tail = newNode; } else { // Иначе подвешивание к хвосту - tail.next = newNode; - newNode.prev = tail; + tail.setNext(newNode); + newNode.setPrev(tail); tail = newNode; } // Сохранение узла в индексе @@ -36,24 +36,24 @@ private void linkLast(Task task) { private void removeNode(Node node) { if (node == null) return; - Node prevNode = node.prev; - Node nextNode = node.next; + Node prevNode = node.getPrev(); + Node nextNode = node.getNext(); if (prevNode != null) { - prevNode.next = nextNode; // левый сосед смотрит на правого + prevNode.setNext(nextNode); // левый сосед смотрит на правого } else { head = nextNode; // удаление головы и новая голова - это следующий } if (nextNode != null) { - nextNode.prev = prevNode; // правый сосед теперь смотрит на левого + nextNode.setPrev(prevNode); // правый сосед теперь смотрит на левого } else { tail = prevNode; // удаление хвоста и новый хвост - это предыдущий } // Обнуление ссылок удалённого узла (помощь Garbage Collector) - node.prev = null; - node.next = null; + node.setPrev(null); + node.setNext(null); } // Сбор задач всех из двусвязного списка в ArrayList @@ -61,8 +61,8 @@ private List getTasks() { List result = new ArrayList<>(); Node current = head; while (current != null) { - result.add(current.task); - current = current.next; + result.add(current.getTask()); + current = current.getNext(); } return result; } @@ -73,17 +73,13 @@ public void add(Task task) { if (task == null) return; // Если задача уже в истории = удаление старого просмотра - if (historyMap.containsKey(task.getId())) { - removeNode(historyMap.get(task.getId())); - historyMap.remove(task.getId()); // убирание из индекса + Node existingNode = historyMap.remove(task.getId()); + if (existingNode != null) { + removeNode(existingNode); } - // Создание копии, чтобы история хранила снимок состояния на момент просмотра - Task copy = new Task(task.getName(), task.getDescription(), task.getStatus()); - copy.setId(task.getId()); - // Добавление копии в конец списка - linkLast(copy); + linkLast(task); } // Удаление задачи из истории по id diff --git a/src/service/Node.java b/src/service/Node.java index f2a00ad..1e96c11 100644 --- a/src/service/Node.java +++ b/src/service/Node.java @@ -4,11 +4,19 @@ // Двусвязный список с самой задачей и ссылками на соседей public class Node { - Task task; - Node prev; - Node next; + private Task task; + private Node prev; + private Node next; public Node(Task task) { this.task = task; } + + public Task getTask() { return task; } + + public Node getPrev() { return prev; } + public void setPrev(Node prev) { this.prev = prev; } + + public Node getNext() { return next; } + public void setNext(Node next) { this.next = next; } } From 2517e262fde42b56a962823d5cd4b63dd0254694 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Wed, 18 Mar 2026 19:27:22 +0300 Subject: [PATCH 15/20] fix: history now stores snapshots to prevent mutation via setters --- src/service/InMemoryHistoryManager.java | 30 +++++++++++++++++++- test/service/InMemoryHistoryManagerTest.java | 27 ++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/service/InMemoryHistoryManager.java b/src/service/InMemoryHistoryManager.java index df4f6ae..f43da7d 100644 --- a/src/service/InMemoryHistoryManager.java +++ b/src/service/InMemoryHistoryManager.java @@ -1,5 +1,7 @@ package service; +import model.Epic; +import model.Subtask; import model.Task; import java.util.ArrayList; @@ -15,9 +17,35 @@ public class InMemoryHistoryManager implements HistoryManager { // В HashMap ключ = id задачи, значение = узел в списке private final HashMap historyMap = new HashMap<>(); + // Создание снимка задачи, чтобы история не зависела от внешних изменений + private Task makeSnapshot(Task task) { + if (task instanceof Subtask) { + Subtask original = (Subtask) task; + Subtask copy = new Subtask( + original.getName(), + original.getDescription(), + original.getStatus(), + original.getEpicId() + ); + copy.setId(original.getId()); + return copy; + } else if (task instanceof Epic) { + Epic original = (Epic) task; + Epic copy = new Epic(original.getName(), original.getDescription()); + copy.setId(original.getId()); + copy.setStatus(original.getStatus()); + copy.setSubtaskIds(original.getSubtaskIds()); + return copy; + } else { + Task copy = new Task(task.getName(), task.getDescription(), task.getStatus()); + copy.setId(task.getId()); + return copy; + } + } + // Добавление задачи в конец двусвязного списка private void linkLast(Task task) { - Node newNode = new Node(task); + Node newNode = new Node(makeSnapshot(task)); if (tail == null) { // Если список пустой, то полностью новый узел - и голова, и хвост head = newNode; diff --git a/test/service/InMemoryHistoryManagerTest.java b/test/service/InMemoryHistoryManagerTest.java index 51c9043..35de938 100644 --- a/test/service/InMemoryHistoryManagerTest.java +++ b/test/service/InMemoryHistoryManagerTest.java @@ -150,4 +150,31 @@ void repeatedViewShouldMoveTaskToEnd() { assertEquals(3, history.get(1).getId(), "Второй должна быть task3"); assertEquals(1, history.get(2).getId(), "Последней должна быть task1"); } + + // Пустая история должна возвращать пустой список + @Test + void emptyHistoryShouldReturnEmptyList() { + List history = historyManager.getHistory(); + assertNotNull(history, "getHistory() не должен возвращать null"); + assertEquals(0, history.size(), "Пустая история должна содержать 0 элементов"); + } + + // Статус задачи в истории не должен меняться при изменении оригинала через сеттер + @Test + void historyShouldStoreSnapshotNotReference() { + Task task = new Task("Task", "Desc", Status.NEW); + task.setId(1); + + historyManager.add(task); + + // Меняем оригинал после добавления в историю + task.setName("Изменённое имя"); + task.setStatus(Status.DONE); + + Task inHistory = historyManager.getHistory().get(0); + assertEquals("Task", inHistory.getName(), + "История должна хранить снимок: имя не должно измениться"); + assertEquals(Status.NEW, inHistory.getStatus(), + "История должна хранить снимок: статус не должен измениться"); + } } \ No newline at end of file From e3612ae8a7a1560adcf15c011939b122c9734c3f Mon Sep 17 00:00:00 2001 From: Ksenia Date: Wed, 18 Mar 2026 19:32:51 +0300 Subject: [PATCH 16/20] style: fix CheckStyle violations in Node.java --- src/service/Node.java | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/service/Node.java b/src/service/Node.java index 1e96c11..df6bb51 100644 --- a/src/service/Node.java +++ b/src/service/Node.java @@ -12,11 +12,23 @@ public Node(Task task) { this.task = task; } - public Task getTask() { return task; } + public Task getTask() { + return task; + } + + public Node getPrev() { + return prev; + } - public Node getPrev() { return prev; } - public void setPrev(Node prev) { this.prev = prev; } + public void setPrev(Node prev) { + this.prev = prev; + } - public Node getNext() { return next; } - public void setNext(Node next) { this.next = next; } -} + public Node getNext() { + return next; + } + + public void setNext(Node next) { + this.next = next; + } +} \ No newline at end of file From fe466396c0dbd567b90df2e766bd1b322ac57072 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Wed, 18 Mar 2026 19:35:08 +0300 Subject: [PATCH 17/20] style: fix CheckStyle violations in Managers.java --- src/service/Managers.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/service/Managers.java b/src/service/Managers.java index 336dc6d..52faefe 100644 --- a/src/service/Managers.java +++ b/src/service/Managers.java @@ -6,7 +6,7 @@ private Managers() { } // Реализация TaskManager - public static TaskManager getDefault () { + public static TaskManager getDefault() { return new InMemoryTaskManager(); } From 7e8811ad9e8925130fc7fcb40c64524a56ad9045 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Wed, 18 Mar 2026 19:39:06 +0300 Subject: [PATCH 18/20] style: fix CheckStyle violations in HistoryManager.java --- src/service/HistoryManager.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/service/HistoryManager.java b/src/service/HistoryManager.java index 59af633..2508140 100644 --- a/src/service/HistoryManager.java +++ b/src/service/HistoryManager.java @@ -5,6 +5,8 @@ public interface HistoryManager { void add(Task task); + void remove(int id); + List getHistory(); } From b71101eee38439014ae209c61dadb1593838e159 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Wed, 18 Mar 2026 19:40:49 +0300 Subject: [PATCH 19/20] style: fix CheckStyle violations in Main.java --- src/Main.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Main.java b/src/Main.java index 23e9d56..854abdf 100644 --- a/src/Main.java +++ b/src/Main.java @@ -9,7 +9,7 @@ public class Main { // Метод для разделительной строки - private static void printDelimiter(){ + private static void printDelimiter() { System.out.println(); System.out.println("-".repeat(30)); } From 1e00d31dc785e7e48f0b979344e537a0ed8aa533 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Thu, 19 Mar 2026 13:10:35 +0300 Subject: [PATCH 20/20] refactor: eliminate duplicate tail assignment in linkLast --- src/service/InMemoryHistoryManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/service/InMemoryHistoryManager.java b/src/service/InMemoryHistoryManager.java index f43da7d..1a4ad28 100644 --- a/src/service/InMemoryHistoryManager.java +++ b/src/service/InMemoryHistoryManager.java @@ -49,13 +49,13 @@ private void linkLast(Task task) { if (tail == null) { // Если список пустой, то полностью новый узел - и голова, и хвост head = newNode; - tail = newNode; } else { // Иначе подвешивание к хвосту tail.setNext(newNode); newNode.setPrev(tail); - tail = newNode; } + tail = newNode; + // Сохранение узла в индексе historyMap.put(task.getId(), newNode); }