Pincípios SOLID com Javascript

Imagem para Pincípios SOLID com Javascript

It managementPostado em  7 min de leitura

Hi, Cyaanns !!!

Vamos falar sobre os princípios SOLID e sua aplicação em JavaScript.

Os princípios SOLID são um conjunto de diretrizes de programação que ajudam a criar código mais limpo, modular e fácil de manter.

Aqui iremos ver alguns exemplos práticos de como esses princípios podem ser aplicados no contexto do JavaScript, ajudando a manter a flexibilidade e a escalabilidade do código. Se você está interessado em escrever código de alta qualidade em JavaScript, este artigo é uma leitura valiosa para você.

Princípios

  • Princípio da Responsabilidade Única (SRP): Uma classe ou função deve ter apenas uma responsabilidade. Isso ajuda a manter o código mais flexível e fácil de manter.
  • Princípio Aberto/Fechado (OCP): Uma classe deve estar aberta para extensão, mas fechada para modificação. Isso ajuda a manter o código mais modular e menos propenso a erros.
  • Princípio da Substituição de Liskov (LSP): Uma classe derivada deve poder ser usada no lugar de sua classe base sem afetar a correção do programa. Isso ajuda a garantir a consistência e a reutilização do código.
  • Princípio da Segregação de Interface (ISP): Um cliente não deve ser forçado a depender de métodos que não usa. Isso ajuda a manter o código mais modular e fácil de manter.
  • Princípio da Inversão de Dependência (DIP): Os módulos de alto nível não devem depender de módulos de baixo nível. Em vez disso, ambos devem depender de abstrações. Isso ajuda a manter o código mais flexível e fácil de testar.

Responsabilidade única (SRP)

Ao aplicar o SRP em JavaScript, podemos criar código mais modular, coeso e fácil de manter. Além disso, seguindo este princípio, conseguimos garantir que cada parte do nosso código tenha uma única responsabilidade bem definida, o que aumenta a legibilidade e a qualidade do código.

class TasksList {
  constructor() {
    this.tasks = []
  }

  addTask(task) {
    this.tasks.push(task)
  }

  removeTask(task) {
    const index = this.tasks.indexOf(task)
    if (index !== -1) {
      this.tasks.splice(index, 1)
    }
  }

  completeTask(task) {
    task.concluida = true
  }
}

Nesse exemplo, a classe TasksList tem apenas uma responsabilidade, que é gerenciar as tarefas adicionadas a uma lista. A classe tem métodos específicos para adicionar, remover e marcar uma tarefa como concluída, garantindo que cada método tenha uma única responsabilidade bem definida. Isso torna a classe mais fácil de entender e manter.

Aberto/Fechado (OCP)

Ao aplicar o OCP em JavaScript, podemos criar código mais reutilizável e flexível, pois as classes podem ser estendidas sem precisar serem modificadas. Isso facilita a manutenção do código e ajuda a garantir a sua qualidade e escalabilidade. Além disso, a extensão de classes pode ajudar a evitar a duplicação de código e a promover a reutilização de código existente.

class Animal {
  constructor(name) {
    this.name = name
  }

  makeSound() {
    console.log(`${this.name} faz um som.`)
  }
}

class Dog extends Animal {
  constructor(name) {
    super(name)
  }

  makeSound() {
    console.log(`${this.name} late.`)
  }
}

class Cat extends Animal {
  constructor(name) {
    super(name)
  }

  makeSound() {
    console.log(`${this.name} mia.`)
  }
}

class Cow extends Animal {
  constructor(name) {
    super(name)
  }
}

class AnimalSoundMaker {
  constructor(animals) {
    this.animals = animals
  }

  makeSounds() {
    this.animals.forEach((animal) => animal.makeSound())
  }
}

A classe Animal é fechada para modificação, mas aberta para extensão. As classes Dog, Cat e Cow estendem a classe Animal e adicionam seus próprios sons. A classe AnimalSoundMaker recebe uma lista de animais e chama o método makeSound de cada animal. Esta classe não precisa ser modificada se um novo animal for adicionado, desde que o animal implemente o método makeSound.

Substituição de Liskov (LSP)

Suponha que você tenha uma classe Car que define um carro genérico e uma subclasse ElectricCar que herda da classe Car. Ambas as classes têm um método drive() que controla a movimentação do carro correspondente.

De acordo com o princípio de substituição de Liskov, a subclasse ElectricCar deve ser substituível pela classe Car sem afetar a lógica. Isso significa que qualquer método que espera uma instância da classe Car deve poder trabalhar corretamente com uma instância da subclasse ElectricCar.

No entanto, se a implementação do método drive() na subclasse ElectricCar for diferente da implementação na classe Car, isso pode violar o princípio de substituição de Liskov.

A classe Car tenha a seguinte implementação do método drive():

class Car {
  constructor() {
    this.speed = 0
  }

  drive(speed) {
    this.speed = speed
    console.log(`Car is driving at ${this.speed} km/h`)
  }
}

