Skip to content

A task manager which saves information locally and with extention to send data via server

Notifications You must be signed in to change notification settings

Chernosmaga/java-kanban

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

79 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Java Kanban: записная книжка

Идея проекта

Суть проекта заключается в реализации помощника по организации работы. Изначально kanban - это доска для организации работы на производстве в Японии, где на доску вывешивались задачи, поставленные мастерами, ответственными за работу, для выполнения. Доска помогает разбить задачи на более мелкие, чтоб упростить и ускорить процесс реализации. В рамках обучения, я написал доску с возможностью реализации простых CRUD операций и отправки и получения данных с сервера, с помощью библиотек Gson и HttpServer. В проекте реализованы два сервера, один из которых отвечает за приём, отправку, старт и остановку работы сервера, второй - за внутреннюю реализацию процессов работы сервера. Так же, в приложении есть возможность сохранять данные локально (в файле, формата CSV с возможностью последующего считывания с файла при перезагрузке приложения) и написаны JUnit тесты.

Список ТЗ проекта:

Техническое задание 3-го спринта

Как человек обычно делает покупки? Если ему нужен не один продукт, а несколько, то очень вероятно, что сначала он составит список, чтобы ничего не забыть. Сделать это можно где угодно: на листе бумаги, в приложении для заметок или, например, в сообщении самому себе в мессенджере.

А теперь представьте, что это список не продуктов, а полноценных дел. И не каких-нибудь простых вроде «помыть посуду» или «позвонить бабушке», а сложных — например, «организовать большой семейный праздник» или «купить квартиру». Каждая из таких задач может разбиваться на несколько этапов со своими нюансами и сроками. А если над их выполнением будет работать не один человек, а целая команда, то организация процесса станет ещё сложнее.

Трекер задач

Как системы контроля версий помогают команде работать с общим кодом, так и трекеры задач позволяют эффективно организовать совместную работу над задачами. Вам предстоит написать бэкенд для такого трекера. В итоге должна получиться программа, отвечающая за формирование модели данных для этой страницы:

Пользователь не будет видеть консоль вашего приложения. Поэтому нужно сделать так, чтобы методы не просто печатали что-то в консоль, но и возвращали объекты нужных типов. Вы можете добавить консольный вывод для самопроверки в классе Main, но на работу методов он влиять не должен.

Типы задач

Простейшим кирпичиком такой системы является задача (англ. tasks). У задачи есть следующие свойства:

  • Название, кратко описывающее суть задачи (например, «Переезд»).
  • Описание, в котором раскрываются детали.
  • Уникальный идентификационный номер задачи, по которому её можно будет найти.
  • Статус, отображающий её прогресс. Мы будем выделять следующие этапы жизни задачи:
    • NEW — задача только создана, но к её выполнению ещё не приступили.
    • IN_PROGRESS — над задачей ведётся работа.
    • DONE — задача выполнена.

Иногда для выполнения какой-нибудь масштабной задачи её лучше разбить на подзадачи (англ. subtask). Большую задачу, которая делится на подзадачи, мы будем называть эпиком (англ. epic).

Таким образом, в нашей системе задачи могут быть трёх типов: обычные задачи, эпики и подзадачи. Для них должны выполняться следующие условия:

  • Для каждой подзадачи известно, в рамках какого эпика она выполняется.
  • Каждый эпик знает, какие подзадачи в него входят.
  • Завершение всех подзадач эпика считается завершением эпика.

Подсказка: как организовать классы для хранения задач

У одной и той же проблемы в программировании может быть несколько решений. К примеру, вам нужно представить в программе три вида связанных сущностей: задачи, подзадачи и эпики. Вы можете завести один абстрактный класс и связать три других с ним. Или создать один не абстрактный класс и двух его наследников. Или сделать три отдельных класса. Задача программиста — не только сделать выбор, но и обосновать его. Вне зависимости от того, по какому пути вы решите пойти, каждое из этих решений будет лучше в одних ситуациях и хуже в других.

На наш взгляд, самым безопасным способом решения этой задачи будет создание публичного не абстрактного класса tasks. TaskTest. Он представляет отдельно стоящую задачу. Далее от него создать два подкласса: tasks.Subtask и tasks.Epic. Такая структура с одной стороны позволит менять свойства сразу всех видов задач, а с другой — оставит пространство для манёвров, если потребуется изменить только одну из них.

