Princípios SOLID em JavaScript: teoria e prática de A a Z
SOLID é a sigla de cinco princípios fundamentais da programação orientada a objetos, formulados por Robert Martin (Tio Bob). Embora JavaScript não seja uma linguagem OOP clássica (ela usa herança prototípica), esses princípios também se aplicam a ela, especialmente no contexto de Node.js e frameworks modernos. Seguir o SOLID torna o código mais flexível, testável e sustentável. Neste artigo, vamos analisar cada princípio com exemplos práticos em JavaScript.
1. S — Single Responsibility Principle (Princípio da Responsabilidade Única)
Uma classe ou módulo deve ter apenas um motivo para ser alterado. Em outras palavras, cada entidade deve ser responsável por uma tarefa específica.
Exemplo ruim: A classe User gerencia simultaneamente os dados do usuário e envia notificações por e-mail.
class User { constructor(name, email) { this.name = name; this.email = email; }
saveToDatabase() { // Lógica de salvamento no BD console.log(`Salvando usuário ${this.name} no BD`); }
sendWelcomeEmail() { // Lógica de envio de e-mail console.log(`Enviando saudação para ${this.email}`); }}Exemplo bom: Dividimos a responsabilidade em duas classes.
class User { constructor(name, email) { this.name = name; this.email = email; }}
class UserRepository { save(user) { console.log(`Salvando usuário ${user.name} no BD`); }}
class EmailService { sendWelcome(user) { console.log(`Enviando saudação para ${user.email}`); }}
// Usoconst user = new User('João', 'joao@example.com');const repo = new UserRepository();const emailService = new EmailService();
repo.save(user);emailService.sendWelcome(user);2. O — Open-Closed Principle (Princípio Aberto/Fechado)
Entidades de software devem estar abertas para extensão, mas fechadas para modificação. Isso significa que novas funcionalidades devem ser adicionadas através de novo código, e não alterando o código existente.
Exemplo ruim: A classe AreaCalculator requer modificação ao adicionar uma nova forma.
class AreaCalculator { calculate(shape) { if (shape.type === 'circle') { return Math.PI * shape.radius ** 2; } else if (shape.type === 'rectangle') { return shape.width * shape.height; } // Ao adicionar uma nova forma, é necessário modificar este método }}Exemplo bom: Usamos polimorfismo e herança.
class Shape { area() { throw new Error('O método area() deve ser sobrescrito'); }}
class Circle extends Shape { constructor(radius) { super(); this.radius = radius; }
area() { return Math.PI * this.radius ** 2; }}
class Rectangle extends Shape { constructor(width, height) { super(); this.width = width; this.height = height; }
area() { return this.width * this.height; }}
class AreaCalculator { calculate(shape) { return shape.area(); }}
// Usoconst calculator = new AreaCalculator();const circle = new Circle(5);const rectangle = new Rectangle(4, 6);
console.log(calculator.calculate(circle)); // 78.54console.log(calculator.calculate(rectangle)); // 243. L — Liskov Substitution Principle (Princípio da Substituição de Liskov)
Objetos em um programa devem ser substituíveis por instâncias de suas subclasses sem alterar a corretude do programa. Isso significa que as subclasses devem respeitar o comportamento esperado das classes base.
Exemplo ruim: A classe Square estende Rectangle mas quebra o comportamento esperado.
class Rectangle { constructor(width, height) { this.width = width; this.height = height; }
setWidth(width) { this.width = width; }
setHeight(height) { this.height = height; }
area() { return this.width * this.height; }}
class Square extends Rectangle { setWidth(width) { this.width = width; this.height = width; // Mantém a proporção de um quadrado }
setHeight(height) { this.width = height; this.height = height; }}
// Uso problemáticofunction increaseRectangleWidth(rectangle) { rectangle.setWidth(rectangle.width + 1);}
const square = new Square(5, 5);increaseRectangleWidth(square);console.log(square.area()); // Esperado: 30, mas obtém: 36 (pois a altura também foi alterada)Exemplo bom: Criamos uma abstração separada para formas que podem ter largura e altura definidas independentemente.
class Shape { area() { throw new Error('Método area() deve ser sobrescrito'); }}
class Rectangle extends Shape { constructor(width, height) { super(); this.width = width; this.height = height; }
setWidth(width) { this.width = width; }
setHeight(height) { this.height = height; }
area() { return this.width * this.height; }}
class Square extends Shape { constructor(side) { super(); this.side = side; }
setSide(side) { this.side = side; }
area() { return this.side ** 2; }}
// Uso corretoconst square = new Square(5);square.setSide(6);console.log(square.area()); // 364. I — Interface Segregation Principle (Princípio da Segregação de Interface)
Muitas interfaces específicas são melhores do que uma interface geral. As classes não devem ser forçadas a implementar métodos que não utilizam.
Exemplo ruim: Uma interface geral Worker obriga todas as classes a implementar métodos que podem não ser necessários.
class Worker { work() { throw new Error('Método work() deve ser sobrescrito'); }
eat() { throw new Error('Método eat() deve ser sobrescrito'); }
sleep() { throw new Error('Método sleep() deve ser sobrescrito'); }}
class Human extends Worker { work() { console.log('Trabalhando'); } eat() { console.log('Comendo'); } sleep() { console.log('Dormindo'); }}
class Robot extends Worker { work() { console.log('Trabalhando'); } eat() { throw new Error('Robôs não comem'); } // Violação sleep() { throw new Error('Robôs não dormem'); } // Violação}Exemplo bom: Dividimos em interfaces menores e específicas.
class Workable { work() { throw new Error('Método work() deve ser sobrescrito'); }}
class Eatable { eat() { throw new Error('Método eat() deve ser sobrescrito'); }}
class Sleepable { sleep() { throw new Error('Método sleep() deve ser sobrescrito'); }}
class Human extends Workable { work() { console.log('Trabalhando'); }}
// Human também pode implementar Eatable e Sleepableclass HumanFull extends Human { eat() { console.log('Comendo'); } sleep() { console.log('Dormindo'); }}
class Robot extends Workable { work() { console.log('Trabalhando'); }}
// Usoconst human = new HumanFull();human.work();human.eat();human.sleep();
const robot = new Robot();robot.work();// robot.eat(); // Erro: método não existe, o que é correto5. D — Dependency Inversion Principle (Princípio da Inversão de Dependência)
Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações. Abstrações não devem depender de detalhes. Detalhes devem depender de abstrações.
Exemplo ruim: A classe OrderService depende diretamente de uma implementação concreta de banco de dados.
class MySQLDatabase { save(order) { console.log(`Salvando pedido ${order.id} no MySQL`); }}
class OrderService { constructor() { this.database = new MySQLDatabase(); // Dependência direta }
saveOrder(order) { this.database.save(order); }}Exemplo bom: Dependemos de uma abstração (interface) e injetamos a dependência.
class Database { save(order) { throw new Error('Método save() deve ser sobrescrito'); }}
class MySQLDatabase extends Database { save(order) { console.log(`Salvando pedido ${order.id} no MySQL`); }}
class PostgreSQLDatabase extends Database { save(order) { console.log(`Salvando pedido ${order.id} no PostgreSQL`); }}
class OrderService { constructor(database) { this.database = database; // Injeção de dependência }
saveOrder(order) { this.database.save(order); }}
// Usoconst mysqlDb = new MySQLDatabase();const postgresDb = new PostgreSQLDatabase();
const orderService1 = new OrderService(mysqlDb);orderService1.saveOrder({ id: 1 }); // Salvando pedido 1 no MySQL
const orderService2 = new OrderService(postgresDb);orderService2.saveOrder({ id: 2 }); // Salvando pedido 2 no PostgreSQLConclusão
Os princípios SOLID são ferramentas poderosas para criar código limpo, sustentável e escalável em JavaScript. Embora alguns princípios possam parecer abstratos no início, a prática constante ajuda a internalizá-los. Lembre-se: o objetivo não é seguir cegamente as regras, mas entender os problemas que cada princípio resolve e aplicá-los quando fizer sentido para o seu contexto.
Comece com o Princípio da Responsabilidade Única (S) — é o mais fácil de implementar e já trará melhorias significativas na organização do seu código. Depois, gradualmente, incorpore os outros princípios conforme ganhar experiência.