JavaRush /Курсы /Модуль 4: Node.js, Next.js и Angular /Зачем нужны сервисы: бизнес-логика вне компонентов

Зачем нужны сервисы: бизнес-логика вне компонентов

Модуль 4: Node.js, Next.js и Angular
15 уровень , 0 лекция
Открыта

1. Введение

Angular — это не только про красивые компоненты, но и про правильную архитектуру. Представьте себе кухню, где повар (компонент) не только готовит, но и сам ходит за продуктами, моет посуду, считает зарплаты и пишет отчёты. Смешно? Вот и в приложении не стоит заставлять компонент заниматься всем подряд.

Компонент — это, прежде всего, "виджет", который отвечает за внешний вид и взаимодействие с пользователем. Да, он может содержать немного логики, но если весь ваш бизнес-процесс (например, подсчёт стоимости заказа, работа с сервером, обработка корзины) живёт в компоненте — вас ждёт боль, слёзы и рефакторинг.

Сервис — это как отдельный работник на кухне: кто-то закупает продукты, кто-то моет посуду, кто-то ведёт бухгалтерию. Компоненту остаётся только красиво сервировать блюдо и общаться с клиентом.

Что такое сервис в Angular?

Сервис — это обычный TypeScript-класс, который предназначен для хранения и реализации бизнес-логики, данных и функционала, не связанного напрямую с отображением. Сервис создаётся отдельно, а затем "внедряется" (инжектируется) в компоненты, которые хотят им воспользоваться.

Классическая аналогия

  • Компонент — это актёр на сцене, который показывает спектакль (UI).
  • Сервис — это сценарист и гримёр за кулисами, который готовит всё необходимое, чтобы актёр выглядел и действовал правильно.

Для чего нужны сервисы?

  • Хранение и управление данными приложения (например, список задач, корзина покупок, текущий пользователь).
  • Вынесение бизнес-логики (например, подсчёт итоговой суммы, фильтрация, валидация).
  • Общение с внешними источниками данных (API, сервер, LocalStorage).
  • Реализация общих утилит (логирование, уведомления, форматирование дат).

2. Типичная проблема: "Жирные" компоненты

Давайте рассмотрим пример: у нас есть компонент списка задач (todo-list), который:

  • Хранит массив задач.
  • Добавляет/удаляет задачи.
  • Сохраняет задачи в LocalStorage.
  • Фильтрует задачи по статусу.
  • Отправляет задачи на сервер.

Выглядит примерно так (упрощённо):


export class TodoListComponent {
  todos: Todo[] = [];

  constructor() {
    this.loadTodos();
  }

  addTodo(text: string) {
    this.todos.push({ text, done: false });
    this.saveTodos();
  }

  removeTodo(index: number) {
    this.todos.splice(index, 1);
    this.saveTodos();
  }

  saveTodos() {
    localStorage.setItem('todos', JSON.stringify(this.todos));
  }

  loadTodos() {
    const saved = localStorage.getItem('todos');
    this.todos = saved ? JSON.parse(saved) : [];
  }

  // ... и так далее ...
}

Что здесь не так? Компонент отвечает за всё сразу: и за отображение, и за бизнес-логику, и за работу с хранилищем. Если вы захотите использовать эти функции в другом компоненте — придётся копировать код. Если потребуется изменить способ хранения (например, перейти на сервер) — нужно менять компонент. Это не масштабируется!

3. Сервисы решают проблемы архитектуры

Преимущества сервисов

  1. Переиспользуемость: Один сервис — много компонентов. Логику можно использовать где угодно.
  2. Тестируемость: Тестировать сервисы проще, чем компоненты (нет зависимости от шаблонов и UI).
  3. Единое хранилище: Сервис может выступать как "Single Source of Truth" — все компоненты видят одни и те же данные.
  4. Разделение ответственности: Компонент становится "тонким" и простым, а вся тяжёлая работа уходит в сервис.

Схема взаимодействия


+-------------------+         +----------------------+
|   Компонент 1     | <-----> |      Сервис          |
+-------------------+         +----------------------+
                               ^
+-------------------+         /
|   Компонент 2     | <------+
+-------------------+

4. Как создать сервис в Angular

Создать сервис очень просто (и даже проще, чем сварить кофе!):

1. Обычный TypeScript-класс


export class TodoService {
  private todos: Todo[] = [];

  getTodos() {
    return this.todos;
  }
  // ...другие методы
}

2. @Injectable() — магия DI

Чтобы Angular мог "внедрять" сервис в компоненты, добавьте декоратор @Injectable():


import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root' // сервис будет синглтоном на всё приложение
})
export class TodoService {
  // ...
}

3. Использование в компоненте

В компоненте сервис "подключается" через конструктор:


import { TodoService } from './todo.service';

@Component({ /* ... */ })
export class TodoListComponent {
  constructor(private todoService: TodoService) {}

  get todos() {
    return this.todoService.getTodos();
  }

