Aplicando SOLID no NestJS

rafael_avelarcampos_e71c

Rafael Avelar Campos

Posted on November 8, 2024

Aplicando SOLID no NestJS

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) { /*...*/ }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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> {}
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
rafael_avelarcampos_e71c
Rafael Avelar Campos

Posted on November 8, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Aplicando SOLID no NestJS
solidjs Aplicando SOLID no NestJS

November 8, 2024