Идентификатор задачи

У каждого типа задач есть идентификатор. Это целое число, уникальное для всех типов задач. По нему мы находим, обновляем, удаляем задачи. При создании задачи менеджер присваивает ей новый идентификатор.

Для генерации идентификаторов можно использовать числовое поле класса менеджер, увеличивая его на 1, когда нужно получить новое значение.

Менеджер

Кроме классов для описания задач, вам нужно реализовать класс для объекта-менеджера. Он будет запускаться на старте программы и управлять всеми задачами. В нём должны быть реализованы следующие функции:

  • Возможность хранить задачи всех типов. Для этого вам нужно выбрать подходящую коллекцию.
  • Методы для каждого из типа задач(Задача/Эпик/Подзадача):
    • Получение списка всех задач.
    • Удаление всех задач.
    • Получение по идентификатору.
    • Создание. Сам объект должен передаваться в качестве параметра.
    • Обновление. Новая версия объекта с верным идентификатором передаётся в виде параметра.
    • Удаление по идентификатору.
  • Дополнительные методы:
    • Получение списка всех подзадач определённого эпика.
  • Управление статусами осуществляется по следующему правилу:
    • Менеджер сам не выбирает статус для задачи. Информация о нём приходит менеджеру вместе с информацией о самой задаче. По этим данным в одних случаях он будет сохранять статус, в других будет рассчитывать.
    • Для эпиков:
      • если у эпика нет подзадач или все они имеют статус NEW, то статус должен быть NEW.
      • если все подзадачи имеют статус DONE, то и эпик считается завершённым — со статусом DONE.
      • во всех остальных случаях статус должен быть IN_PROGRESS.

Подсказки

Хранение задач

Итак, вам нужно:

  1. Получать задачи по идентификатору.
  2. Выводить списки задач разных типов.

Один из способов организовать такое хранение — это присвоить соответствие между идентификатором и задачей при помощи HashMap. Поскольку идентификатор не может повторяться (иначе он не был бы идентификатором), такой подход позволит быстро получать задачу. Чтобы получать разные типы задач, вы можете создать три HashMap по одной на каждый из видов задач.

Обновление данных

При обновлении можете считать, что на вход подаётся новый объект, который должен полностью заменить старый. К примеру, метод для обновления эпика может принимать эпик в качестве входных данных public void updateTask(tasks.TaskTest tasks). Если вы храните эпики в HashMap, где ключами являются идентификаторы, то обновление — это запись нового эпика tasks.put(tasks.getId(), tasks)).

Обновление статуса задачи

Фраза «информация приходит вместе с информацией по задаче» означает, что не существует отдельного метода, который занимался бы только обновлением статуса задачи. Вместо этого статус задачи обновляется вместе с полным обновлением задачи.

Обновление эпиков

Из описания задачи видно, что эпик не управляет своим статусом самостоятельно. Это значит:

  1. Пользователь не должен иметь возможности поменять статус эпика самостоятельно.
  2. Когда меняется статус любой подзадачи в эпике, вам необходимо проверить, что статус эпика изменится соответствующим образом. При этом изменение статуса эпика может и не произойти, если в нём, к примеру, всё ещё есть незакрытые задачи.

И ещё кое-что...

  1. Проверка кода называется тестированием. Мы будем подробно рассказывать об этом дальше в курсе. Тем не менее сам процесс тестирования можно начать уже сейчас. Создайте в классе Main метод static void main(String[] args) и внутри него:
    • Создайте 2 задачи, один эпик с 2 подзадачами, а другой эпик с 1 подзадачей.
    • Распечатайте списки эпиков, задач и подзадач, через System.out.println(..)
    • Измените статусы созданных объектов, распечатайте. Проверьте, что статус задачи и подзадачи сохранился, а статус эпика рассчитался по статусам подзадач.
    • И, наконец, попробуйте удалить одну из задач и один из эпиков. Воспользуйтесь дебаггером, поставляемым вместе со средой разработки, что бы понять логику работы программы и отладить.
  2. Не оставляйте в коде мусор — превращённые в комментарии или ненужные куски кода. Это сквозной проект, на его основе вы будете делать несколько следующих домашних заданий.
  3. Давайте коммитам осмысленные комментарии: порядок в репозитории и коде — ключ к успеху написания хороших программ.
