Voltar para Artigos
Arquitetura11 min de leitura

SOLID na Prática: Os Cinco Princípios com Exemplos Reais em TypeScript

SRP, OCP, LSP, ISP e DIP aplicados em código TypeScript real. God Classes, extension points com Strategy Pattern, contratos de interface e Inversão de Dependência com tsyringe — SOLID como ferramenta de design, não decoração de currículo.

12 de agosto de 2026

SOLID é o acrônimo mais citado em entrevistas e o mais mal aplicado na prática. Os cinco princípios não são regras para decorar — são guias de design que tornam código mais fácil de estender, testar e modificar. Este artigo mostra cada princípio com o anti-pattern (o que evitar) e a solução refatorada em TypeScript real.

S — Single Responsibility Principle (SRP)

"Uma classe deve ter apenas um motivo para mudar." O motivo para mudar é quem pede a mudança: regra de negócio, infraestrutura de banco, provedor de e-mail. Se três pessoas diferentes da empresa podem pedir mudanças no mesmo arquivo, ele tem três responsabilidades.

SRP: Antes e Depois
// ❌ RUIM: CreateUserService faz tudo
export class CreateUserService {
  async execute(data: unknown) {
    // Valida os dados (responsabilidade de validação)
    if (!data.email) throw new Error('Email obrigatório');

    // Acessa o banco diretamente (responsabilidade de persistência)
    const exists = await db.query('SELECT * FROM users WHERE email = $1', [data.email]);
    if (exists.rows.length) throw new Error('Email já cadastrado');

    // Faz o hash (responsabilidade de criptografia)
    const hash = await bcrypt.hash(data.password, 10);
    await db.query('INSERT INTO users...');

    // Envia e-mail (responsabilidade de notificação)
    await nodemailer.sendMail({ to: data.email, subject: 'Bem-vindo!' });
  }
}

// ✅ BOM: Cada classe tem uma responsabilidade
// UsersController → extrai dados do HTTP e devolve resposta
// CreateUserUseCase → orquestra a regra de negócio
// IUsersRepository → persiste e consulta usuários
// IMailProvider → envia e-mails
// CreateUserInputSchema (Zod) → valida e normaliza entrada

// Quando o provedor de e-mail mudar (Nodemailer → SES),
// apenas o MailProvider muda. O Use Case não é tocado.

O — Open/Closed Principle (OCP)

"Aberto para extensão, fechado para modificação." Quando precisar de novo comportamento, adicione código — não modifique código existente que já funciona e tem testes.

OCP com Strategy Pattern
// ❌ RUIM: if/else gigante que precisa ser modificado para cada novo provedor
class NotificationService {
  async send(type: string, message: string, userId: string) {
    if (type === 'email') { /* ... */ }
    else if (type === 'sms') { /* ... */ }
    else if (type === 'push') { /* ... */ }
    // Para adicionar WhatsApp: precisa modificar esta classe
  }
}

// ✅ BOM: Strategy Pattern — extensão sem modificação
interface INotificationStrategy {
  send(message: string, userId: string): Promise<void>;
}

class EmailNotificationStrategy implements INotificationStrategy {
  async send(message: string, userId: string): Promise<void> { /* ... */ }
}

class SMSNotificationStrategy implements INotificationStrategy {
  async send(message: string, userId: string): Promise<void> { /* ... */ }
}

// Para WhatsApp: cria WhatsAppNotificationStrategy — sem tocar nas existentes
class WhatsAppNotificationStrategy implements INotificationStrategy {
  async send(message: string, userId: string): Promise<void> { /* ... */ }
}

class NotificationService {
  constructor(private strategy: INotificationStrategy) {}

  async send(message: string, userId: string): Promise<void> {
    return this.strategy.send(message, userId);
  }
}

L — Liskov Substitution Principle (LSP)

"Objetos de uma subclasse devem poder substituir objetos da superclasse sem quebrar o comportamento do programa." O LSP é violado quando uma implementação concreta não honra o contrato da interface:

LSP com repositórios
// O LSP garante que InMemoryUsersRepository possa substituir TypeORMUsersRepository
// em testes sem que o Use Case perceba a diferença
interface IUsersRepository {
  findById(id: string): Promise<User | null>;
}