  addTodo(text: string) {
    this.todoService.addTodo(text);
  }
}

Теперь компонент ничего не знает о том, как именно хранятся задачи — он просто вызывает методы сервиса.

5. Пример: Перенос логики из компонента в сервис

Давайте вынесем всю работу с задачами из компонента в сервис.

todo.service.ts


import { Injectable } from '@angular/core';

export interface Todo {
  text: string;
  done: boolean;
}

@Injectable({
  providedIn: 'root'
})
export class TodoService {
  private todos: Todo[] = [];

  constructor() {
    this.loadTodos();
  }

  getTodos(): Todo[] {
    return this.todos;
  }

  addTodo(text: string) {
    this.todos.push({ text, done: false });
    this.saveTodos();
  }

  removeTodo(index: number) {
    this.todos.splice(index, 1);
    this.saveTodos();
  }

  saveTodos() {
    localStorage.setItem('todos', JSON.stringify(this.todos));
  }

  loadTodos() {
    const saved = localStorage.getItem('todos');
    this.todos = saved ? JSON.parse(saved) : [];
  }
}

todo-list.component.ts


import { Component } from '@angular/core';
import { TodoService, Todo } from './todo.service';

@Component({
  selector: 'app-todo-list',
  template: `
    <ul>
        <li *ngFor="let todo of todos; let i = index">
            {{ todo.text }}
            <button (click)="remove(i)">Удалить</button>
        </li>
    </ul>
    <input [(ngModel)]="newTodo" placeholder="Новая задача" />
    <button (click)="add()">Добавить</button>
  `
})
export class TodoListComponent {
  newTodo = '';

  constructor(private todoService: TodoService) {}

  get todos(): Todo[] {
    return this.todoService.getTodos();
  }

  add() {
    if (this.newTodo.trim()) {
      this.todoService.addTodo(this.newTodo.trim());
      this.newTodo = '';
    }
  }

  remove(index: number) {
    this.todoService.removeTodo(index);
  }
}

Теперь компонент стал тонким и простым — он только отображает и делегирует действия сервису.

6. Полезные нюансы

Когда сервис обязателен, а когда — нет?

Обязательно используйте сервисы, если:

  • Логику нужно переиспользовать в нескольких компонентах.
  • Данные должны быть общими для нескольких частей приложения (например, корзина покупок, профиль пользователя).
  • Работа с сервером (HTTP-запросы, WebSocket и т.д.).
  • Хранение состояния между переходами по страницам (роутинг).

Можно обойтись без сервиса, если:

  • Логика очень простая и специфична только для одного компонента.
  • Нет необходимости делиться данными или функционалом с другими частями приложения.
  • Вы пишете прототип или "игрушечный" пример (но даже тут сервисы часто полезны для тренировки правильной архитектуры).

Сервисы — это не только про данные

Сервисы часто используются не только для хранения данных, но и для:

  • Работы с сервером: отдельный сервис для HTTP-запросов.
  • Авторизации: сервис для хранения и проверки токена пользователя.
  • Логирования: сервис для вывода сообщений в консоль или отправки на сервер.
  • Валидации: сервис для проверки данных формы.
  • Вспомогательных функций: форматирование дат, чисел, конвертация валют и т.д.

Как сервисы работают "под капотом": инъекция зависимостей

Angular использует механизм Dependency Injection (DI) — внедрение зависимостей. Это значит, что вы не создаёте сервис через new, а просто объявляете его в конструкторе. Angular сам создаёт экземпляр сервиса и "вкладывает" его в компонент. Это позволяет делать сервисы синглтонами (один экземпляр на всё приложение), а также удобно тестировать и подменять сервисы при необходимости.

Факт для любознательных:
Если вы укажете providedIn: 'root' в декораторе @Injectable(), сервис будет создан единожды на всё приложение (singleton). Если укажете в массиве providers компонента — сервис будет создан отдельно для каждого экземпляра компонента.

7. Типичные ошибки при работе с сервисами

Если часть бизнес-логики осталась в компоненте, а часть ушла в сервис — возникает путаница и дублирование. Лучше вынести всю "тяжёлую работу" в сервис, а компонент оставить только для отображения и вызова методов.

Сервис создаётся вручную через new. Это ломает систему DI! Никогда не делайте const s = new MyService(). Используйте только внедрение через конструктор.

Сервис не декорирован @Injectable() или не указан providedIn. Без этого Angular не сможет создать и внедрить сервис автоматически.

Дублирование сервисов в разных модулях/компонентах. Если вы случайно добавите сервис в providers разных компонентов, получите несколько экземпляров (и разные данные). Обычно сервисы должны быть синглтонами (providedIn: 'root').

Сервис хранит состояние, но вы ожидаете, что оно "сбрасывается" при переходе между страницами. Синглтон-сервисы живут всё время работы приложения. Если нужно сбрасывать состояние — делайте это явно.

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