Introdução: SOLID no ecossistema JavaScript
SOLID é um acrônimo de cinco princípios fundamentais de design orientado a objetos, formulados por Robert Martin. No contexto do JavaScript, especialmente ao desenvolver em Node.js com TypeScript e frameworks modernos (NestJS, Express), esses princípios se tornam criticamente importantes para construir aplicações escaláveis e sustentáveis. Em nível avançado, o SOLID vai além de classes e interfaces simples, abordando a arquitetura de módulos, injeção de dependências e testes.
Neste artigo, examinaremos cada princípio SOLID com exemplos de código para Node.js, focando em nuances práticas e erros típicos. Você aprenderá como aplicar SOLID em projetos reais, usando os recursos do JavaScript (ES6+) e TypeScript.
1. Princípio da Responsabilidade Única (SRP) em JavaScript
Teoria: Uma classe ou módulo deve ter apenas um motivo para mudar. Em JavaScript, isso é frequentemente violado quando lógica de negócios, trabalho com banco de dados e formatação de respostas são misturados em um único arquivo.
Prática (Node.js): Separe as responsabilidades no nível de serviços e repositórios.
// ❌ Violação do SRP: a classe processa requisições, valida e salvaclass UserService { createUser(data) { if (!data.email) throw new Error('Email required'); const db = new Database(); return db.save('users', data); }}
// ✅ Cumprimento do SRP: separação em validador, repositório e serviçoclass UserValidator { validate(data) { if (!data.email) throw new Error('Email required'); }}
class UserRepository { constructor(db) { this.db = db; } save(data) { return this.db.save('users', data); }}
class UserService { constructor(validator, repository) { this.validator = validator; this.repository = repository; } createUser(data) { this.validator.validate(data); return this.repository.save(data); }}Use injeção de dependências (DI) para passar o validador e o repositório para o serviço. Isso simplifica os testes e a substituição de componentes.
2. Princípio do Aberto/Fechado (OCP) através de polimorfismo
Teoria: As classes devem estar abertas para extensão, mas fechadas para modificação. Em JavaScript, isso é alcançado por meio de herança, composição ou estratégias.
Prática (Node.js com TypeScript): Use classes abstratas ou interfaces para definir um contrato.
// Classe abstrata para relatóriosabstract class ReportGenerator { abstract generate(data: any): string;}
class PDFReport extends ReportGenerator { generate(data: any): string { return `PDF: ${JSON.stringify(data)}`; }}
class CSVReport extends ReportGenerator { generate(data: any): string { return `CSV: ${data.join(',')}`; }}
// O código cliente não muda ao adicionar novos formatosclass ReportService { constructor(private generator: ReportGenerator) {} createReport(data: any) { return this.generator.generate(data); }}Para JavaScript puro, use fábricas ou estratégias (padrão Strategy) para evitar modificar o código existente.
3. Princípio da Substituição de Liskov (LSP) em código assíncrono
Teoria: Objetos de uma classe base devem ser substituíveis por objetos de uma classe derivada sem quebrar o programa. Em JavaScript, o LSP é frequentemente violado ao sobrescrever métodos com contratos diferentes (por exemplo, tipos de retorno ou exceções).
Prática (Node.js com async/await): Certifique-se de que as subclasses não enfraqueçam as pré-condições nem fortaleçam as pós-condições.
class Database { async connect() { return 'con