Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Сервисы и слои

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

Три слоя

СлойАннотацияОтвечает заНе должен
Контроллер@RestControllerАдреса, разбор запроса, коды ответаСодержать правила предметной области
Сервис@ServiceЛогику: проверки, расчёты, транзакцииЗнать про HTTP и ResponseEntity
Репозиторий@RepositoryЧтение и запись данныхРешать, что можно сохранять

Запрос идёт сверху вниз: контроллер → сервис → репозиторий. Ответ — обратно.

Сервис блога целиком

// src/main/java/com/example/blog/PostService.java
@Service
public class PostService {
 
    private final PostRepository repository;
    private final Clock clock;
    private final BlogProperties properties;
 
    public PostService(PostRepository repository, Clock clock, BlogProperties properties) {
        this.repository = repository;
        this.clock = clock;
        this.properties = properties;
    }
 
    public List<Post> findAll(String author) {
        return repository.findAll().stream()
                .filter(post -> author == null || post.author().equals(author))
                .sorted(Comparator.comparing(Post::id))
                .limit(properties.pageSize())
                .toList();
    }
 
    public Post findById(Long id) {
        return repository.findById(id)
                .orElseThrow(() -> new PostNotFoundException(id));
    }
 
    public Post create(NewPostRequest request) {
        Post post = new Post(null, request.title(), request.body(), request.author(), Instant.now(clock));
        return repository.save(post);
    }
 
    public Post update(Long id, NewPostRequest request) {
        Post old = findById(id);
        return repository.save(new Post(id, request.title(), request.body(), request.author(), old.createdAt()));
    }
 
    public void delete(Long id) {
        findById(id);
        repository.deleteById(id);
    }
}

Обратите внимание: сервис бросает PostNotFoundException, а не возвращает код 404. Он не знает, что его вызывают по HTTP. Превращать исключение в ответ — работа обработчика из прошлого урока.

Файлы проекта

src/main/java/com/example/blog/
├── BlogApplication.java
├── AppConfig.java            настройки и бины
├── BlogProperties.java       группа настроек
├── PostController.java       слой контроллеров
├── ApiExceptionHandler.java  ошибки → ответы
├── NewPostRequest.java       входные данные
├── PostService.java          слой сервисов
├── PostNotFoundException.java
├── PostRepository.java       слой данных
└── Post.java                 модель

Пока классов немного, им удобно в одном пакете. Когда проект растёт, их раскладывают по пакетам-разделам: com.example.blog.post, com.example.blog.comment, com.example.blog.user. Всё про статьи лежит рядом, и не нужно прыгать по папкам controllers и services.

Как проверить всё сразу

curl -X POST http://localhost:8080/api/posts -H "Content-Type: application/json" \
  -d '{"title":"Второй","body":"Текст","author":"oleg"}'
curl -X PUT http://localhost:8080/api/posts/2 -H "Content-Type: application/json" \
  -d '{"title":"Второй (исправлено)","body":"Текст","author":"oleg"}'
curl -i -X DELETE http://localhost:8080/api/posts/2

Создание вернёт 201, изменение — 200 с новым заголовком, удаление — 204. Повторное удаление той же статьи ответит 404 через обработчик ошибок.

Частая ошибка

Соблазнительно передать репозиторий прямо в контроллер и написать логику там: «найди, проверь, сохрани». Сначала это короче, но затем ту же логику понадобится вызвать из страницы сайта или фоновой задачи — и её придётся копировать. Держите контроллер тонким: принял запрос, вызвал сервис, вернул ответ.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.