Техническое задание 4-го спринта

Менеджер теперь интерфейс

Из темы об абстракции и полиморфизме вы узнали, что при проектировании кода полезно разделять требования к желаемой функциональности объектов и то, как эта функциональность реализована. То есть набор методов, который должен быть у объекта, лучше вынести в интерфейс, а реализацию этих методов – в класс, который его реализует. Теперь нужно применить этот принцип к менеджеру задач.

  • Класс TaskManager должен стать интерфейсом. В нём нужно собрать список методов, которые должны быть у любого объекта-менеджера. Вспомогательные методы, если вы их создавали, переносить в интерфейс не нужно.
  • Созданный ранее класс менеджера нужно переименовать в InMemoryTaskManager. Именно то, что менеджер хранит всю информацию в оперативной памяти, и есть его главное свойство, позволяющее эффективно управлять задачами. Внутри класса должна остаться реализация методов. При этом важно не забыть имплементировать TaskManager, ведь в Java класс должен явно заявить, что он подходит под требования интерфейса.

Подсказки

Как быстро сделать TaskManager интерфейсом

Достаточно просто убрать у всех методов блок реализации, а ключевое слово class заменить на interface.

Что делать с новым классом InMemoryTaskManager

В InMemoryTaskManager нужно скопировать бывшее содержимое класса TaskManager. Чтобы класс реализовывал интерфейс, необходимо после его названия указать ключевое слово implements и имя интерфейса — class InMemoryTaskManager implements TaskManager. Перед реализацией методов интерфейса нужна аннотация @Override.

История просмотров задач

Добавьте в программу новую функциональность — нужно, чтобы трекер отображал последние просмотренные пользователем задачи. Для этого добавьте метод getHistory() в TaskManager и реализуйте его — он должен возвращать последние 10 просмотренных задач. Просмотром будем считаться вызов у менеджера методов получения задачи по идентификатору — getTask(), getSubtask() и getEpic(). От повторных просмотров избавляться не нужно. У метода getHistory() не будет параметров. Это значит, он формирует свой ответ, анализируя исключительно внутреннее состояние полей объекта менеджера. Подумайте, каким образом и какие данные вы запишете в поля менеджера для возможности извлекать из них историю посещений. Так как в истории отображается, к каким задачам было обращение в методах getTask(), getSubtask() и getEpic(), эти данные в полях менеджера будут обновляться при вызове этих трех методов.

Обратите внимание, что просмотрен может быть любой тип задачи. То есть возвращаемый список задач может содержать объект одного из трех типов на любой своей позиции. Чтобы описать ячейку такого списка, нужно вспомнить о полиморфизме и выбрать тип, являющийся общим родителем обоих классов.

Подсказки

Как отобразить в коде историю просмотров

История просмотров задач — это упорядоченный набор элементов, для хранения которых отлично подойдёт список. При создании менеджера заведите список для хранения просмотренных задач. Этот список должен обновляться в методах getSubtask() и getEpic() — просмотренные задачи должны добавляться в конец.

Обновление истории просмотров

Учитывайте, что размер списка для хранения просмотров не должен превышать десяти элементов. Если размер списка исчерпан, из него нужно удалить самый старый элемент — тот который находится в начале списка.

Где пригодится тип списка просмотренных задач

Для списка просмотренных задач нужен тип TaskTest. Метод getHistory() должен возвращать список именно такого типа. В итоге он будет выглядеть так — List<TaskTest> getHistory().

Утилитарный класс

Со временем в приложении трекера появится несколько реализаций интерфейса TaskManager. Чтобы не зависеть от реализации, создайте утилитарный класс Managers. На нём будет лежать вся ответственность за создание менеджера задач. То есть Managers должен сам подбирать нужную реализацию TaskManager и возвращать объект правильного типа. У Managers будет метод getDefault(). При этом вызывающему неизвестен конкретный класс, только то, что объект, который возвращает getDefault(), реализует интерфейс TaskManager.

Подсказка про getDefault()

Метод getDefault() будет без параметров. Он должен возвращать объект-менеджер, поэтому типом его возвращаемого значения будет TaskManager.

