Pular para o conteúdo

Exemplos de como utilizar os principios SOLID no Framework Nest.js

O NestJS é um framework para aplicações Node.js baseado em padrões de projeto de código limpo. Você pode aplicar os principios SOLID em seu código usando o NestJS da seguinte maneira:

  1. Single Responsibility Principle (SRP): Você pode dividir sua aplicação em diferentes módulos, cada um responsável por uma única responsabilidade. Por exemplo, você pode ter um módulo de autenticação, um módulo de produtos e um módulo de pagamentos.
  2. Open-Closed Principle (OCP): Você pode criar interfaces para suas classes e permitir que elas sejam estendidas, mas não modificadas. Isso permitirá que você adicione novas funcionalidades à sua aplicação sem precisar alterar o código existente.
  3. Liskov Substitution Principle (LSP): Você pode usar herança para criar hierarquias de classes e garantir que as subclasses possam ser usadas no lugar de suas superclasses sem causar problemas.
  4. Interface Segregation Principle (ISP): Você pode criar interfaces específicas para diferentes grupos de clientes, em vez de criar uma única interface com muitos métodos que nem todos os clientes precisam.
  5. Dependency Inversion Principle (DIP): Você pode usar injeção de dependência para inverter a dependência de suas classes em outras classes concretas. Isso permitirá que você mude facilmente as implementações de suas dependências sem precisar alterar o código de suas classes.

Single Responsibility Principle (SRP) exemplo

Exemplo de como aplicar o Single Responsibility Principle (SRP) no NestJS:

Imagine que você está criando uma aplicação de e-commerce. Você pode criar um módulo de autenticação responsável por lidar com tarefas relacionadas à autenticação, como criar novas contas de usuário, fazer login e deslogar. Esse módulo teria as seguintes responsabilidades:

@Module({
  imports: [],
  controllers: [AuthController],
  providers: [AuthService],
})
export class AuthModule {}

Em seguida, você pode criar um módulo de produtos responsável por lidar com tarefas relacionadas aos produtos, como exibir a lista de produtos, exibir detalhes de um produto específico e adicionar um produto ao carrinho de compras. Esse módulo teria as seguintes responsabilidades:

@Module({
  imports: [],
  controllers: [ProductController],
  providers: [ProductService],
})
export class ProductModule {}

Dessa maneira, cada módulo tem uma única responsabilidade e fica mais fácil de manter e atualizar sua aplicação no futuro.

Open-Closed Principle (OCP) exemplo

Exemplo de como aplicar o Open-Closed Principle (OCP) no NestJS:

Imagine que você está criando um sistema de gerenciamento de tarefas. Você pode criar uma interface chamada Task com um método execute():

export interface Task {
  execute(): void;
}

Em seguida, você pode criar uma classe concreta chamada EmailTask que implementa a interface Task e envia um email:

export class EmailTask implements Task {
  constructor(private emailService: EmailService) {}

  execute(): void {
    this.emailService.sendEmail();
  }
}

Agora, se você quiser adicionar uma nova funcionalidade ao sistema de gerenciamento de tarefas, como enviar uma mensagem de texto, basta criar uma nova classe chamada SmsTask que implementa a interface Task e envia uma mensagem de texto:

export class SmsTask implements Task {
  constructor(private smsService: SmsService) {}

  execute(): void {
    this.smsService.sendSms();
  }
}

Dessa maneira, você pode adicionar novas funcionalidades ao sistema sem precisar alterar a classe EmailTask, pois ela está aberta para extensão, mas fechada para modificação.

Liskov Substitution Principle (LSP) exemplo

Exemplo de como aplicar o Liskov Substitution Principle (LSP) no NestJS:

Imagine que você está criando uma aplicação de gerenciamento de contas bancárias. Você pode criar uma classe abstrata chamada Account com um método withdraw():

export abstract class Account {
  abstract withdraw(amount: number): void;
}

Em seguida, você pode criar duas subclasses concretas: CheckingAccount e SavingsAccount. A classe CheckingAccount herda de Account e implementa o método withdraw() de acordo com as regras de saque de uma conta corrente:

export class CheckingAccount extends Account {
  withdraw(amount: number): void {
    // implementação de saque de conta corrente
  }
}

A classe SavingsAccount também herda de Account e implementa o método withdraw() de acordo com as regras de saque de uma conta poupança:

