SOLID é uma sigla que todo desenvolvedor conhece mas poucos aplicam de forma consistente. Os cinco princípios existem porque código que os viola fica difícil de mudar, testar, e estender. Aqui está cada um com um problema real e a solução.
S: Single Responsibility Principle
Uma classe ou função deve ter um motivo para mudar. Quando uma função faz muita coisa, qualquer mudança em uma dessas coisas arrisca quebrar as outras.
// ❌ Faz demais: muda por múltiplos motivos
class UserService {
async createUser(data: CreateUserDto) {
const user = await this.db.user.create({ data })
await this.sendWelcomeEmail(user.email)
await this.auditLog('user.created', user.id)
return user
}
}
// ✅ Cada responsabilidade separada
class UserService {
constructor(
private readonly userRepo: UserRepository,
private readonly emailService: EmailService,
private readonly auditService: AuditService
) {}
async createUser(data: CreateUserDto) {
const user = await this.userRepo.create(data)
await this.emailService.sendWelcome(user.email)
await this.auditService.log('user.created', user.id)
return user
}
}
A versão correta tem três razões para mudar: repositório de dados, template de email, ou política de auditoria. A versão errada tem uma razão para cada mudança de qualquer uma dessas três.
O: Open/Closed Principle
Entidades de software devem estar abertas para extensão e fechadas para modificação. Adicione comportamento sem alterar código existente.
// ❌ Para adicionar tipo de pagamento, modifica a classe
class PaymentProcessor {
process(type: string, amount: number) {
if (type === 'credit_card') { /* ... */ }
else if (type === 'pix') { /* ... */ }
// adicionar 'boleto' requer modificar este método
}
}
// ✅ Adiciona comportamento sem modificar código existente
interface PaymentMethod {
process(amount: number): Promise<PaymentResult>
}
class CreditCardPayment implements PaymentMethod {
async process(amount: number) { /* ... */ }
}
class PixPayment implements PaymentMethod {
async process(amount: number) { /* ... */ }
}
// Para adicionar BoletoPayment, cria uma nova classe
// Nenhum código existente é modificado
L: Liskov Substitution Principle
Subtipos devem ser substituíveis por seus tipos base sem alterar o comportamento correto do programa. Se você precisa de instanceof para decidir comportamento, violou LSP.
// ❌ Subtipo quebrando contrato
class Rectangle {
constructor(protected width: number, protected height: number) {}
setWidth(w: number) { this.width = w }
setHeight(h: number) { this.height = h }
area() { return this.width * this.height }
}
class Square extends Rectangle {
setWidth(w: number) { this.width = w; this.height = w }
setHeight(h: number) { this.width = h; this.height = h }
}
// Square não pode ser substituído por Rectangle sem quebrar expectativas
function increaseWidth(rect: Rectangle) {
rect.setWidth(rect.width + 1) // Square muda height também
}
// ✅ Composição ou hierarquia que preserva contrato
interface Shape {
area(): number
scale(factor: number): Shape
}
I: Interface Segregation Principle
Clientes não devem ser forçados a depender de interfaces que não usam. Interfaces grandes forçam implementações que contêm código morto.
// ❌ Interface gigante: força implementações incompletas
interface DataStore {
read(id: string): Promise<any>
write(id: string, data: any): Promise<void>
delete(id: string): Promise<void>
subscribe(event: string, cb: Function): void
connect(): Promise<void>
}
// ✅ Interfaces segregadas
interface Reader {
read(id: string): Promise<any>
}
interface Writer {
write(id: string, data: any): Promise<void>
}
interface EventSource {
subscribe(event: string, cb: Function): void
}
// Cada implementação depende só do que usa
D: Dependency Inversion Principle
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.
// ❌ Alto nível depende de baixo nível
class OrderService {
constructor() {
this.db = new PrismaClient() // acoplamento direto
}
}
// ✅ Ambos dependem de abstração
interface OrderRepository {
save(order: Order): Promise<void>
findById(id: string): Promise<Order | null>
}
class OrderService {
constructor(private readonly repo: OrderRepository) {}
}
// PrismaUserRepository implementa OrderRepository
// Em testes, usa InMemoryOrderRepository
Como aplicar no dia a dia
SOLID não é para aplicar em todo código. É para aplicar onde o custo de mudança é alto: domínio de negócio, camadas de serviço, pontos de extensão. Utility functions, helpers simples, e código de configuração não precisam de SOLID.
A pergunta que guia: "Se eu precisar mudar X, quantas outras coisas vão quebrar?" Se a resposta é muitas, refatore. Se é poucas, está bom o suficiente.
Curtiu o conteúdo?
Construo produtos web e soluções com IA do jeito certo — arquitetura sólida, código sustentável e entrega real.
Vamos conversar