Статусы задач как перечисление

Так как варианты возможных статусов у задачи ограничены, для их хранения в программе лучше завести перечисляемый тип enum. Ранее мы использовали для хранения статусов задач тип String — теперь три соответствующих поля в классе нужно объединить в enum с тремя значениями. Не забудьте, что все элементы перечисления принято писать как константы: в верхнем регистре.

Тестирование вашего решения

Убедитесь, что ваше решение работает! В главном классе воспроизведите несложный пользовательский сценарий:

  • создайте несколько задач разного типа.
  • вызовите разные методы интерфейса TaskManager и напечатайте историю просмотров после каждого вызова. Если код рабочий, то история просмотров задач будет отображаться корректно.

Сделайте историю задач интерфейсом

В этом спринте возможности трекера ограничены — в истории просмотров допускается дублирование и она может содержать только десять задач. В следующем спринте вам нужно будет убрать дубли и расширить её размер. Чтобы подготовиться к этому, проведите рефакторинг кода. Создайте отдельный интерфейс для управления историей просмотров — HistoryManager. У него будет два метода. Первый add(TaskTest tasks) должен помечать задачи как просмотренные, а второй getHistory() — возвращать их список. Объявите класс InMemoryHistoryManager и перенесите в него часть кода для работы с историей из класса InMemoryTaskManager. Новый класс InMemoryHistoryManager должен реализовывать интерфейс HistoryManager. Добавьте в служебный класс Managers статический метод HistoryManager getDefaultHistory(). Он должен возвращать объект InMemoryHistoryManager — историю просмотров. Проверьте, что теперь InMemoryTaskManager обращается к менеджеру истории через интерфейс HistoryManager и использует реализацию, которую возвращает метод getDefaultHistory().

Техническое задание 5-го спринта

Обратите внимание

Недостаточно реализовать код таким образом, чтобы программа пробегалась по всей истории просмотров и только после этого удаляла предыдущий просмотр. Ведь тогда время работы этой программы будет линейно зависеть от длины истории. Ваша цель — реализовать функциональность так, чтобы время просмотра задачи никак не зависело от общего количества задач в истории.

Интерфейс HistoryManager

У нас уже есть интерфейс, осталось добавить метод void remove(int id) для удаления задачи из просмотра. И реализовать его в классе InMemoryHistoryManager. Добавьте его вызов при удалении задач, чтобы они также удалялись из истории просмотров.

Подсказка: структура интерфейса HistoryManager

Интерфейс HistoryManager будет иметь следующую структуру.

public interface HistoryManager {
    void add(TaskTest tasks);
    void remove(int id);
    List<TaskTest> getHistory();
}

Дальнейшая разработка алгоритма с CustomLinkedList и HashMap

Программа должна запоминать порядок вызовов метода add, ведь именно в этом порядке просмотры будут выстраиваться в истории. Для хранения порядка вызовов удобно использовать список.

Если какая-либо задача просматривалась несколько раз, в истории должен отобразиться только последний просмотр. Предыдущий просмотр должен быть удалён сразу же после появления нового — за O(1). Из темы о списках вы узнали, что константное время выполнения операции может гарантировать связный список LinkedList. Однако эта стандартная реализация в данном случае не подойдёт. Поэтому вам предстоит написать собственную.

CustomLinkedList позволяет удалить элемент из произвольного места за О(1) с одним важным условием — если программа уже дошла до этого места по списку. Чтобы выполнить условие, создайте стандартную HashMap. Её ключом будет id задачи, просмотр которой требуется удалить, а значением — место просмотра этой задачи в списке, то есть узел связного списка. С помощью номера задачи можно получить соответствующий ему узел связного списка и удалить его.

Реализация метода getHistory должна перекладывать задачи из связного списка в ArrayList для формирования ответа.

Подсказки

Про CustomLinkedList

Сначала напишите свою реализацию двусвязного списка задач с методами linkLast и getTasks. linkLast будет добавлять задачу в конец этого списка, а getTasks собирать все задачи из него в обычный ArrayList. Убедитесь, что решение работает. Отдельный класс для списка создавать не нужно — реализуйте его прямо в классе InMemoryHistoryManager. А вот отдельный класс Node для узла списка необходимо добавить.