export class SavingsAccount extends Account {
  withdraw(amount: number): void {
    // implementação de saque de conta poupança
  }
}

Agora, você pode criar uma função que aceite qualquer objeto que implemente a interface Account e faça um saque:

function makeWithdraw(account: Account, amount: number): void {
  account.withdraw(amount);
}

Você pode chamar essa função passando um objeto CheckingAccount ou SavingsAccount sem problemas, pois ambos são subclasses de Account e implementam o método withdraw() de acordo com as regras específicas de cada tipo de conta. Isso é o que o Liskov Substitution Principle (LSP) nos garante.

Interface Segregation Principle (ISP) exemplo

Exemplo de como aplicar o Interface Segregation Principle (ISP) no NestJS:

Imagine que você está criando uma aplicação de gerenciamento de contatos. Você pode criar uma interface chamada Contact com os seguintes métodos:

export interface Contact {
  getName(): string;
  getEmail(): string;
  getPhone(): string;
}

Agora, imagine que você tem dois tipos de clientes para sua aplicação: empresas e pessoas físicas. As empresas têm um nome, um email e um telefone, mas as pessoas físicas têm um nome, um email e duas opções de telefone (residencial e celular).

Se você criar uma classe concreta chamada Company que implementa a interface Contact, ela precisará implementar todos os três métodos, mesmo que o método getPhone() não seja relevante para ela:

export class Company implements Contact {
  constructor(private name: string, private email: string, private phone: string) {}

  getName(): string {
    return this.name;
  }

  getEmail(): string {
    return this.email;
  }

  getPhone(): string {
    return this.phone;
  }
}

Isso viola o Interface Segregation Principle (ISP), pois a interface Contact está obrigando a classe Company a implementar métodos que ela não precisa.

Para resolver isso, você pode criar duas interfaces mais específicas: CompanyContact e PersonContact. A interface CompanyContact teria os métodos getName() e getEmail(), enquanto a interface PersonContact teria os métodos getName(), getEmail() e getPhone().

Dessa maneira, a classe Company só precisaria implementar a interface CompanyContact, enquanto a classe Person precisaria implementar a interface PersonContact. Isso garantiria que cada classe só precisaria implementar os métodos que realmente precisa, cumprindo o Interface Segregation Principle (ISP).

Dependency Inversion Principle (DIP) exemplo

Exemplo de como aplicar o Dependency Inversion Principle (DIP) no NestJS:

Imagine que você está criando uma aplicação de gerenciamento de contas bancárias. Você pode criar uma classe chamada BankAccount que depende de uma classe TransactionService para realizar transações:

export class BankAccount {
  constructor(private transactionService: TransactionService) {}

  deposit(amount: number): void {
    this.transactionService.deposit(amount);
  }

  withdraw(amount: number): void {
    this.transactionService.withdraw(amount);
  }
}

Essa abordagem viola o Dependency Inversion Principle (DIP), pois a classe BankAccount está diretamente dependente da classe concreta TransactionService. Isso pode ser um problema, pois se você quiser mudar a implementação de TransactionService, terá que alterar o código da classe BankAccount.

Para resolver isso, você pode inverter a dependência da classe BankAccount em TransactionService. Para isso, você pode criar uma interface chamada TransactionRepository com os métodos deposit() e withdraw():

export interface TransactionRepository {
  deposit(amount: number): void;
  withdraw(amount: number): void;
}

Em seguida, você pode criar a classe TransactionService para implementar a interface TransactionRepository:

export class TransactionService implements TransactionRepository {
  deposit(amount: number): void {
    // implementação de depósito
  }

  withdraw(amount: number): void {
    // implementação de saque
  }
}

Agora, você pode alterar a classe BankAccount para depende da interface TransactionRepository em vez da classe concreta TransactionService:

export class BankAccount {
  constructor(private transactionRepository: TransactionRepository) {}

  deposit(amount: number): void {
    this.transactionRepository.deposit(amount);
  }

  withdraw(amount: number): void {
    this.transactionRepository.withdraw(amount);
  }
}

Dessa maneira, a classe BankAccount fica independente da implementação concreta de TransactionService e pode ser usada com qualquer outra implementação de TransactionRepository. Isso garante o Dependency Inversion Principle (DIP).

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.