Introdução
O NestJS é um framework progressivo para construir aplicações back-end em Node.js que facilita a criação de APIs escaláveis e modulares. Um dos conceitos fundamentais para manter uma base de código limpa e sustentável em projetos NestJS é seguir os princípios SOLID.
Os princípios SOLID formam uma base sólida para o design orientado a objetos, ajudando a tornar o código mais legível, fácil de manter e escalável. Neste artigo, vamos revisar cada um desses princípios (SRP, OCP, LSP, ISP e DIP) e explorar como aplicá-los com exemplos práticos no NestJS.
Princípio da Responsabilidade Única (SRP)
O Princípio da Responsabilidade Única (Single Responsibility Principle) afirma que uma classe deve ter apenas uma razão para mudar, ou seja, uma única responsabilidade. No contexto de NestJS, isso significa manter o foco de cada serviço, controlador ou repositório em uma função específica.
Exemplo de Aplicação SRP no NestJS
Imagine que temos uma aplicação com funcionalidades de usuários e autenticação. Ao invés de ter um único UserService
para gerenciar tanto as informações dos usuários quanto a autenticação, podemos dividir essas responsabilidades em serviços separados:
// src/user/user.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserService {
// Funções relacionadas à manipulação de dados do usuário
async createUser(data: CreateUserDto) { /*...*/ }
async getUserById(id: string) { /*...*/ }
}
// src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class AuthService {
// Funções relacionadas à autenticação
async login(credentials: LoginDto) { /*...*/ }
async validateUser(token: string) { /*...*/ }
}
Dividir os serviços dessa forma facilita a manutenção e evita que mudanças em uma funcionalidade afetem outra.
Princípio Aberto/Fechado (OCP)
O Princípio Aberto/Fechado (Open/Closed Principle) afirma que classes, módulos e funções devem estar abertos para extensão, mas fechados para modificação. Isso significa que devemos ser capazes de adicionar novos comportamentos ao sistema sem modificar o código existente.
Exemplo de Aplicação OCP no NestJS
Suponha que temos uma funcionalidade de envio de notificações. Se quisermos suportar diferentes canais de notificação (por exemplo, e-mail e SMS), podemos criar uma interface NotificationService
e implementar classes específicas para cada tipo de notificação.
// src/notification/interfaces/notification.interface.ts
export interface NotificationService {
sendNotification(message: string): Promise<void>;
}
// src/notification/services/email-notification.service.ts
import { Injectable } from '@nestjs/common';
import { NotificationService } from '../interfaces/notification.interface';
@Injectable()
export class EmailNotificationService implements NotificationService {
async sendNotification(message: string): Promise<void> {
console.log('Enviando notificação por email:', message);
}
}
// src/notification/services/sms-notification.service.ts
import { Injectable } from '@nestjs/common';
import { NotificationService } from '../interfaces/notification.interface';
@Injectable()
export class SmsNotificationService implements NotificationService {
async sendNotification(message: string): Promise<void> {
console.log('Enviando notificação por SMS:', message);
}
}
Com essa abordagem, podemos adicionar novos canais de notificação sem modificar o código existente, apenas criando novas implementações da interface NotificationService
.
Princípio da Substituição de Liskov (LSP)
O Princípio da Substituição de Liskov (Liskov Substitution Principle) declara que as subclasses devem ser substituíveis por suas superclasses sem alterar a funcionalidade do programa. No NestJS, ao usar injeção de dependência, precisamos garantir que as implementações injetadas cumpram o contrato da interface.
Exemplo de Aplicação LSP no NestJS
No exemplo de notificação, garantimos que qualquer implementação de NotificationService
(EmailNotificationService
, SmsNotificationService
, etc.) pode substituir a outra sem mudar o comportamento esperado do código:
// src/notification/notification.controller.ts
import { Controller, Inject } from '@nestjs/common';
import { NotificationService } from './interfaces/notification.interface';
@Controller('notification')
export class NotificationController {
constructor(
@Inject('NotificationService') private readonly notificationService: NotificationService,
) {}
sendMessage(message: string) {
this.notificationService.sendNotification(message);
}
}
O controlador NotificationController
pode trabalhar com qualquer implementação de NotificationService
, mantendo a conformidade com o princípio LSP.
Princípio da Segregação de Interface (ISP)
O Princípio da Segregação de Interface (Interface Segregation Principle) afirma que uma classe não deve ser obrigada a implementar interfaces que não usa. No NestJS, esse princípio pode ser aplicado ao criar interfaces menores e mais específicas para cada responsabilidade.
Exemplo de Aplicação ISP no NestJS
Em vez de criar uma interface grande para gerenciar todas as operações de um serviço, separamos as interfaces em responsabilidades menores. Por exemplo, para um serviço de repositório de dados:
// src/repository/interfaces/read.interface.ts
export interface Read<T> {
findAll(): Promise<T[]>;
findOne(id: string): Promise<T>;
}
// src/repository/interfaces/write.interface.ts
export interface Write<T> {
create(item: T): Promise<T>;
update(id: string, item: T): Promise<T>;
delete(id: string): Promise<void>;
}
// src/repository/interfaces/repository.interface.ts
import { Read } from './read.interface';
import { Write } from './write.interface';
export interface Repository<T> extends Read<T>, Write<T> {}
As classes podem implementar apenas as interfaces que realmente precisam, evitando métodos não utilizados.
Princípio da Inversão de Dependência (DIP)
O Princípio da Inversão de Dependência (Dependency Inversion Principle) declara que módulos de alto nível não devem depender de módulos de baixo nível, mas ambos devem depender de abstrações. Em NestJS, podemos aplicar DIP usando injeção de dependência e interfaces.
Exemplo de Aplicação DIP no NestJS
No exemplo abaixo, o controlador depende da interface NotificationService
ao invés de uma implementação específica:
// src/notification/notification.module.ts
import { Module } from '@nestjs/common';
import { NotificationController } from './notification.controller';
import { EmailNotificationService } from './services/email-notification.service';
@Module({
controllers: [NotificationController],
providers: [
{ provide: 'NotificationService', useClass: EmailNotificationService },
],
})
export class NotificationModule {}
Dessa forma, podemos alterar a implementação de NotificationService
sem modificar NotificationController
, respeitando o princípio de inversão de dependência.
Conclusão
Aplicar os princípios SOLID no NestJS ajuda a criar uma arquitetura mais robusta, modular e fácil de manter. Seguir esses princípios exige disciplina e planejamento, mas, a longo prazo, traz benefícios significativos para a qualidade e a escalabilidade do código. Ao desenvolver um projeto NestJS, mantenha os princípios SOLID em mente para estruturar um código limpo e sustentável.
Top comments (0)