Robert C. Martin cunhou a sigla SOLID em Agile Software Development: Principles, Patterns, and Practices (Prentice Hall, 2002). Cada letra nomeia uma regra de design que responde a um tipo de dor de manutenção. Cada seção abaixo mostra o padrão de código que viola o princípio, a versão corrigida e o custo que isso muda.
S: responsabilidade única
Uma classe ou função muda por um motivo só. Quando ela concentra gravação de dado, envio de email e auditoria, qualquer alteração nessas três frentes passa a mexer no mesmo arquivo.
// viola: muda por três 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
}
}
// corrigido: cada responsabilidade no seu lugar
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 corrigida muda quando o repositório, o template de email ou a política de auditoria mudam. Robert C. Martin chama esse número de motivos de "axis of change", e o Clean Code (Prentice Hall, 2008) repete a mesma ideia para função: função curta com um nome que descreve uma ação.
O: aberto para extensão, fechado para modificação
Adicionar meio de pagamento não deve alterar a classe que processa pagamento. O código abaixo exige edição do método toda vez que um tipo novo entra:
// viola: cada tipo novo obriga edição aqui
class PaymentProcessor {
process(type: string, amount: number) {
if (type === 'credit_card') { /* ... */ }
else if (type === 'pix') { /* ... */ }
// 'boleto' obriga mexer neste método
}
}
// corrigido: tipo novo vira classe nova
interface PaymentMethod {
process(amount: number): Promise<PaymentResult>
}
class CreditCardPayment implements PaymentMethod {
async process(amount: number) { /* ... */ }
}
class PixPayment implements PaymentMethod {
async process(amount: number) { /* ... */ }
}
BoletoPayment entra depois sem tocar em nenhuma das classes acima. A Strategy do Refactoring.Guru é a mesma estrutura com outro nome.
L: substituição de Liskov
Subtipo entra no lugar do tipo base e o programa continua correto. Quando o código pede instanceof para decidir comportamento, o contrato já quebrou.
// viola: Square muda a altura junto
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 }
}
function increaseWidth(rect: Rectangle) {
rect.setWidth(rect.width + 1) // no Square a altura muda junto
}
// corrigido: contrato que os dois tipos honram
interface Shape {
area(): number
scale(factor: number): Shape
}
Rectangle e Square compartilham forma geométrica e não compartilham contrato de setter. Hierarquia separada, ou composição, resolve sem enganar o compilador.
I: segregação de interfaces
Interface grande obriga implementador a escrever método que não usa. O DataStore abaixo força quem só lê a assinar connect(), write() e subscribe().
// viola: cinco métodos para quem lê três
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>
}
// corrigido: uma interface por capacidade
interface Reader {
read(id: string): Promise<any>
}
interface Writer {
write(id: string, data: any): Promise<void>
}
interface EventSource {
subscribe(event: string, cb: Function): void
}
TypeScript ajuda aqui: Pick<DataStore, 'read'> serve para ad hoc, mas interface nomeada comunica intenção a quem chega depois.
D: inversão de dependência
Serviço de alto nível declara de que precisa e recebe a implementação pronta. Quem instancia fica na borda da aplicação.
// viola: instância concreta dentro do serviço
class OrderService {
constructor() {
this.db = new PrismaClient()
}
}
// corrigido: contrato no domínio, implementação fora
interface OrderRepository {
save(order: Order): Promise<void>
findById(id: string): Promise<Order | null>
}
class OrderService {
constructor(private readonly repo: OrderRepository) {}
}
Em produção o container injeta PrismaOrderRepository; no teste entra InMemoryOrderRepository e o teste roda sem Postgres. Framework de injeção como o reflect-metadata do NestJS automatiza essa costura, mas a decisão de desacoplar continua sendo do código de domínio.
Onde aplicar e onde deixar quieto
SOLID entra onde o custo de mudança é alto: regra de negócio, camada de serviço, ponto de extensão que time vai tocar toda semana. Função utilitária de quatro linhas, script de migração e código de configuração não recebem interface nem herança.
Martin Fowler trata desse limite no ensaio Is Design Dead?: desenho evolutivo parte de estrutura simples e refatora quando a dor aparece. A pergunta prática antes de puxar um padrão: se a mudança X chegar amanhã, quantos arquivos quebram? Poucos, segue em frente. Muitos, extraia a abstração.
Referências
- Robert C. Martin, Agile Software Development: Principles, Patterns, and Practices (Prentice Hall, 2002)
- Robert C. Martin, Clean Code (Prentice Hall, 2008)
- Is Design Dead? (Martin Fowler, 2001)
- Strategy Pattern (Refactoring.Guru)
- SOLID (Wikipedia)
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