E a subclasse ElectricCar tenha a seguinte implementação do método drive():

class ElectricCar extends Car {
  constructor() {
    super()
  }

  drive(speed) {
    if (speed > 100) {
      console.log('Electric car cannot drive that fast')
    } else {
      super.drive(speed)
    }
  }
}

Nesse caso, se um método espera uma instância da classe Car e recebe uma instância da subclasse ElectricCar, o controle de movimentação do carro pode ser incorreto, violando o princípio de substituição de Liskov.

Para corrigir isso, a implementação do método drive() na subclasse ElectricCar deve ser consistente com a implementação na classe Car. Por exemplo, a implementação na subclasse ElectricCar poderia ser a seguinte:

class ElectricCar extends Car {
  constructor() {
    super()
  }

  drive(speed) {
    super.drive(Math.min(speed, 100))
  }
}

Nesse caso, a subclasse ElectricCar ainda controla a movimentação do carro, mas de uma forma consistente com a implementação na classe Car. Se a velocidade solicitada for maior do que 100 km/h, a subclasse ElectricCar ajusta automaticamente a velocidade para 100 km/h, em vez de emitir uma mensagem de erro.

Segregação de interface (ISP)

Se você estiver trabalhando em um e-commerce que envolve um módulo de carrinho de compras, um módulo de processamento de pagamento e um módulo de gerenciamento de estoque. Em vez de cada módulo depender diretamente dos outros, você pode definir interfaces que descrevem as operações que cada módulo oferece e que devem ser chamadas pelos outros módulos.

interface ShoppingCart {
  addItem(item: Item): void;
  removeItem(item: Item): void;
  getItems(): Item[];
  clear(): void;
}

A interface para o módulo de processamento de pagamento pode ser assim:

interface PaymentProcessor {
  authorizePayment(payment: Payment): boolean;
  capturePayment(payment: Payment): boolean;
  voidPayment(payment: Payment): boolean;
}

E a interface para o módulo de gerenciamento de estoque pode ser assim:

interface InventoryManager {
  getStock(item: Item): number;
  reserveStock(item: Item, quantity: number): boolean;
  releaseStock(item: Item, quantity: number): boolean;
}

Dessa forma, cada módulo só precisa conhecer as interfaces que ele depende e pode se comunicar com outros módulos por meio dessas interfaces. Isso permite que cada módulo seja desenvolvido e testado de forma independente e facilita a manutenção e evolução do sistema como um todo.

Inversão de Dependência (DIP)

Vamos supor que você está trabalhando em um sistema de vendas online que tem um módulo de pagamento que utiliza um serviço externo para processar os pagamentos. Nesse caso, você pode criar uma classe PaymentService que depende diretamente do serviço externo:

class PaymentService {
  constructor() {
    this.paymentGateway = new PaymentGateway()
  }

  processPayment(order) {
    // chama o serviço externo para processar o pagamento
    this.paymentGateway.processPayment(order.total, order.customerInfo)
  }
}

Essa implementação viola o princípio de inversão de dependência (DIP) porque a classe PaymentService depende diretamente de uma implementação concreta do serviço externo, o que torna o código inflexível e difícil de manter.

Para corrigir isso, você pode inverter a dependência e criar uma interface PaymentGatewayInterface que a classe PaymentService irá depender. Essa interface pode conter apenas os métodos que a classe PaymentService utiliza:

class PaymentService {
  constructor(paymentGateway) {
    this.paymentGateway = paymentGateway
  }

  processPayment(order) {
    // chama o método `processPayment` da interface `PaymentGatewayInterface`
    this.paymentGateway.processPayment(order.total, order.customerInfo)
  }
}

Nesse caso, a classe PaymentService agora depende de uma interface PaymentGatewayInterface ao invés de depender diretamente da implementação concreta do serviço externo. Isso permite que você mude o serviço externo no futuro sem ter que alterar o código da classe PaymentService. Para mudar o serviço externo, você só precisa criar uma nova classe que implementa a interface PaymentGatewayInterface:

class PaypalGateway implements PaymentGatewayInterface {
  processPayment(amount, customerInfo) {
    // chama a API do Paypal para processar o pagamento
    // ...
  }
}

const paypalGateway = new PaypalGateway()
const paymentService = new PaymentService(paypalGateway)

Dessa forma, você pode alternar entre diferentes serviços externos de pagamento sem ter que alterar o código da classe PaymentService. Isso é uma aplicação do princípio de inversão de dependência, que prega que as dependências devem ser abstratas e depender de interfaces ao invés de depender de implementações específicas.

Conclusão

Seguir os princípios SOLID é essencial para criar um código de alta qualidade e fácil de manter, que pode ser estendido e modificado com facilidade. Ao aplicar esses princípios em sua programação, você pode melhorar a eficiência e a escalabilidade do seu código e, consequentemente, aumentar a qualidade e a produtividade do seu trabalho.

Referências

Artigos relacionados