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