Pincípios SOLID com Javascript

Imagem para Pincípios SOLID com Javascript

It managementPostado em  Atualizado em  8 min de leitura

Olá, Perfil !!!

Vamos explorar os princípios SOLID e como eles podem ser aplicados em JavaScript para criar código mais limpo, modular e fácil de manter.

Os princípios SOLID são diretrizes de programação que podem ser utilizadas para desenvolver sistemas mais robustos e flexíveis. Neste artigo, apresentaremos exemplos práticos de como aplicar esses princípios no contexto do JavaScript.

Ao seguir esses princípios, você poderá criar um código mais escalável e fácil de manter. Se você busca desenvolver 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 para 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, tornando 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, garantindo consistência e 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, mantendo o código mais modular e fácil de manter.
  • Princípio da Inversão de Dependência (DIP): 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, o que torna o código mais flexível e fácil de testar.

Responsabilidade única (SRP)

Ao aplicar o princípio da Responsabilidade Única (SRP) em JavaScript, podemos criar um código mais modular, coeso e fácil de manter. Ao seguir esse princípio, garantimos que cada parte do código tenha uma única responsabilidade bem definida, o que aumenta a legibilidade e a qualidade do código. Isso torna a manutenção do código mais simples e reduz a possibilidade de efeitos colaterais indesejados. O SRP ajuda a evitar o acoplamento excessivo entre diferentes partes do sistema, facilitando o reuso e a atualização do código. Em resumo, a aplicação do SRP ajuda a criar um código mais organizado e eficiente.

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
  }
}

No exemplo, a classe TasksList possui apenas uma responsabilidade, que é gerenciar as tarefas adicionadas a uma lista. A classe contém 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. Essa abordagem torna a classe mais fácil de entender e manter, reduzindo a complexidade e tornando-a mais modular. Ao seguir o princípio da Responsabilidade Única, tornamos nosso código mais coeso e aumentamos a sua qualidade, evitando que a classe se torne excessivamente grande e difícil de entender.

Aberto/Fechado (OCP)

Ao aplicar o princípio Aberto/Fechado (OCP) em JavaScript, podemos criar um código mais reutilizável e flexível. Ao seguir esse princípio, permitimos que as classes sejam estendidas sem precisar serem modificadas, tornando a manutenção do código mais fácil. Isso ajuda a garantir a qualidade e escalabilidade do código. A extensão de classes também ajuda a evitar a duplicação de código e promove a reutilização de código existente, tornando o desenvolvimento mais eficiente. Em resumo, ao aplicar o OCP, tornamos nosso código mais modular, extensível e fácil de manter, o que resulta em um código mais robusto e de alta qualidade.

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 é fundamental 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, o que consequentemente leva a um aumento na qualidade e produtividade do seu trabalho. Além disso, seguir os princípios pode ajudá-lo a evitar a duplicação de código, a reduzir a complexidade do seu código e a torná-lo mais modular, o que ajuda a facilitar a manutenção do código a longo prazo. Em resumo, ao aplicar os princípios SOLID em sua programação, você está investindo na qualidade do seu código e no sucesso do seu projeto.

Referências