Про метод removeNode

Добавьте метод removeNode в класс. В качестве параметра этот метод должен принимать объект Node — узел связного списка и вырезать его.

Про HashMap

Создайте HashMap — будет достаточно её стандартной реализации. В ключах будут храниться id задач, а в значениях — узлы связного списка. Изначально HashMap пустая. Она будет заполняться по мере добавления новых задач. Напишите реализацию метода add(TaskTest tasks). Теперь с помощью HashMap и метода удаления removeNode метод add(TaskTest tasks) будет быстро удалять задачу из списка, если она там есть, а затем вставлять её в конец двусвязного списка. После добавления задачи не забудьте обновить значение узла в HashMap.

Техническое задание 6-го спринта

В этом спринте вы добавите в трекер задач ещё одну полезную опцию. Текущая реализация хранит состояние менеджера в оперативной памяти, из-за этого после перезапуска приложения все нужные нам данные теряются. Решить эту проблему может такой класс менеджера, который будет после каждой операции автоматически сохранять все задачи и их состояние в специальный файл.

Вам предстоит создать вторую реализацию менеджера. У него будет такая же система классов и интерфейсов, как и у нынешнего. Новый и старый менеджеры будут отличаться только деталями реализации методов: один хранит информацию в оперативной памяти, другой — в файле.

Вторая реализация менеджера

Итак, создайте класс FileBackedTasksManager. В нём вы будете прописывать логику автосохранения в файл. Этот класс, как и InMemoryTasksManager, должен имплементировать интерфейс менеджера TasksManager.

Теперь нужно написать реализацию для нового класса. Если у вас появится желание просто скопировать код из InMemoryTasksManager и дополнить его в нужных местах функцией сохранения в файл, остановитесь! Старайтесь избегать дублирования кода, это признак плохого стиля.

В данном случае есть более изящное решение: можно наследовать FileBackedTasksManager от InMemoryTasksManager и получить от класса-родителя желаемую логику работы менеджера. Останется только дописать в некоторых местах вызовы метода автосохранения.

Метод автосохранения

Пусть новый менеджер получает файл для автосохранения в своём конструкторе и сохраняет его в поле. Создайте метод save без параметров — он будет сохранять текущее состояние менеджера в указанный файл.

Теперь достаточно переопределить каждую модифицирующую операцию таким образом, чтобы сначала выполнялась версия, унаследованная от предка, а затем — метод save. Например:

@Override
public void addSubtask(Subtask subtask) {
    super.addSubtask(subtask);
    save();
}

Затем нужно продумать логику метода save. Что он должен сохранять? Все задачи, подзадачи, эпики и историю просмотра любых задач. Для удобства работы рекомендуем выбрать текстовый формат CSV (англ. Comma-Separated Values, «значения, разделённые запятыми»). Тогда файл с сохранёнными данными будет выглядеть так:

id,type,name,status,description,epic
1,TASK,Task1,NEW,Description task1,
2,EPIC,Epic2,DONE,Description epic2,
3,SUBTASK,Sub Task2,DONE,Description sub task3,2

2,3 

Сначала через запятую перечисляются все поля задач. Ниже находится список задач, каждая из них записана с новой строки. Дальше — пустая строка, которая отделяет задачи от истории просмотров. И заключительная строка — это идентификаторы задач из истории просмотров.

Файл из нашего примера можно прочитать так: в трекер добавлены задача, эпик и подзадача. Эпик и подзадача просмотрены и выполнены. Задача осталась в состоянии новой и не была просмотрена.

Как сохранять задачи в файл и считывать их из него

  • Создайте enum с типами задач.
  • Напишите метод сохранения задачи в строку String toString(TaskTest task) или переопределите базовый.
  • Напишите метод создания задачи из строки TaskTest fromString(String value).
  • Напишите статические методы static String historyToString(HistoryManager manager) и static List<Integer> historyFromString(String value) для сохранения и восстановления менеджера истории из CSV.

Как прочитать файл

В Java есть несколько способов чтения файлов. Вы можете использовать такой:

Files.readString(Path.of(path)); 

Проверка работы нового менеджера

Исключения вида IOException нужно отлавливать внутри метода save и кидать собственное непроверяемое исключение ManagerSaveException. Благодаря этому можно не менять сигнатуру методов интерфейса менеджера.

