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:
- 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.
- 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.
- 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.
- 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.
- 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).