SOLID é um conjunto de cinco princípios de design orientado a objetos que, quando aplicados juntos, resultam em código mais fácil de manter, testar e estender. Vamos ver cada um com exemplos concretos.
S — Single Responsibility
Uma classe deve ter apenas uma razão para mudar.
// ❌ Errado: mistura lógica de negócio e persistência
class UserService {
createUser(data: CreateUserDTO) {
const user = new User(data);
// Valida, salva e envia email — responsabilidades demais
this.validate(user);
this.db.save(user);
this.mailer.sendWelcome(user);
}
}
// ✅ Correto: cada classe tem um papel
class UserFactory { create(data: CreateUserDTO): User { ... } }
class UserRepository { save(user: User): void { ... } }
class WelcomeMailer { send(user: User): void { ... } }
O — Open/Closed
Aberto para extensão, fechado para modificação.
// ✅ Correto: novos descontos sem modificar a classe existente
interface DiscountStrategy {
apply(price: number): number;
}
class PercentDiscount implements DiscountStrategy {
constructor(private pct: number) {}
apply(price: number) { return price * (1 - this.pct / 100); }
}
class FlatDiscount implements DiscountStrategy {
constructor(private amount: number) {}
apply(price: number) { return price - this.amount; }
}
L — Liskov Substitution
Subclasses devem ser substituíveis por suas classes base.
O exemplo clássico é o quadrado/retângulo: um Square que herda de Rectangle e sobrescreve setWidth quebrando o invariante de área — isso viola o LSP.
I — Interface Segregation
Clientes não devem depender de interfaces que não utilizam.
// ❌ Interface gorda
interface Animal {
walk(): void;
fly(): void;
swim(): void;
}
// ✅ Interfaces específicas
interface Walker { walk(): void; }
interface Flyer { fly(): void; }
interface Swimmer { swim(): void; }
class Duck implements Walker, Flyer, Swimmer { ... }
class Dog implements Walker, Swimmer { ... }
D — Dependency Inversion
Dependa de abstrações, não de implementações concretas.
// ✅ O serviço depende da abstração, não do Prisma diretamente
interface UserRepository {
findById(id: string): Promise<User | null>;
}
class UserService {
constructor(private repo: UserRepository) {}
async getUser(id: string) {
return this.repo.findById(id);
}
}
// Pode injetar PrismaUserRepository, InMemoryUserRepository, etc.
Aplicar SOLID de forma dogmática pode resultar em over-engineering. Use esses princípios como guia, não como regra absoluta. A pergunta certa é sempre: essa abstração resolve um problema real que tenho hoje ou pode surgir amanhã?