Мы исходим из того, что наш менеджер работает в идеальных условиях. Над ним не совершаются недопустимые операции, и все его действия со средой (например, сохранение файла) завершаются успешно.

Помимо метода сохранения создайте статический метод static FileBackedTasksManager loadFromFile(File file), который будет восстанавливать данные менеджера из файла при запуске программы. Не забудьте убедиться, что новый менеджер задач работает так же, как предыдущий. И проверьте работу сохранения и восстановления менеджера из файла (сериализацию).

Техническое задание 7-го спринта

Покройте код тестами

Ваша цель — написать отдельный тест для каждого публичного метода: стандартный кейс его работы и граничные случаи.

Потребуются следующие тесты:

  1. Для расчёта статуса Epic. Граничные условия:
    • Пустой список подзадач.
    • Все подзадачи со статусом NEW.
    • Все подзадачи со статусом DONE.
    • Подзадачи со статусами NEW и DONE.
    • Подзадачи со статусом IN_PROGRESS.
  2. Для двух менеджеров задач InMemoryTasksManager и FileBackedTasksManager.
    • Чтобы избежать дублирования кода, необходим базовый класс с тестами на каждый метод из интерфейса abstract class TaskManagerTest<T extends TaskManager>.
    • Для подзадач нужно дополнительно проверить наличие эпика, а для эпика — расчёт статуса.
    • Для каждого метода нужно проверить его работу:
      • Со стандартным поведением.
      • С пустым списком задач.
      • С неверным идентификатором задачи (пустой и/или несуществующий идентификатор).
  3. Для HistoryManager — тесты для всех методов интерфейса. Граничные условия:
    • Пустая история задач.
    • Дублирование.
    • Удаление из истории: начало, середина, конец.
  4. Дополнительно для FileBackedTasksManager — проверка работы по сохранению и восстановлению состояния. Граничные условия:
    • Пустой список задач.
    • Эпик без подзадач.
    • Пустой список истории.

После написания тестов ещё раз проверьте их наличие по списку. Убедитесь, что они работают.

Подсказка: примеры тестов

Тест создания задачи.

@Test
    void addNewTask() {
        Task task = new Task("Test addNewTask", "Test addNewTask description", NEW);
        final int taskId = taskManager.addNewTask(task);

        final Task savedTask = taskManager.getTask(taskId);

        assertNotNull(savedTask, "Задача не найдена.");
        assertEquals(task, savedTask, "Задачи не совпадают.");

        final List<Task> tasks = taskManager.getTasks();

        assertNotNull(tasks, "Задачи на возвращаются.");
        assertEquals(1, tasks.size(), "Неверное количество задач.");
        assertEquals(task, tasks.get(0), "Задачи не совпадают.");
    } 

Тест добавления в историю.

@Test
    void add() {
        historyManager.add(task);
        final List<Task> history = historyManager.getHistory();
        assertNotNull(history, "История не пустая.");
        assertEquals(1, history.size(), "История не пустая.");
    } 

Добавьте продолжительность и дату старта

Добавьте новые поля в задачи:

  • duration — продолжительность задачи, оценка того, сколько времени она займёт в минутах (число);
  • startTime — дата, когда предполагается приступить к выполнению задачи.
  • getEndTime() — время завершения задачи, которое рассчитывается исходя из startTime и duration.

Менять сигнатуры методов интерфейса TaskManager не понадобится: при создании или обновлении задач все его методы будут принимать и возвращать объект, в который вы добавите два новых поля.

С классом Epic придётся поработать дополнительно. Продолжительность эпика — сумма продолжительности всех его подзадач. Время начала — дата старта самой ранней подзадачи, а время завершения — время окончания самой поздней из задач. Новые поля duration и startTime этого класса будут расчётные — аналогично полю статус. Для реализации getEndTime() удобно добавить поле endTime в Epic и рассчитать его вместе с другими полями.

Не забудьте также доработать опцию сохранения состояния в файл: добавьте в сериализацию новые поля.

Добавьте в тесты проверку новых полей.

Выведите список задач в порядке приоритета