// ❌ VIOLA LSP: InMemory lança erro em vez de retornar null
class BrokenInMemoryRepository implements IUsersRepository {
  async findById(id: string): Promise<User | null> {
    // Contrato: retorna null quando não encontrado
    // Violação: lança exceção — comportamento diferente do TypeORM
    throw new Error('Not found'); // ← quebra o contrato!
  }
}

// ✅ RESPEITA LSP: mesma semântica da implementação real
class InMemoryUsersRepository implements IUsersRepository {
  private items: Array<User> = [];
  async findById(id: string): Promise<User | null> {
    return this.items.find((u) => u.id === id) ?? null; // ← honra o contrato
  }
}

I — Interface Segregation Principle (ISP)

"Clientes não devem ser forçados a depender de interfaces que não usam." Interfaces gordas criam acoplamento desnecessário:

ISP: Interfaces focadas
// ❌ RUIM: interface grande que força implementar métodos não usados
interface IEmailService {
  sendWelcomeEmail(user: User): Promise<void>;
  sendPasswordResetEmail(user: User, token: string): Promise<void>;
  sendOrderConfirmation(order: Order): Promise<void>;
  sendMarketingEmail(users: Array<User>, campaign: string): Promise<void>;
}

// Para testar CreateUserUseCase, o FakeMailProvider precisa implementar
// sendOrderConfirmation e sendMarketingEmail que ele nunca usa

// ✅ BOM: interfaces focadas por contexto de uso
interface IWelcomeMailSender {
  sendWelcomeEmail(user: User): Promise<void>;
}

interface IPasswordResetMailSender {
  sendPasswordResetEmail(user: User, token: string): Promise<void>;
}

// Use Case depende apenas da interface que usa
class CreateUserUseCase {
  constructor(private mailSender: IWelcomeMailSender) {}
  // Não sabe que sendPasswordResetEmail existe
}

D — Dependency Inversion Principle (DIP)

"Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações." O Use Case é alto nível (regra de negócio). TypeORM é baixo nível (infraestrutura). O DIP exige que o Use Case dependa da interface, não da implementação:

DIP com tsyringe
// ❌ RUIM: Use Case importa TypeORM diretamente
import { dataSource } from '../typeorm';

export class CreateUserUseCase {
  async execute(dto: CreateUserDTO) {
    const repo = dataSource.getRepository(UserModel); // ← depende de infra
    const exists = await repo.findOneBy({ email: dto.email });
    // ...
  }
}

// ✅ BOM: Use Case depende da abstração
import { injectable, inject } from 'tsyringe';

@injectable()
export class CreateUserUseCase {
  constructor(
    @inject('UsersRepository')  // Abstração — não sabe qual classe concreta é
    private usersRepo: IUsersRepository,

    @inject('MailProvider')
    private mailProvider: IWelcomeMailSender
  ) {}

  async execute(dto: CreateUserDTO): Promise<User> {
    const exists = await this.usersRepo.existsByEmail(dto.email);
    if (exists) throw new AppError('E-mail já cadastrado.', 409);

    const passwordHash = await hash(dto.password, 12);
    const user = await this.usersRepo.create({ ...dto, passwordHash });

    await this.mailProvider.sendWelcomeEmail(user);
    return user;
  }
}

// O container decide qual implementação injetar:
// - Em produção: TypeORMUsersRepository + NodemailerMailProvider
// - Em testes:   InMemoryUsersRepository + FakeMailProvider

SOLID não significa criar uma interface para tudo. O guia prático: crie abstrações (interfaces) quando o componente pode ter múltiplas implementações (banco de dados, provedor de e-mail, storage), quando precisa ser substituído em testes, ou quando pode mudar independentemente do restante. Para casos simples sem essas necessidades, o acoplamento direto é aceitável — SOLID é uma ferramenta, não um dogma.

Conclusão

Os cinco princípios do SOLID trabalham juntos: SRP cria classes focadas, OCP permite extensão sem modificação, LSP garante contratos confiáveis, ISP mantém interfaces enxutas, e DIP inverte as dependências colocando a lógica de negócio no centro. O resultado é um código onde adicionar features é criar novos arquivos, não modificar os existentes — e onde testes unitários são naturais, não sacrifícios.