diogodev_
Conteudo

10 de março de 20264 minutos de leitura

Introdução aos princípios SOLID

Os princípios SOLID são um conjunto de boas práticas de design de software que ajudam a criar sistemas mais organizados, flexíveis e fáceis de manter. Eles são amplamente utilizados no desenvolvimento orientado a objetos e são considerados uma base importante para escrever código limpo e sustentável.

Contexto

Conforme aplicações crescem, é comum surgirem problemas como:

  • código difícil de manter
  • classes com muitas responsabilidades
  • dependências complexas entre componentes

Para ajudar a resolver esses problemas, surgiram princípios de design que orientam como estruturar melhor o software. Entre eles, os princípios SOLID são alguns dos mais conhecidos na engenharia de software moderna.

Os princípios do SOLID

S — Princípio da Responsabilidade Única (SRP)

O Single Responsibility Principle afirma que uma classe deve ter apenas uma razão para mudar.

Ou seja, cada classe deve possuir apenas uma responsabilidade dentro do sistema.

Exemplo ruim

Neste exemplo, a mesma classe cria usuários e também envia emails.

class UserService {
createUser(user) {
console.log("Usuário criado")
}

sendEmail(user) {
console.log("Enviando email para usuário")
}

}

O problema aqui é que a classe possui duas responsabilidades diferentes.

Se a lógica de envio de email mudar, precisamos alterar essa classe.

Exemplo aplicando SRP

class UserService {
createUser(user) {
console.log("Usuário criado")
}
}

class EmailService {
sendEmail(user) {
console.log("Enviando email para usuário")
}
}

Agora cada classe possui uma única responsabilidade, facilitando manutenção e testes.

O — Princípio do Aberto/Fechado (OCP)

O Open/Closed Principle diz que entidades devem estar abertas para extensão, mas fechadas para modificação.

Isso significa que devemos conseguir adicionar novos comportamentos sem modificar código existente.

Exemplo ruim

class DiscountService {
calculate(type) {
if(type === "regular") {
return 10
}

if(type === "premium") {
return 20
}
}
}

Toda vez que surgir um novo tipo de desconto precisamos modificar a classe.

Exemplo aplicando OCP

class RegularDiscount {
calculate() {
return 10
}
}

class PremiumDiscount {
calculate() {
return 20
}
}

class DiscountService {
calculate(discountStrategy) {
return discountStrategy.calculate()
}
}

Agora podemos criar novos tipos de desconto sem alterar o código existente.

L — Princípio da Substituição de Liskov (LSP)

Proposto por Barbara Liskov, esse princípio afirma que uma classe filha deve poder substituir a classe pai sem quebrar o comportamento esperado.

Exemplo ruim

class Bird {
fly() {
console.log("Voando")
}
}

class Penguin extends Bird {
fly() {
throw new Error("Pinguins não voam")
}
}

Aqui temos um problema: nem todo pássaro voa.

Exemplo corrigido

class Bird {}

class FlyingBird extends Bird {
fly() {
console.log("Voando")
}
}

class Penguin extends Bird {}

class Eagle extends FlyingBird {}

Agora apenas pássaros que voam possuem o método fly(), respeitando o princípio.

I — Princípio da Segregação de Interfaces (ISP)

O Interface Segregation Principle diz que uma classe não deve ser forçada a implementar métodos que não utiliza.

Exemplo ruim

interface Worker {
work()
eat()
sleep()
}

class Robot implements Worker {
work() {
console.log("Trabalhando")
}

eat() {
throw new Error("Robô não come")
}

sleep() {
throw new Error("Robô não dorme")
}
}

A interface obriga a classe a implementar métodos que não fazem sentido.

Exemplo aplicando ISP

interface Workable {
work()
}

interface Eatable {
eat()
}

class Human implements Workable, Eatable {
work() {
console.log("Trabalhando")
}

eat() {
console.log("Comendo")
}
}

class Robot implements Workable {
work() {
console.log("Trabalhando")
}
}

Agora cada classe implementa apenas o que precisa.

D — Princípio da Inversão de Dependência (DIP)

O Dependency Inversion Principle afirma que:

  • módulos de alto nível não devem depender de módulos de baixo nível
  • ambos devem depender de abstrações

Exemplo ruim

class MySQLDatabase {
save(data) {
console.log("Salvando no MySQL")
}
}

class UserService {
constructor() {
this.database = new MySQLDatabase()
}

save(user) {
this.database.save(user)
}
}

Aqui o UserService depende diretamente do MySQL.

Exemplo aplicando DIP

interface Database {
save(data)
}

class MySQLDatabase implements Database {
save(data) {
console.log("Salvando no MySQL")
}
}

class MongoDatabase implements Database {
save(data) {
console.log("Salvando no MongoDB")
}
}

class UserService {
constructor(database) {
this.database = database
}

save(user) {
this.database.save(user)
}
}

Agora podemos trocar o banco facilmente:

new UserService(new MySQLDatabase())

ou

new UserService(new MongoDatabase())

Conclusão

Os princípios SOLID ajudam a estruturar melhor o código e a criar sistemas mais fáceis de manter, testar e evoluir.

Ao aplicar esses princípios, conseguimos:

  • reduzir acoplamento
  • aumentar coesão
  • facilitar manutenção
  • permitir evolução do sistema sem quebrar funcionalidades existentes

Embora nem sempre seja necessário aplicar todos os princípios em todas as situações, compreender SOLID é um passo importante para escrever software de qualidade.