Сервисы: где жить логике

Сервис — это класс без шаблона, куда переезжает вся логика, не относящаяся к отрисовке: запросы, расчёты, общее состояние.

«Компонент должен заниматься видом. Как только он начинает считать налоги и ходить в сеть — пора звать сервис».

Компоненты отвечают за интерфейс. Но куда деть бизнес-логику — загрузку товаров, корзину, авторизацию? Если запихнуть всё в компонент, он разбухнет и станет неповторимо уникальным. Решение — сервисы: обычные TypeScript-классы, которые инкапсулируют логику и данные, и которые можно переиспользовать в любом компоненте.

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

@Injectable({ providedIn: 'root' })
export class CartService {
  private items: string[] = [];

  add(product: string) { this.items.push(product); }
  getItems() { return this.items; }
  get total() { return this.items.length; }
}

Декоратор @Injectable помечает класс как доступный для внедрения зависимостей. Опция providedIn: 'root' говорит: создай один общий экземпляр на всё приложение. Именно «один экземпляр» делает сервис идеальным местом для общего состояния — корзина одна, и все компоненты видят одни и те же товары.

Как работает под капотом

providedIn: 'root' регистрирует сервис в корневом инжекторе. Angular создаёт экземпляр лениво — только когда его впервые попросят, — и кеширует. Все последующие запросы получают тот же объект (паттерн «синглтон»). Это же включает tree-shaking: если сервис нигде не используется, сборщик выкинет его из бандла.

   корневой инжектор
        |
   первый запрос CartService
        |
   создан 1 экземпляр  ----+
        |                  |
   компонент A  ----+      | один и тот же
   компонент B  ----+------+ объект
   компонент C  ----+

Частые ошибки

  • Хранить состояние в компоненте, а не в сервисе. Тогда при уничтожении компонента данные теряются.
  • Создавать сервис через new CartService(). Так вы получите отдельный экземпляр мимо DI — потеряете синглтон и зависимости.
  • Мешать вид и логику. HTTP-запросы и расчёты в шаблоне или обработчике клика — признак того, что нужен сервис.

Best practices

  • По умолчанию используйте providedIn: 'root' — это просто и поддерживает tree-shaking.
  • Один сервис — одна зона ответственности: CartService, AuthService, ProductApiService.
  • Скрывайте внутренние данные через private, отдавайте наружу методы — это защищает инварианты.

Итоги. Сервис — класс с логикой и состоянием, помеченный @Injectable. providedIn: 'root' даёт единый синглтон на приложение. Дальше разберём, как сервис попадает в компонент — через внедрение зависимостей.

Закрепляем

Главный принцип урока — разделение ответственности. Компоненты отвечают за то, что видит пользователь; сервисы — за то, как устроена логика и где живут данные. Как только в компоненте появляется бизнес-расчёт, запрос в сеть или состояние, которое должно пережить этот компонент, — это сигнал вынести логику в сервис. Так компоненты остаются тонкими и сменными, а ценная логика — переиспользуемой и тестируемой в одном месте.

Опция providedIn: 'root' делает сервис синглтоном — единственным экземпляром на всё приложение, который создаётся лениво при первом обращении и затем переиспользуется. Именно эта «единственность» превращает сервис в естественное хранилище общего состояния: одна корзина, один сервис авторизации, один кеш. Бонусом идёт tree-shaking: если сервис нигде не используется, сборщик просто выкинет его из финального бандла, и он не утяжелит приложение. Это сочетание удобства и эффективности и сделало providedIn: 'root' выбором по умолчанию.

ЭлементРоль
@InjectableПомечает класс как внедряемый
providedIn: 'root'Синглтон на всё приложение
private поляСкрытое состояние сервиса
публичные методыКонтролируемый доступ к логике
Проверьте себя
1. Что означает providedIn: 'root' у сервиса?
AСервис работает только в корневом компоненте
BСоздаётся один общий (синглтон) экземпляр на всё приложение с поддержкой tree-shaking
CСервис нельзя внедрить в компоненты
DСервис загружается с сервера
2. Почему создавать сервис через new CartService() — плохо?
AЭто синтаксическая ошибка
BВы получите отдельный экземпляр мимо DI, потеряв синглтон и автоматическое внедрение зависимостей
Cnew работает только с компонентами
DТак медленнее