Отсортируйте все задачи по приоритету — то есть по startTime. Если дата старта не задана, добавьте задачу в конец списка задач, подзадач, отсортированных по startTime. Напишите новый метод getPrioritizedTasks, возвращающий список задач и подзадач в заданном порядке.

Предполагается, что пользователь будет часто запрашивать этот список задач и подзадач, поэтому подберите подходящую структуру данных для хранения. Сложность получения должна быть уменьшена с O(n log n) до O(n).

Подсказка: как ускорить сортировку

Если сортировать список заново каждый раз, сложность получения будет O(n log n). Можно хранить все задачи заранее отсортированными с помощью класса TreeSet.

Проверьте пересечения

Предполагается, что пользователь будет выполнять не более одной задачи за раз. Научите трекер проверять, что задачи и подзадачи не пересекаются по времени выполнения. Добавьте валидацию во время создания или изменения задач, подзадач.

Подсказка: как искать пересечения за O(n)

getPrioritizedTasks возвращает отсортированный список задач. По нему можно пройтись за O(n) и проверить все задачи на пересечение.

Дополнительное задание

А теперь необязательное задание для тех, кто хочет бросить себе вызов! Подумайте, какая структура данных и какой алгоритм проверки подойдут, чтобы уменьшить сложность поиска пересечений до O(1).

Подсказка: как искать пересечения за O(1)

Пусть все задачи располагаются на сетке с интервалами в 15 минут, а планирование возможно только на год вперёд. В этом случае можно заранее заполнить таблицу, где ключ — это интервал, а значение — объект boolean (свободно время или нет). В итоге для эффективного поиска пересечений достаточно будет проверить, что свободны все 15-минутные интервалы задачи.

Техническое задание 8-го спринта

Возвращаемся к работе над менеджером задач. Основная логика приложения реализована, теперь можно сделать для него API. Вам предстоит настроить доступ к методам менеджера через HTTP-запросы.

Прорабатываем логику API

Вам нужно реализовать API, где эндпоинты будут соответствовать вызовам базовых методов интерфейса TaskManager. Соответствие эндпоинтов и методов называется маппингом.

Сначала добавьте в проект библиотеку Gson для работы с JSON. Далее создайте класс HttpTaskServer, который будет слушать порт 8080 и принимать запросы. Добавьте в него реализацию FileBackedTaskManager, которую можно получить из утилитного класса Managers. После этого можно реализовать маппинг запросов на методы интерфейса TaskManager.

API должен работать так, чтобы все запросы по пути /tasks/<ресурсы> приходили в интерфейс TaskManager. Путь для обычных задач — /tasks/task, для подзадач — /tasks/subtask, для эпиков — /tasks/epic. Получить все задачи сразу можно будет по пути /tasks/, а получить историю задач по пути /tasks/history.

Для получения данных должны быть GET-запросы. Для создания и изменения — POST-запросы. Для удаления — DELETE-запросы. Задачи передаются в теле запроса в формате JSON. Идентификатор (id) задачи следует передавать параметром запроса (через вопросительный знак).

В результате для каждого метода интерфейса TaskManager должен быть создан отдельный эндпоинт, который можно будет вызвать по HTTP.

Подсказка: как получить все задачи

HttpClient client = HttpClient.newHttpClient();
URI url = URI.create("http://localhost:8080/tasks/task/");
HttpRequest request = HttpRequest.newBuilder().uri(url).GET().build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

Подсказка: как создать задачу

URI url = URI.create("http://localhost:8080/tasks/task/");
Gson gson = new Gson();
String json = gson.toJson(newTask);
final HttpRequest.BodyPublisher body = HttpRequest.BodyPublishers.ofString(json);
HttpRequest request = HttpRequest.newBuilder().uri(url).POST(body).build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

Подсказка: как получить задачу с id = 1

HttpClient client = HttpClient.newHttpClient();
URI url = URI.create("http://localhost:8080/tasks/task/?id=1");
HttpRequest request = HttpRequest.newBuilder().uri(url).GET().build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); 

Как проверить эндпоинты

  • Через Insomnia.
  • С помощью плагина для браузера, к примеру, RESTED, Postman, RESTClient или других.
  • В IDEA через шаблоны HTTP-запросов — scratch file. Нажмите комбинацию CTRL+SHIFT+ALT+Insert и выберите HTTP Request.

Доделываем HTTP-сервер для хранения задач

