SOLID: Melhores Práticas para Desenvolvedores Seniores – Guia Completo

SOLID: Melhores Práticas para Desenvolvedores Seniores – Guia Completo





Melhores Práticas de SOLID para Seniors



Melhores Práticas de SOLID para Seniors

Guia técnico objetivo para equipes maduras manterem a qualidade do software, com foco em manutenibilidade, evolução de código e decisões de design fundamentadas.


1) SRP — Princípio da Responsabilidade Única

Em bases de código extensas, manter cada unidade com uma única responsabilidade reduz acoplamento, facilita testes e torna a evolução menos arriscada. Para seniors, o desafio é identificar fronteiras claras entre domínio, infraestrutura e orquestração sem inflar módulos com múltiplas motivações.

  • Divida classes e módulos por responsabilidade de domínio, evitando God Objects que agrupem lógica de persistência, validação, apresentação e orquestração em uma única classe.
  • Crie serviços, repositórios e validadores independentes que exponham apenas o que é necessário.
  • Justifique cada unidade pela mudança que a origem de uma mudança de requisito causaria.
  • Escreva testes unitários para cada responsabilidade de forma isolada.

Exemplo rápido de refatoração (TypeScript):

// Violação de SRP
class UserManager {
  fetchUser(id: string) { /* ... */ }
  saveUser(user: any) { /* ... */ }
  validateUser(user: any) { /* ... */ }
  log(action: string, msg: string) { /* ... */ }
}

// SRP aplicado
class UserRepository {
  fetchUser(id: string) { /* ... */ }
  save(user: any) { /* ... */ }
}
class UserValidator {
  isValid(user: any): boolean { /* ... */ }
}
class UserService {
  constructor(private repo: UserRepository, private validator: UserValidator) {}
  saveIfValid(user: any) {
    if (this.validator.isValid(user)) this.repo.save(user);
  }
}

2) OCP — Aberto para Extensão, Fechado para Modificação

OCP orienta a adicionar comportamento sem tocar no código existente. Em projetos com evolução constante, isso evita regressões e facilita a introdução de novas estratégias ou formatos de saída.\nPara seniors, a prática é contemplar extensões através de abstrações bem definidas, evitando mudanças diretas em classes que já estão estáveis.

  • Prefira interfaces/abonstrações que permitam novas implementações sem alterar a API existente.
  • Utilize padrões como Strategy, Factory e Plugins para estender comportamentos de forma controlada.
  • Avalie o custo de abstrações: elas devem trazer clareza e ganho de testabilidade, não complexidade desnecessária.

Exemplo de extensão via estratégia (TypeScript):

interface RenderingStrategy {
  render(data: any): string;
}
class JsonRendering implements RenderingStrategy {
  render(data: any) { return JSON.stringify(data); }
}
class XmlRendering implements RenderingStrategy {
  render(data: any) { /* converte para XML */ return "<xml>..."; }
}
class Presenter {
  constructor(private strategy: RenderingStrategy) {}
  present(data: any) { return this.strategy.render(data); }
}

// Uso
const p1 = new Presenter(new JsonRendering());
console.log(p1.present({ id: 1 }));
const p2 = new Presenter(new XmlRendering());
console.log(p2.present({ id: 1 }));

3) LSP — Substituição de Liskov

Subtipos devem poder substituir seus tipos base sem alterar as propriedades desejáveis do programa. Violação comum aparece quando subclasses alteram contratos ou invariantes esperados pelos clientes.

  • Garanta contratos consistentes entre classe base e derivadas.
  • Evite sobregravar comportamentos que violem invariantes do base.
  • Prefira composição sobre herança para evitar dependência implícita de estado interno.

Exemplo clássico (violação de LSP) e correção:

// Violação de LSP
class Rectangle { constructor(protected w: number, protected h: number) {} setWidth(w: number) { this.w = w; } setHeight(h: number) { this.h = h; } area(): number { return this.w * this.h; } }
class Square extends Rectangle {
  setWidth(w: number) { super.setWidth(w); this.h = w; } // quebra invariante: largura e altura devem manter relação
}

// Correção: evitar herança quando não se pode manter o contrato
interface Shape { area(): number; }
class RectangleShape implements Shape { constructor(private w: number, private h: number) {} area() { return this.w * this.h; } }
class SquareShape implements Shape { constructor(private side: number) {} area() { return this.side * this.side; } }

4) DIP — Dependência de Abstrações

O DIP sugere que módulos de alto nível não devem depender de módulos de baixo nível; ambos devem depender de abstrações. Em equipes seniores, isso se traduz em desacoplamento explícito, injeção de dependências e uso de contêineres simples ou factories para gerenciar dependências.

  • Dependa de interfaces, não de implementações concretas.
  • Injete dependências via construtor, setters ou provedores de serviço.
  • Considere o uso de um DI container mínimo ou padrões de factory para facilitar testes e evolução.

Exemplo de DI simples (TypeScript):

interface IDataSource {
  fetch(id: string): Promise;
}
class ApiSource implements IDataSource {
  async fetch(id: string) { /* chamada à API */ return { id }; }
}
class UserService {
  constructor(private dataSource: IDataSource) {}
  async getUser(id: string) {
    return this.dataSource.fetch(id);
  }
}

// Injeção manual (padrão simples)
const api = new ApiSource();
const service = new UserService(api);
service.getUser("123").then(console.log);

Gostou deste guia técnico? Continue explorando o conhecimento de software aplicado em equipes seniores. Abaixo estão sugestões de leitura para aprofundar ainda mais seus estudos sobre SOLID e design de software.