Пример с AuthGuard
Возьмём пример из feeldown-backend
С первого взгляда кажется, что класс перенасыщен, проверка метаданных, hash-сервис, парсинг slug-а, однако на самом деле этот класс делегирует задачи профильным утилитам.
- Делегирование задач
AuthGuardServiceвыполняет всю работу по валидации запроса, а наш guard просто вызываетthis.service.validateRequest(request).HashServiceвыполняет роль парсера токена и всех операций, связанных с криптографией и токенами.SlugPipeпарсит сложные slug-и.
- Разделение логики
validateMetadataвалидирует все метаданные, проверяет прав доступа, разделяем основную работу и второстепенную.validateOnlyMeотдельная большая валидацияOnlyMe-метаполя.validateUsernameпросто защита от дублирования кода.
- Использование
tryCatchThrow, чтобы избавиться от больших повторяющихсяtry-catch-блоков.
AuthGuardследует SRP не потому, что он маленький, а потому что его единственная ответственность — это управление потоком аутентификации. (решить: пропустить запрос или нет). Всё остальное — это детали, которые он делегирует.
Что плохо
validateOnlyMeможно было вынести в отдельный Guard или в отдельный сервис, чтобы Guard занимался только пропуском- Смешивание ответственности:
- AuthGuard проверяет, авторизован ли пользователь и имеет ли он доступ к конкретному ресурсу (Authentication и Authorization), лучше всего разделить его два два Guard-а.
validateMetadataвалидирует все метаданные в одном месте через условия, что нарушает open-closed принцип, лучше всего сделатьMetadataHandler, чтобы не изменять метод, а добавлять класс и изменять только массив всех метаданных.SlugPipeв Guard-е недопустим, потому что он трансформирует данные, что Guard делать не должен, лучше всего попробовать передать уже спрасированные данные или вынести парсер в отдельный сервис.
Однако
«Однако» — несмотря на выявленные недостатки, AuthGuard всё же демонстрирует главную идею SRP:
класс должен иметь только одну причину для изменения.
В текущей реализации такой причиной является управление потоком аутентификации (пропустить или отклонить запрос).
Всё, что выходит за эти рамки (парсинг slug, криптография, проверка метаданных), уже делегировано, пусть и не идеально.
Настоящая проблема — не в нарушении SRP как такового,
а в смешении уровней абстракции и нарушении Open/Closed Principle.
Если мы выделим OnlyMeGuard и MetadataHandlerChain, то AuthGuard станет эталонным примером SRP:
его единственная задача — скомпоновать результаты проверок и принять решение.
Запомните: SRP — это не про размер класса или количество методов. Это про направление изменений. Если изменение требований к аутентификации заставляет менять код парсинга slug — значит, ответственность размазана. Если же вы можете добавить новый тип авторизации, не трогая
AuthGuard, — вы на верном пути.
Улучшенный код
only-me.guard.ts
import type { ExecutionContext } from "@nestjs/common";
import type { OnlyMeMetadata } from "@types";
import type { Request } from "express";
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
} from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { Metadata } from "@/enums";
import { OnlyMeGuardService } from "./only-me-guard.service";
@Injectable()
export class OnlyMeGuard implements CanActivate {
public constructor(
private readonly reflector: Reflector,
private readonly service: OnlyMeGuardService,
private readonly serverUserService: ServerUserService,
) {}
public async canActivate(context: ExecutionContext): Promise<boolean> {
const onlyMeMetadata = this.reflector.get<OnlyMeMetadata>(
Metadata.isOnlyMe,
context.getHandler(),
);
if (!onlyMeMetadata) {
return true;
}
const request = context.switchToHttp().getRequest<Request>();
const user = this.serverUserService.getByRequest(request);
if (!user) {
throw new ForbiddenException("User not authenticated");
}
const validated = await this.service.execute(
user,
onlyMeMetadata,
request.params,
);
if (!validated) {
throw new ForbiddenException("You are not the owner of this resource");
}
return true;
}
}
export const ONLY_ME_GUARD_PROVIDERS = [
OnlyMeGuardService,
ServerUserService,
PrismaService,
] as const;
export default OnlyMeGuard;auth.guard.ts
import type { ExecutionContext } from "@nestjs/common";
import type { Request } from "express";
import { Injectable, CanActivate } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { Metadata } from "@/enums";
import { AuthGuardService } from "./auth-guard.service";
@Injectable()
export class AuthGuard implements CanActivate {
public constructor(
private readonly reflector: Reflector,
private readonly service: AuthGuardService,
private readonly serverUserService: ServerUserService,
) {}
public async canActivate(context: ExecutionContext): Promise<boolean> {
const handler = context.getHandler();
const isPublic = this.reflector.get<boolean>(Metadata.isPublic, handler);
if (isPublic) {
return true;
}
const skipAuthGuard = this.reflector.get<boolean>(
Metadata.skipAuthGuard,
handler,
);
if (skipAuthGuard) {
return true;
}
const request = context.switchToHttp().getRequest<Request>();
const user = await this.service.execute(request);
if (!user) {
throw new UnauthorizedException("Invalid credentials");
}
this.serverUserService.setByRequest(request, user);
return true;
}
}
export const AUTH_GUARD_PROVIDERS = [
AuthGuardService,
ServerUserService,
PrismaService,
] as const;
export default AuthGuard;Примечание: здесь использовано упрощение. По-хорошему для каждой метадаты использовано свой класс, однако в данном случае это может оказаться переусложнение кода.
Здесь можно было бы применить паттерн “Цепочка обязанностей”, но для трёх условий мы пока оставляем так, чтобы не переусложнять
Пример в контроллере:
@Controller("posts")
export class PostsController {
@Public()
@Get("public")
public get() {
/* ... */
}
@UseGuards(AuthGuard)
@Post()
public post(@Body() post: CreatePostDto) {
/* ... */
}
@UseGuards(AuthGuard, OnlyMeGuard)
@Delete(ROUTES.DELETE)
public delete(@Parameter("id") id: string) {
/* ... */
}
}Что мы сделали и зачем
Мы взяли AuthGuard, который успел обрасти ответственностью, и аккуратно разобрали его на части.
Не потому что он плохой — просто каждая деталь теперь лежит там, где ей положено.
Вот что конкретно произошло:
-
OnlyMeGuardуехал в отдельный гараж. Вся логика сisOnlyMe, сравнениемid,usernameиslug— всё это переехало в новый класс. Теперь он занимается ровно одним делом: проверяет, имеет ли пользователь право распоряжаться этим ресурсом. И больше ничем. -
AuthGuardперестал быть универсальной сущностью. Его задача теперь кристально прозрачна:- проверить, не публичный ли маршрут (
isPublic); - не пропущен ли он специальным флагом (
skipAuthGuard); - если нет — провалидировать учётные данные через
AuthGuardService; - и положить пользователя в
ServerUserService, чтобы остальные знали, кто тут главный.
- проверить, не публичный ли маршрут (
-
ServerUserServiceстал центральным диспетчером по пользователям. Раньше каждый Guard сам решал, как достать пользователя: кто черезrequest.user, кто черезHashService. Теперь всё идёт через один сервис. Он знает, где лежит пользователь, и умеет его доставать — хоть изrequest, хоть из кэша, хоть из базы. Остальным об этом думать не надо. -
Зависимости похудели. Из
AuthGuardулетелиSlugPipeиHashService— они там больше не нужны. ИзOnlyMeGuardубрали всю муть с токенами. Каждый Guard знает ровно столько, сколько ему нужно, и не лезет в чужие дела. -
Guard-ы теперь можно смешивать как конструктор. Хотите просто проверить, что пользователь залогинен? Берите
AuthGuard. Хотите ещё и проверить, что он владеет ресурсом? ДобавьтеOnlyMeGuard. Хотите открытый маршрут? Просто повесьте@Public()— и ни одного Guard.
Что в итоге
Теперь у каждого Guard ровно одна причина, чтобы измениться:
AuthGuardпереписывают только тогда, когда меняют логику аутентификации — например, переходят с JWT на сессии или добавляют новый тип токена.OnlyMeGuardтрогают только тогда, когда меняются правила доступа — например, появляется новый способ проверки владельца (по email или по uuid).ServerUserServiceживёт своей жизнью и отвечает за то, чтобы пользователь всегда был под рукой.
Мы не просто починили нарушение SRP — мы сделали код гибким, читаемым и удобным для тестирования. И, что важно, теперь, когда через полгода придёт новый разработчик и откроет этот файл, ему не придётся гадать, что тут за что отвечает. Всё разложено по полочкам.
А главное — мы убрали смешение уровней абстракции.
Теперь AuthGuard не знает, как парсить slug, а OnlyMeGuard не знает, как работать с токенами.
Каждый занимается своим делом — и делает это хорошо.