Сейчас задачи хранятся в файлах. Нужно перенести их на сервер. Для этого напишите HTTP-клиент. С его помощью мы переместим хранение состояния менеджера из файлов на отдельный сервер. Шаблон сервера находится в этом репозитории. Склонируйте его и перенесите в проект класс KVServer. В классе Main посмотрите пример, как запустить сервер правильно. Добавьте такой же код в свой проект. В примере сервер запускается на порту 8078, если нужно, это можно изменить.

Подсказка: как работает сервер KVServer

KVServer — это хранилище, где данные хранятся по принципу <ключ-значение>. Он умеет:

  • GET /register — регистрировать клиента и выдавать уникальный токен доступа (аутентификации). Это нужно, чтобы хранилище могло работать сразу с несколькими клиентами.
  • POST /save/<ключ>?API_TOKEN= — сохранять содержимое тела запроса, привязанное к ключу.
  • GET /load/<ключ>?API_TOKEN= — возвращать сохранённые значение по ключу.

Вам нужно дописать реализацию запроса load() — это метод, который отвечает за получение данных. Доделайте логику работы сервера по комментариям (комментарии затем можно убрать). После этого запустите сервер и проверьте, что получение значения по ключу работает. Для начальной отладки можно делать запросы без авторизации, используя код DEBUG.

Пишем HTTP-клиент

Для работы с хранилищем вам потребуется HTTP-клиент, который будет делегировать вызовы методов в HTTP-запросы. Создайте класс KVTaskClient. Его будет использовать класс HttpTaskManager, который мы скоро напишем.

При создании KVTaskClient учтите следующее:

  • Конструктор принимает URL к серверу хранилища и регистрируется. При регистрации выдаётся токен (API_TOKEN), который нужен при работе с сервером.
  • Метод void put(String key, String json) должен сохранять состояние менеджера задач через запрос POST /save/<ключ>?API_TOKEN=.
  • Метод String load(String key) должен возвращать состояние менеджера задач через запрос GET /load/<ключ>?API_TOKEN=.

Далее проверьте код клиента в main. Для этого запустите KVServer, создайте экземпляр KVTaskClient. Затем сохраните значение под разными ключами и проверьте, что при запросе возвращаются нужные данные. Удостоверьтесь, что если изменить значение, то при повторном вызове вернётся уже не старое, а новое.

Новая реализация менеджера задач

Теперь можно создать новую реализацию интерфейса TaskManager — класс HttpTaskManager. Он будет наследовать от FileBackedTasksManager.

Конструктор HttpTaskManager должен будет вместо имени файла принимать URL к серверу KVServer. Также HttpTaskManager создаёт KVTaskClient, из которого можно получить исходное состояние менеджера. Вам нужно заменить вызовы сохранения состояния в файлах на вызов клиента.

В конце обновите статический метод getDefault() в утилитарном классе Managers, чтобы он возвращал HttpTaskManager.

Тестирование

Код проверки в Main.main перестал работать. Это произошло, потому что Managers.getDefault() теперь возвращает новую реализацию менеджера задач, а она не может работать без запуска сервера. Вам нужно это исправить.

Добавьте запуск KVServer в Main.main и перезапустите пример использования менеджера. Убедитесь, что всё работает и состояние задач теперь хранится на сервере.

Теперь можно добавить тесты для HttpTaskManager аналогично тому как сделали для FileBackedTasksManager, отличие только, вместо проверки восстановления состояния менеджера из файла, данные будут восстанавливаться с KVServer сервера.

Напишите тесты для каждого эндпоинта HttpTaskServer. Чтобы каждый раз не добавлять запуск KVServer и HttpTaskServer серверов, можно реализовать в классах с тестами отдельный метод. Пометьте его аннотацией @BeforeAll — если предполагается запуск серверов для всех тестов или аннотацией @BeforeEach — если для каждого теста требуется отдельный запуск.

Подсказка: как остановить KVServer

Если запускать новый сервер перед каждым тестом на том же порту, то потребуется остановить предыдущий. Для этого реализуйте метод stop() в KVServer. Его вызов поместите в отдельный метод в тестах. Пометьте его аннотацией @AfterEach.

About

A task manager which saves information locally and with extention to send data via server

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages