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.
Sou Apaixonado pela programação e estou trilhando o caminho de ter cada diz mais conhecimento e trazer toda minha experiência vinda do Design para a programação resultando em layouts incríveis e idéias inovadoras! Conecte-se Comigo!