diff --git a/CHANGELOG.md b/CHANGELOG.md index 0089a5f..729f773 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,32 @@ -Все заметные изменения проекта будут документироваться в этом файле. +# Журнал изменений +Все заметные изменения проекта будут документироваться в этом файле. Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.0.0/), и этот проект придерживается [Semantic Versioning](https://semver.org/spec/v2.0.0.html) +## [2.2.0] - 2025-06-08 + +### Добавлено +- Универсальный интерфейс `Identifiable` для всех сущностей с ID +- Универсальный класс `EntityService` для стандартных CRUD-операций +- Универсальный интерфейс `Repository` для базовых операций с данными +- Утилитарный класс `Formatter` с методом `asTable()` для форматирования таблиц + +### Изменено +- **[BREAKING]** Обобщение CLI-команд для поддержки разных типов сущностей через `EntityService` +- Рефакторинг `TaskService` для расширения `EntityService` вместо собственной реализации +- Переименование `InMemoryTaskRepository` → `InMemoryRepository` с поддержкой generics +- Унификация работы с моделью `Task` через интерфейс `Identifiable` +- Вынесение логики форматирования таблиц из `ListCommand` в отдельный utility-класс +- Обновление `ListCommand` и `SearchCommand` для использования `Formatter.asTable()` +- Замена метода `list()` на `getAll()` в сервисном слое + +### Технические улучшения +- Улучшена типобезопасность через использование generics +- Повышена переиспользуемость кода за счёт вынесения общей логики +- Упрощена архитектура через унификацию интерфейсов +- Удалено дублирование кода форматирования между командами + ## [2.1.0] - 2025-06-01 ### Добавлено @@ -12,13 +36,12 @@ ### Изменено - Обновлены внутренние зависимости между компонентами CLI (`SearchCommand`, `ListCommand`) и моделью данных (`Task`, `SimpleTask`) -- Добавлена обработка минимальной длины поискового запроса (3 символа) в `SearchCommand`# Журнал изменений - +- Добавлена обработка минимальной длины поискового запроса (3 символа) в `SearchCommand` ## [2.0.0] - 2025-05-29 ### Добавлено - Базовая функциональность для управления задачами -- Команды: create, list, delete +- Команды: create, list, delete, complete - Консольный интерфейс для взаимодействия с приложением - In-memory хранилище задач \ No newline at end of file diff --git a/README.md b/README.md index b771189..2e289d8 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,92 @@ # TodoApp ## Описание -TodoApp - это консольное приложение для управления списком задач, разработанное на Java в качестве учебного проекта. Приложение позволяет создавать, просматривать, отмечать как выполненные и удалять задачи через интерфейс командной строки. + +TodoApp - это консольное приложение для управления списком задач, разработанное на Java в качестве учебного проекта. Приложение построено на универсальной архитектуре с использованием generics, что позволяет легко расширять функционал для работы с различными типами сущностей. + + +### Особенности +- Консольный интерфейс с интуитивными командами +- Универсальная архитектура на базе `EntityService` и `Repository` +- Поддержка поиска задач по тексту +- Красивое табличное отображение данных +- Типобезопасность через использование generics + ## Требования + - Java 24 или выше - Maven 3.8.x или выше + ## Установка и запуск + ```bash cd ./todo mvn clean package java -jar ./target/todo.jar ``` + +## Использование + ### Доступные команды - `create <название_задачи>` - Создать новую задачу -- `list` - Показать список всех задач +- `list` - Показать список всех задач в табличном виде - `complete ` - Отметить задачу как выполненную -- `delete ` - Удалить задачу -- `search <текст>` - Найти задачи по тексту +- `delete ` - Удалить задачу по ID +- `search <текст>` - Найти задачи по тексту (минимум 3 символа) +- `help` - Показать список всех команд - `exit` - Выйти из приложения -## Структура проекта -- `model` - Модели данных (Task, SimpleTask) -- `repo` - Репозитории для хранения задач -- `service` - Бизнес-логика -- `cli` - Компоненты интерфейса командной строки + +## Пример использования + +```shell +todo> create Изучить Java generics +Задача "Изучить Java generics" успешно добавлена! + +todo> create Написать документацию +Задача "Написать документацию" успешно добавлена! + +todo> list + +ID │ Название задачи │ Статус +-------------------------------------------------- +1 │ Изучить Java generics │ В процессе +2 │ Написать документацию │ В процессе + + +todo> complete 1 + +Задача ID-1 выполнена. + +todo> search Java + +ID │ Название задачи │ Статус +-------------------------------------------------- +1 │ Изучить Java generics │ Выполнена + +``` ## Версии -Актуальная версия: 2.1 + +Актуальная версия: 2.2.0 + +### История изменений +- 2.2.0 - Универсальная архитектура с generics, утилиты форматирования +- 2.1.0 - Добавлена команда поиска, улучшено отображение +- 2.0.0 - Базовый функционал управления задачами + +Подробная история изменений доступна в CHANGELOG.md + +### Разработка +Проект использует современные подходы Java-разработки: +- Generic programming для типобезопасности +- Разделение ответственности через слоистую архитектуру +- Command pattern для CLI-команд +- Repository pattern для работы с данными ## Лицензия -[MIT](LICENSE) \ No newline at end of file + +MIT \ No newline at end of file diff --git a/todo/pom.xml b/todo/pom.xml index 3058f16..b17d35f 100644 --- a/todo/pom.xml +++ b/todo/pom.xml @@ -6,7 +6,7 @@ ru.kamask.pet todo - 2.1 + 2.2.0 24 diff --git a/todo/src/main/java/ru/kamask/pet/todo/TodoApp.java b/todo/src/main/java/ru/kamask/pet/todo/TodoApp.java index 232ccbb..8bbd7c2 100644 --- a/todo/src/main/java/ru/kamask/pet/todo/TodoApp.java +++ b/todo/src/main/java/ru/kamask/pet/todo/TodoApp.java @@ -3,12 +3,13 @@ package ru.kamask.pet.todo; import java.io.IOException; import ru.kamask.pet.todo.cli.CliEngine; -import ru.kamask.pet.todo.repo.InMemoryTaskRepository; +import ru.kamask.pet.todo.model.SimpleTask; +import ru.kamask.pet.todo.repo.InMemoryRepository; import ru.kamask.pet.todo.service.TaskService; public class TodoApp { public static void main(String[] args) throws IOException { - var service = new TaskService(new InMemoryTaskRepository()); + var service = new TaskService(new InMemoryRepository()); var cli = new CliEngine(service); cli.start(); diff --git a/todo/src/main/java/ru/kamask/pet/todo/cli/CliEngine.java b/todo/src/main/java/ru/kamask/pet/todo/cli/CliEngine.java index f345398..f11450b 100644 --- a/todo/src/main/java/ru/kamask/pet/todo/cli/CliEngine.java +++ b/todo/src/main/java/ru/kamask/pet/todo/cli/CliEngine.java @@ -7,21 +7,22 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Optional; -import ru.kamask.pet.todo.service.TaskService; +import ru.kamask.pet.todo.model.Identifiable; +import ru.kamask.pet.todo.service.EntityService; public class CliEngine { private HashMap registry = new HashMap<>(); - private TaskService service; + private EntityService service; private BufferedReader reader; - public CliEngine(TaskService service) { + public CliEngine(EntityService service) { this.service = service; reader = new BufferedReader(new InputStreamReader(System.in)); initializeCommands(); } public void start() throws IOException { - System.out.println("\nДобро пожаловать в Список задач 2.1!"); + System.out.println("\nДобро пожаловать в Список задач 2.2!"); System.out.println("Введите help для просмтора доступных команд или exit для выхода."); while (true) { System.out.print("\ntodo> "); diff --git a/todo/src/main/java/ru/kamask/pet/todo/cli/Command.java b/todo/src/main/java/ru/kamask/pet/todo/cli/Command.java index 52dde08..4587b73 100644 --- a/todo/src/main/java/ru/kamask/pet/todo/cli/Command.java +++ b/todo/src/main/java/ru/kamask/pet/todo/cli/Command.java @@ -2,13 +2,14 @@ package ru.kamask.pet.todo.cli; import java.util.Optional; -import ru.kamask.pet.todo.service.TaskService; +import ru.kamask.pet.todo.model.Identifiable; +import ru.kamask.pet.todo.service.EntityService; public interface Command { String templateUsage = " %-30s // %s"; String errorMessage = "Не корректно введена команда. Введите help для спарвки."; - Optional handle(String[] args, TaskService service); + Optional handle(String[] args, EntityService service); String name(); diff --git a/todo/src/main/java/ru/kamask/pet/todo/cli/CompleteCommand.java b/todo/src/main/java/ru/kamask/pet/todo/cli/CompleteCommand.java index a0ece27..8678876 100644 --- a/todo/src/main/java/ru/kamask/pet/todo/cli/CompleteCommand.java +++ b/todo/src/main/java/ru/kamask/pet/todo/cli/CompleteCommand.java @@ -2,6 +2,8 @@ package ru.kamask.pet.todo.cli; import java.util.Optional; +import ru.kamask.pet.todo.model.Identifiable; +import ru.kamask.pet.todo.service.EntityService; import ru.kamask.pet.todo.service.TaskService; public class CompleteCommand implements Command { @@ -16,14 +18,16 @@ public class CompleteCommand implements Command { } @Override - public Optional handle(String[] args, TaskService service) { + public Optional handle(String[] args, EntityService service) { if (args.length != 1) return Optional.of(Command.errorMessage); + TaskService taskService = (TaskService) service; + try { int id = Integer.parseInt(args[0]); - return Optional.of(service.complete(id) + return Optional.of(taskService.complete(id) ? "Задача ID-%d выполнена.".formatted(id) : "Задача ID-%d не найдена.".formatted(id)); diff --git a/todo/src/main/java/ru/kamask/pet/todo/cli/CreateCommand.java b/todo/src/main/java/ru/kamask/pet/todo/cli/CreateCommand.java index 4549625..6505adb 100644 --- a/todo/src/main/java/ru/kamask/pet/todo/cli/CreateCommand.java +++ b/todo/src/main/java/ru/kamask/pet/todo/cli/CreateCommand.java @@ -2,21 +2,24 @@ package ru.kamask.pet.todo.cli; import java.util.Optional; +import ru.kamask.pet.todo.model.Identifiable; +import ru.kamask.pet.todo.service.EntityService; import ru.kamask.pet.todo.service.TaskService; -public class CreateCommand implements Command { +public class CreateCommand implements Command{ @Override public String name() { return "create"; } @Override - public Optional handle(String[] args, TaskService service) { + public Optional handle(String[] args, EntityService service) { if (args.length > 0) { var title = String.join(" ", args); if (title.length() > 30) return Optional.of("Ошибка: максимальная длинна названия задачи 30 символов."); - service.create(title); + TaskService taskService = (TaskService) service; + taskService.create(title); return Optional.of(String.format("Задача \"%s\" успешно добавлена!", title)); } return Optional.of(Command.errorMessage); diff --git a/todo/src/main/java/ru/kamask/pet/todo/cli/DeleteCommand.java b/todo/src/main/java/ru/kamask/pet/todo/cli/DeleteCommand.java index b037776..50968cf 100644 --- a/todo/src/main/java/ru/kamask/pet/todo/cli/DeleteCommand.java +++ b/todo/src/main/java/ru/kamask/pet/todo/cli/DeleteCommand.java @@ -2,9 +2,10 @@ package ru.kamask.pet.todo.cli; import java.util.Optional; -import ru.kamask.pet.todo.service.TaskService; +import ru.kamask.pet.todo.model.Identifiable; +import ru.kamask.pet.todo.service.EntityService; -public class DeleteCommand implements Command { +public class DeleteCommand implements Command{ @Override public String name() { return "delete"; @@ -16,7 +17,7 @@ public class DeleteCommand implements Command { } @Override - public Optional handle(String[] args, TaskService service) { + public Optional handle(String[] args, EntityService service) { if (args.length != 1) return Optional.of(Command.errorMessage); diff --git a/todo/src/main/java/ru/kamask/pet/todo/cli/ListCommand.java b/todo/src/main/java/ru/kamask/pet/todo/cli/ListCommand.java index 80b2fc5..059892a 100644 --- a/todo/src/main/java/ru/kamask/pet/todo/cli/ListCommand.java +++ b/todo/src/main/java/ru/kamask/pet/todo/cli/ListCommand.java @@ -1,11 +1,11 @@ package ru.kamask.pet.todo.cli; -import java.util.List; import java.util.Optional; -import ru.kamask.pet.todo.model.SimpleTask; -import ru.kamask.pet.todo.model.Task; +import ru.kamask.pet.todo.model.Identifiable; +import ru.kamask.pet.todo.service.EntityService; import ru.kamask.pet.todo.service.TaskService; +import ru.kamask.pet.todo.util.Formatter; public class ListCommand implements Command { @Override @@ -14,34 +14,17 @@ public class ListCommand implements Command { } @Override - public Optional handle(String[] args, TaskService service) { + public Optional handle(String[] args, EntityService service) { if (args.length > 0) return Optional.of(Command.errorMessage); - var res = formatWithTable(service.list(), "Список задач пуст."); + TaskService taskService = (TaskService) service; - return Optional.of(res); + return Optional.of(Formatter.asTable(taskService.getAll())); } @Override public String usage() { return String.format(templateUsage, name(), "Список всех задач."); } - - String formatWithTable(List tasks, String msgIfEmpty){ - String template = "%-2s | %-30s | %s\n"; - String res = ""; - res += String.format(template, "ID", "Название задачи", "Статус"); - res += "-".repeat(50) + "\n"; - - if (tasks.size() == 0) - return res + "\n" + msgIfEmpty; - - for (Task task : tasks) { - SimpleTask.Data data = ((SimpleTask) task).data(); - res += String.format(template, data.id(), data.title(), data.done() ? "выполнено" : "не выполнено"); - } - - return res; - } } diff --git a/todo/src/main/java/ru/kamask/pet/todo/cli/SearchCommand.java b/todo/src/main/java/ru/kamask/pet/todo/cli/SearchCommand.java index ae4ede6..a0880c1 100644 --- a/todo/src/main/java/ru/kamask/pet/todo/cli/SearchCommand.java +++ b/todo/src/main/java/ru/kamask/pet/todo/cli/SearchCommand.java @@ -2,7 +2,10 @@ package ru.kamask.pet.todo.cli; import java.util.Optional; +import ru.kamask.pet.todo.model.Identifiable; +import ru.kamask.pet.todo.service.EntityService; import ru.kamask.pet.todo.service.TaskService; +import ru.kamask.pet.todo.util.Formatter; public class SearchCommand implements Command { @Override @@ -16,15 +19,16 @@ public class SearchCommand implements Command { } @Override - public Optional handle(String[] args, TaskService service) { + public Optional handle(String[] args, EntityService service) { if (args.length != 1) return Optional.of(Command.errorMessage); if (args[0].length() < 3) return Optional.of("Длина запроса должна быть не менее 3 символов."); - var matchTask = service.search(args[0]); - var res = new ListCommand().formatWithTable(matchTask, "Не найдено задач, соответствующих запросу."); + TaskService taskService = (TaskService) service; - return Optional.of(res); + var matchTask = taskService.search(args[0]); + + return Optional.of(Formatter.asTable(matchTask, "Не найдено задач, соответствующих запросу.")); } } diff --git a/todo/src/main/java/ru/kamask/pet/todo/model/Identifiable.java b/todo/src/main/java/ru/kamask/pet/todo/model/Identifiable.java new file mode 100644 index 0000000..0941fcc --- /dev/null +++ b/todo/src/main/java/ru/kamask/pet/todo/model/Identifiable.java @@ -0,0 +1,5 @@ +package ru.kamask.pet.todo.model; + +public interface Identifiable { + int getId(); +} diff --git a/todo/src/main/java/ru/kamask/pet/todo/model/Task.java b/todo/src/main/java/ru/kamask/pet/todo/model/Task.java index f160ca3..ccdb3a2 100644 --- a/todo/src/main/java/ru/kamask/pet/todo/model/Task.java +++ b/todo/src/main/java/ru/kamask/pet/todo/model/Task.java @@ -1,6 +1,6 @@ package ru.kamask.pet.todo.model; -public abstract class Task { +public abstract class Task implements Identifiable{ private static int nextId = 1; protected int id; @@ -11,10 +11,15 @@ public abstract class Task { this.title = title; } - public int id() { + @Override + public int getId() { return id; } + public int id() { + return getId(); + } + @Override public String toString() { return String.format("Задача: id - %d, title: \"%s\"", id, title); diff --git a/todo/src/main/java/ru/kamask/pet/todo/repo/InMemoryTaskRepository.java b/todo/src/main/java/ru/kamask/pet/todo/repo/InMemoryRepository.java similarity index 60% rename from todo/src/main/java/ru/kamask/pet/todo/repo/InMemoryTaskRepository.java rename to todo/src/main/java/ru/kamask/pet/todo/repo/InMemoryRepository.java index 47bd259..fab4543 100644 --- a/todo/src/main/java/ru/kamask/pet/todo/repo/InMemoryTaskRepository.java +++ b/todo/src/main/java/ru/kamask/pet/todo/repo/InMemoryRepository.java @@ -6,23 +6,23 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import ru.kamask.pet.todo.model.Task; +import ru.kamask.pet.todo.model.Identifiable; -public class InMemoryTaskRepository implements TaskRepository { - private Map storage = new HashMap<>(); +public class InMemoryRepository implements Repository { + private Map storage = new HashMap<>(); @Override - public void save(Task task) { - storage.put(task.id(), task); + public void save(T obj) { + storage.put(obj.getId(), obj); } @Override - public Optional findById(int id) { + public Optional findById(int id) { return Optional.ofNullable(storage.get(id)); } @Override - public List findAll() { + public List findAll() { return new ArrayList<>(storage.values()); } diff --git a/todo/src/main/java/ru/kamask/pet/todo/repo/Repository.java b/todo/src/main/java/ru/kamask/pet/todo/repo/Repository.java new file mode 100644 index 0000000..dcf7b37 --- /dev/null +++ b/todo/src/main/java/ru/kamask/pet/todo/repo/Repository.java @@ -0,0 +1,14 @@ +package ru.kamask.pet.todo.repo; + +import java.util.List; +import java.util.Optional; + +import ru.kamask.pet.todo.model.Identifiable; + +public interface Repository { + void save(T obj); + Optional findById(int id); + List findAll(); + void delete(int id); + boolean has(int id); +} diff --git a/todo/src/main/java/ru/kamask/pet/todo/repo/TaskRepository.java b/todo/src/main/java/ru/kamask/pet/todo/repo/TaskRepository.java deleted file mode 100644 index 3ae050d..0000000 --- a/todo/src/main/java/ru/kamask/pet/todo/repo/TaskRepository.java +++ /dev/null @@ -1,18 +0,0 @@ -package ru.kamask.pet.todo.repo; - -import java.util.List; -import java.util.Optional; - -import ru.kamask.pet.todo.model.Task; - -public interface TaskRepository { - void save(Task task); - - Optional findById(int id); - - List findAll(); - - void delete(int id); - - boolean has(int id); -} diff --git a/todo/src/main/java/ru/kamask/pet/todo/service/EntityService.java b/todo/src/main/java/ru/kamask/pet/todo/service/EntityService.java new file mode 100644 index 0000000..720aa7f --- /dev/null +++ b/todo/src/main/java/ru/kamask/pet/todo/service/EntityService.java @@ -0,0 +1,35 @@ +package ru.kamask.pet.todo.service; + +import java.util.List; +import java.util.Optional; + +import ru.kamask.pet.todo.model.Identifiable; +import ru.kamask.pet.todo.repo.Repository; + +public class EntityService { + private final Repository repo; + + protected EntityService(Repository repo) { + this.repo = repo; + } + + public void save(T obj) { + repo.save(obj); + } + + public Optional getById(int id) { + return repo.findById(id); + } + + public List getAll() { + return repo.findAll(); + } + + public void remove(int id) { + repo.delete(id); + } + + public boolean has(int id) { + return repo.has(id); + } +} diff --git a/todo/src/main/java/ru/kamask/pet/todo/service/TaskService.java b/todo/src/main/java/ru/kamask/pet/todo/service/TaskService.java index e8e0498..c1ab2ef 100644 --- a/todo/src/main/java/ru/kamask/pet/todo/service/TaskService.java +++ b/todo/src/main/java/ru/kamask/pet/todo/service/TaskService.java @@ -1,29 +1,23 @@ package ru.kamask.pet.todo.service; import java.util.List; -import java.util.Optional; -import ru.kamask.pet.todo.model.Task; import ru.kamask.pet.todo.model.SimpleTask; -import ru.kamask.pet.todo.repo.TaskRepository; +import ru.kamask.pet.todo.repo.Repository; -public class TaskService { - private final TaskRepository repo; +public class TaskService extends EntityService { - public TaskService(TaskRepository repo) { - this.repo = repo; + public TaskService(Repository repo) { + super(repo); } public void create(String title) { - repo.save(new SimpleTask(title)); + super.save(new SimpleTask(title)); } - public Optional getById(int id) { - return repo.findById(id); - } public boolean complete(int id) { - var taskOpt = repo.findById(id); + var taskOpt = super.getById(id); if (taskOpt.isPresent()) { taskOpt.get().markAsCompleted(); return true; @@ -31,21 +25,10 @@ public class TaskService { return false; } - public List list() { - return repo.findAll(); - } - public void remove(int id) { - repo.delete(id); - } - - public boolean has(int id) { - return repo.has(id); - } - - public List search(String query) { - return repo.findAll().stream() - .filter(task -> ((SimpleTask) task).data().title().contains(query)) + public List search(String query) { + return super.getAll().stream() + .filter(task -> task.data().title().contains(query)) .toList(); } } diff --git a/todo/src/main/java/ru/kamask/pet/todo/util/Formatter.java b/todo/src/main/java/ru/kamask/pet/todo/util/Formatter.java new file mode 100644 index 0000000..fd60064 --- /dev/null +++ b/todo/src/main/java/ru/kamask/pet/todo/util/Formatter.java @@ -0,0 +1,27 @@ +package ru.kamask.pet.todo.util; + +import java.util.List; + +import ru.kamask.pet.todo.model.SimpleTask; + +public class Formatter { + public static String asTable(List tasks){ + return asTable(tasks, "Список задач пуст."); + } + + public static String asTable(List tasks, String msgIfEmpty){ + String template = "%-2s | %-30s | %s\n"; + var res = new StringBuilder(String.format(template, "ID", "Название задачи", "Статус")); + res.append("-".repeat(50) + "\n"); + + if (tasks.size() == 0) + return res.append("\n" + msgIfEmpty).toString(); + + for (SimpleTask task : tasks) { + SimpleTask.Data data = task.data(); + res.append(String.format(template, data.id(), data.title(), data.done() ? "выполнено" : "не выполнено")); + } + + return res.toString(); + } +}