Суть проекта заключается в реализации помощника по организации работы. Изначально kanban - это доска для организации работы на производстве в Японии, где на доску вывешивались задачи, поставленные мастерами, ответственными за работу, для выполнения. Доска помогает разбить задачи на более мелкие, чтоб упростить и ускорить процесс реализации. В рамках обучения, я написал доску с возможностью реализации простых CRUD
операций и отправки и получения данных с сервера, с помощью библиотек Gson
и HttpServer
. В проекте реализованы два сервера, один из которых отвечает за приём, отправку, старт и остановку работы сервера, второй - за внутреннюю реализацию процессов работы сервера. Так же, в приложении есть возможность сохранять данные локально (в файле, формата CSV с возможностью последующего считывания с файла при перезагрузке приложения) и написаны JUnit
тесты.
Пользователь не будет видеть консоль вашего приложения. Поэтому нужно сделать так, чтобы методы не просто печатали что-то в консоль, но и возвращали объекты нужных типов. Вы можете добавить консольный вывод для самопроверки в класcе Main, но на работу методов он влиять не должен.
- Java Core
- JUnit 5
- GSON
Типы задач:
-
Обычные задачи: создать задачу, обновить задачу, найти задачу по id, получить список всех задач;
-
Эпики: создать эпик, добавить подзадачу, удалить эпик с подзадачами, получить список всех подзадач эпика;
-
Подзадачи: добавить подзадачу к эпику, изменить статус подзадачи, получить эпик подзадачи;
История просмотра:
- Пользователь может посмотреть свою историю просмотра задач;
java-kanban/src/Main -> "run"
Или запуск и тестирование приложения можно найти чуть ниже в шестой части проекта.
Первая часть
Простейшим кирпичиком такой системы является задача (англ. task).
У задачи есть следующие свойства:
- Название, кратко описывающее суть задачи (например, «Переезд»).
- Описание, в котором раскрываются детали.
- Уникальный идентификационный номер задачи, по которому её можно будет найти.
- Статус, отображающий её прогресс. Мы будем выделять следующие этапы жизни задачи:
NEW
— задача только создана, но к её выполнению ещё не приступили.IN_PROGRESS
— над задачей ведётся работа.DONE
— задача выполнена.
Иногда для выполнения какой-нибудь масштабной задачи её лучше разбить на подзадачи (англ. subtask). Большую задачу, которая делится на подзадачи, мы будем называть эпиком (англ. epic).
Таким образом, в нашей системе задачи могут быть трёх типов: обычные задачи, эпики и подзадачи. Для них должны выполняться следующие условия:
- Для каждой подзадачи известно, в рамках какого эпика она выполняется.
- Каждый эпик знает, какие подзадачи в него входят.
- Завершение всех подзадач эпика считается завершением эпика.
Кроме классов для описания задач, вам нужно реализовать класс для объекта-менеджера. Он будет запускаться на старте программы и управлять всеми задачами. В нём должны быть реализованы следующие функции:
- Возможность хранить задачи всех типов. Для этого вам нужно выбрать подходящую коллекцию.
- Методы для каждого из типа задач(Задача/Эпик/Подзадача):
- Получение списка всех задач.
- Удаление всех задач.
- Получение по идентификатору.
- Создание. Сам объект должен передаваться в качестве параметра.
- Обновление. Новая версия объекта с верным идентификатором передаётся в виде параметра.
- Удаление по идентификатору.
- Дополнительные методы:
- Получение списка всех подзадач определённого эпика.
- Управление статусами осуществляется по следующему правилу:
- Менеджер сам не выбирает статус для задачи. Информация о нём приходит менеджеру вместе с информацией о самой задаче. По этим данным в одних случаях он будет сохранять статус, в других будет рассчитывать.
- Для эпиков:
- если у эпика нет подзадач или все они имеют статус
NEW
, то статус должен бытьNEW
. - если все подзадачи имеют статус
DONE
, то и эпик считается завершённым — со статусомDONE
. - во всех остальных случаях статус должен быть
IN_PROGRESS
.
- если у эпика нет подзадач или все они имеют статус
Вторая часть
Из темы об абстракции и полиморфизме вы узнали, что при проектировании кода полезно разделять требования к желаемой функциональности объектов и то, как эта функциональность реализована. То есть набор методов, который должен быть у объекта, лучше вынести в интерфейс, а реализацию этих методов – в класс, который его реализует. Теперь нужно применить этот принцип к менеджеру задач.
- Класс
TaskManager
должен стать интерфейсом. В нём нужно собрать список методов, которые должны быть у любого объекта-менеджера. Вспомогательные методы, если вы их создавали, переносить в интерфейс не нужно. - Созданный ранее класс менеджера нужно переименовать в
InMemoryTaskManager
. Именно то, что менеджер хранит всю информацию в оперативной памяти, и есть его главное свойство, позволяющее эффективно управлять задачами. Внутри класса должна остаться реализация методов. При этом важно не забыть имплементироватьTaskManager
, ведь в Java класс должен явно заявить, что он подходит под требования интерфейса.
Добавьте в программу новую функциональность — нужно, чтобы трекер отображал последние просмотренные пользователем задачи. Для этого добавьте метод getHistory()
в TaskManager
и реализуйте его — он должен возвращать последние 10 просмотренных задач. Просмотром будем считаться вызов у менеджера методов получения задачи по идентификатору — getTask()
, getSubtask()
и getEpic()
. От повторных просмотров избавляться не нужно.
Пример формирования истории просмотров задач после вызовов методов менеджера:
У метода getHistory()
не будет параметров. Это значит, он формирует свой ответ, анализируя исключительно внутреннее состояние полей объекта менеджера. Подумайте, каким образом и какие данные вы запишете в поля менеджера для возможности извлекать из них историю посещений. Так как в истории отображается, к каким задачам было обращение в методах getTask()
, getSubtask()
и getEpic()
, эти данные в полях менеджера будут обновляться при вызове этих трех методов.
Со временем в приложении трекера появится несколько реализаций интерфейса TaskManager
. Чтобы не зависеть от реализации, создайте утилитарный класс Managers
. На нём будет лежать вся ответственность за создание менеджера задач. То есть Managers
должен сам подбирать нужную реализацию TaskManager
и возвращать объект правильного типа.
У Managers
будет метод getDefault()
. При этом вызывающему неизвестен конкретный класс, только то, что объект, который возвращает getDefault()
, реализует интерфейс TaskManager
.
В этом спринте возможности трекера ограничены — в истории просмотров допускается дублирование и она может содержать только десять задач. В следующем спринте вам нужно будет убрать дубли и расширить её размер. Чтобы подготовиться к этому, проведите рефакторинг кода.
Создайте отдельный интерфейс для управления историей просмотров — HistoryManager
. У него будет два метода. Первый add(Task task)
должен помечать задачи как просмотренные, а второй getHistory()
— возвращать их список.
Объявите класс InMemoryHistoryManager
и перенесите в него часть кода для работы с историей из класса InMemoryTaskManager
. Новый класс InMemoryHistoryManager
должен реализовывать интерфейс HistoryManager
.
Добавьте в служебный класс Managers
статический метод HistoryManager
getDefaultHistory()
. Он должен возвращать объект InMemoryHistoryManager
— историю просмотров.
Проверьте, что теперь InMemoryTaskManager
обращается к менеджеру истории через интерфейс HistoryManager
и использует реализацию, которую возвращает метод getDefaultHistory()
.
Третья часть
Программа должна запоминать порядок вызовов метода add
, ведь именно в этом порядке просмотры будут выстраиваться в истории. Для хранения порядка вызовов удобно использовать список.
Если какая-либо задача просматривалась несколько раз, в истории должен отобразиться только последний просмотр. Предыдущий просмотр должен быть удалён сразу же после появления нового — за O(1)
. Из темы о списках вы узнали, что константное время выполнения операции может гарантировать связный список LinkedList
. Однако эта стандартная реализация в данном случае не подойдёт. Поэтому вам предстоит написать собственную.
CustomLinkedList
позволяет удалить элемент из произвольного места за О(1)
с одним важным условием — если программа уже дошла до этого места по списку. Чтобы выполнить условие, создайте стандартную HashMap
. Её ключом будет id
задачи, просмотр которой требуется удалить, а значением — место просмотра этой задачи в списке, то есть узел связного списка. С помощью номера задачи можно получить соответствующий ему узел связного списка и удалить его.
Реализация метода getHistory
должна перекладывать задачи из связного списка в ArrayList
для формирования ответа.
Четвертая часть
Нужно, создать класс 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
Сначала через запятую перечисляются все поля задач. Ниже находится список задач, каждая из них записана с новой строки. Дальше — пустая строка, которая отделяет задачи от истории просмотров. И заключительная строка — это идентификаторы задач из истории просмотров.
Файл из нашего примера можно прочитать так: в трекер добавлены задача, эпик и подзадача. Эпик и подзадача просмотрены и выполнены. Задача осталась в состоянии новой и не была просмотрена.
Исключения вида IOException
нужно отлавливать внутри метода save и кидать собственное непроверяемое исключение ManagerSaveException
. Благодаря этому можно не менять сигнатуру методов интерфейса менеджера.
Мы исходим из того, что наш менеджер работает в идеальных условиях.
Над ним не совершаются недопустимые операции, и все его действия со средой (например, сохранение файла) завершаются успешно.
Помимо метода сохранения создайте статический метод static FileBackedTasksManager
loadFromFile(File file)
, который будет восстанавливать данные менеджера из файла при запуске программы. Не забудьте убедиться, что новый менеджер задач работает так же, как предыдущий. И проверьте работу сохранения и восстановления менеджера из файла (сериализацию).
Пятая часть
Потребуются следующие тесты. \
- Для расчёта статуса Epic. Граничные условия:
- Пустой список подзадач.
- Все подзадачи со статусом
NEW
. - Все подзадачи со статусом
DONE
. - Подзадачи со статусами
NEW
иDONE
. - Подзадачи со статусом
IN_PROGRESS
.
- Для двух менеджеров задач
InMemoryTasksManager
иFileBackedTasksManager
. \- Чтобы избежать дублирования кода, необходим базовый класс с тестами на каждый метод из интерфейса
abstract class TaskManagerTest<T extends TaskManager>
. - Для подзадач нужно дополнительно проверить наличие эпика, а для эпика — расчёт статуса.
- Для каждого метода нужно проверить его работу:
- Со стандартным поведением.
- С пустым списком задач.
- С неверным идентификатором задачи (пустой и/или несуществующий идентификатор).
- Чтобы избежать дублирования кода, необходим базовый класс с тестами на каждый метод из интерфейса
- Для
HistoryManager
— тесты для всех методов интерфейса. Граничные условия:- Пустая история задач.
- Дублирование.
- Удаление из истории: начало, середина, конец.
- Дополнительно для FileBackedTasksManager — проверка работы по сохранению и восстановлению состояния. Граничные условия:
- Пустой список задач.
- Эпик без подзадач.
- Пустой список истории.
Добавьте новые поля в задачи:
duration
— продолжительность задачи, оценка того, сколько времени она займёт в минутах (число);startTime
— дата, когда предполагается приступить к выполнению задачи.getEndTime()
— время завершения задачи, которое рассчитывается исходя изstartTime
иduration
. Менять сигнатуры методов интерфейсаTaskManager
не понадобится: при создании или обновлении задач все его методы будут принимать и возвращать объект, в который вы добавите два новых поля. \
С классом `Epic` придётся поработать дополнительно. Продолжительность эпика — сумма продолжительности всех его подзадач. Время начала — дата старта самой ранней подзадачи, а время завершения — время окончания самой поздней из задач. Новые поля `duration` и `startTime` этого класса будут расчётные — аналогично полю статус. Для реализации `getEndTime()` удобно добавить поле `endTime` в `Epic` и рассчитать его вместе с другими полями.
Отсортируйте все задачи по приоритету — то есть по startTime
. Если дата старта не задана, добавьте задачу в конец списка задач, подзадач, отсортированных по startTime
. Напишите новый метод getPrioritizedTasks
, возвращающий список задач и подзадач в заданном порядке.
Предполагается, что пользователь будет часто запрашивать этот список задач и подзадач, поэтому подберите подходящую структуру данных для хранения. Сложность получения должна быть уменьшена с O(n log n)
до O(n)
.
Шестая часть
Вам нужно реализовать 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.
Проверить API можно несколькими способами.
- Через
Insomnia
. - С помощью плагина для браузера, к примеру, RESTED, Postman, RESTClient или других. Выбрать и скачать подходящий можно по ссылке.
- В IDEA через шаблоны HTTP-запросов — scratch file. Нажмите комбинацию
CTRL+SHIFT+ALT+Insert
и выберите HTTP Request. Доделываем HTTP-сервер для хранения задач Сейчас задачи хранятся в файлах. Нужно перенести их на сервер. Для этого напишите HTTP-клиент. С его помощью мы переместим хранение состояния менеджера из файлов на отдельный сервер. Шаблон сервера находится в репозитории — https://github.com/praktikum-java/java-core-bighw-kvserver. Склонируйте его и перенесите в проект классKVServer
. В классе Main посмотрите пример, как запустить сервер правильно. Добавьте такой же код в свой проект. В примере сервер запускается на порту 8078, если нужно, это можно изменить.
Вам нужно дописать реализацию запроса load()
— это метод, который отвечает за получение данных. Доделайте логику работы сервера по комментариям (комментарии затем можно убрать). После этого запустите сервер и проверьте, что получение значения по ключу работает. Для начальной отладки можно делать запросы без авторизации, используя код DEBUG
.
Для работы с хранилищем вам потребуется 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
— если для каждого теста требуется отдельный запуск.