Princípios SOLID

Introdução: Por Que SOLID Importa

Os princípios SOLID foram formalizados por Robert C. Martin no início dos anos 2000, mas suas raízes remontam a décadas de pesquisa em engenharia de software — desde os trabalhos de David Parnas sobre information hiding (1972) até a formalização de Barbara Liskov sobre subtipagem comportamental (1987).

SOLID não é um checklist dogmático. É um conjunto de heurísticas que guiam decisões de design quando o custo de mudança futura é alto. O ponto crítico é saber quando aplicar e quando relaxar cada princípio.

A diferença entre uma abordagem dogmática e pragmática:

Dogmático:  "Preciso criar 47 classes para seguir SRP"
Pragmático: "Este módulo tem duas razões para mudar? Se sim, separo.
             Se não, manter junto reduz complexidade acidental."

S — Single Responsibility Principle (SRP)

A Definição Real (Não a Simplificada)

A definição popular — “uma classe deve ter apenas uma responsabilidade” — é vaga e leva a over-engineering. A definição precisa de Robert C. Martin:

“Um módulo deve ter um, e apenas um, motivo para mudar.”

Mais especificamente: um módulo deve ser responsável perante um, e apenas um, ator (stakeholder).

Métricas de Coesão e Acoplamento

Para avaliar SRP objetivamente, use métricas:

// LCOM (Lack of Cohesion of Methods)
// LCOM = 0 → alta coesão (bom)
// LCOM alto → baixa coesão (possível violação de SRP)

// Exemplo: classe com LCOM alto (baixa coesão)
class UserManager {
  private db: Database;
  private emailClient: SmtpClient;
  private pdfGenerator: PdfEngine;

  // Grupo 1: operações de persistência (usa db)
  createUser(data: UserData): User {
    return this.db.insert('users', data);
  }

  findUser(id: string): User {
    return this.db.findById('users', id);
  }

  // Grupo 2: operações de email (usa emailClient)
  sendWelcomeEmail(user: User): void {
    this.emailClient.send({
      to: user.email,
      template: 'welcome',
    });
  }

  sendPasswordReset(user: User, token: string): void {
    this.emailClient.send({
      to: user.email,
      template: 'reset',
      data: { token },
    });
  }

  // Grupo 3: operações de relatório (usa pdfGenerator)
  generateUserReport(users: User[]): Buffer {
    return this.pdfGenerator.render('user-report', { users });
  }
}

// LCOM = 3 (três grupos de métodos que não compartilham campos)
// Três atores diferentes: DBA, time de marketing, time de BI

A Refatoração Correta

// Cada classe responde a UM ator e tem LCOM ≈ 0

class UserRepository {
  constructor(private readonly db: Database) {}

  create(data: UserData): User {
    return this.db.insert('users', data);
  }

  findById(id: string): User | null {
    return this.db.findById('users', id);
  }

  findByEmail(email: string): User | null {
    return this.db.findOne('users', { email });
  }
}

class UserNotificationService {
  constructor(private readonly emailClient: SmtpClient) {}

  sendWelcome(user: User): void {
    this.emailClient.send({
      to: user.email,
      template: 'welcome',
    });
  }

  sendPasswordReset(user: User, token: string): void {
    this.emailClient.send({
      to: user.email,
      template: 'reset',
      data: { token },
    });
  }
}

class UserReportGenerator {
  constructor(private readonly pdfEngine: PdfEngine) {}

  generate(users: User[]): Buffer {
    return this.pdfEngine.render('user-report', { users });
  }
}

Métricas Práticas

Acoplamento Aferente (Ca): quantos módulos dependem DESTE módulo
  → Ca alto = módulo é muito usado → mudanças aqui quebram muito

Acoplamento Eferente (Ce): de quantos módulos ESTE módulo depende
  → Ce alto = módulo depende de muita coisa → frágil a mudanças externas

Instabilidade (I) = Ce / (Ca + Ce)
  → I = 0: totalmente estável (muitos dependem dele, ele depende de poucos)
  → I = 1: totalmente instável (ninguém depende dele, ele depende de muitos)

Regra: Módulos estáveis devem ser abstratos. Módulos instáveis podem ser concretos.

Anti-Pattern: The God Class

// Violação clássica em aplicações Express/NestJS
class AppController {
  async handleRequest(req: Request, res: Response) {
    // Valida input
    if (!req.body.email) return res.status(400).json({ error: 'Email obrigatório' });

    // Lógica de negócio
    const user = await db.query('SELECT * FROM users WHERE email = $1', [req.body.email]);
    const discount = user.tier === 'premium' ? 0.2 : 0;
    const total = cart.items.reduce((sum, i) => sum + i.price, 0) * (1 - discount);

    // Persistência
    await db.query('INSERT INTO orders (user_id, total) VALUES ($1, $2)', [user.id, total]);

    // Notificação
    await sendgrid.send({ to: user.email, subject: 'Pedido confirmado' });

    // Resposta HTTP
    res.json({ orderId: order.id, total });
  }
}
// Este método tem 5 razões para mudar: validação, regras de negócio,
// persistência, notificação e serialização HTTP.

O — Open/Closed Principle (OCP)

A Definição Formal

“Entidades de software devem ser abertas para extensão, mas fechadas para modificação.”

Na prática: você deve poder adicionar novos comportamentos sem alterar código existente já testado.

Strategy Pattern — O Padrão Clássico para OCP

// Interface define o contrato
interface PaymentStrategy {
  readonly name: string;
  validate(amount: number, metadata: PaymentMetadata): ValidationResult;
  process(amount: number, metadata: PaymentMetadata): Promise<PaymentResult>;
  refund(transactionId: string, amount: number): Promise<RefundResult>;
}

// Cada estratégia é uma implementação independente
class PixPayment implements PaymentStrategy {
  readonly name = 'pix';

  validate(amount: number, metadata: PaymentMetadata): ValidationResult {
    if (!metadata.pixKey) return { valid: false, error: 'Chave PIX obrigatória' };
    if (amount > 100_000) return { valid: false, error: 'Limite PIX excedido' };
    return { valid: true };
  }

  async process(amount: number, metadata: PaymentMetadata): Promise<PaymentResult> {
    const qrCode = await this.generateQRCode(metadata.pixKey, amount);
    return { status: 'pending', qrCode, expiresAt: Date.now() + 30 * 60_000 };
  }

  async refund(transactionId: string, amount: number): Promise<RefundResult> {
    return this.pixApi.initiateRefund(transactionId, amount);
  }

  private async generateQRCode(key: string, amount: number): Promise<string> {
    // Gera QR code EMV/BRCode
    return this.pixApi.createQR({ key, amount });
  }
}

class CreditCardPayment implements PaymentStrategy {
  readonly name = 'credit_card';

  validate(amount: number, metadata: PaymentMetadata): ValidationResult {
    if (!metadata.cardToken) return { valid: false, error: 'Token do cartão obrigatório' };
    if (!this.luhnCheck(metadata.cardNumber)) return { valid: false, error: 'Cartão inválido' };
    return { valid: true };
  }

  async process(amount: number, metadata: PaymentMetadata): Promise<PaymentResult> {
    const auth = await this.gateway.authorize(metadata.cardToken, amount);
    if (!auth.approved) return { status: 'declined', reason: auth.reason };
    const capture = await this.gateway.capture(auth.id, amount);
    return { status: 'approved', transactionId: capture.id };
  }

  async refund(transactionId: string, amount: number): Promise<RefundResult> {
    return this.gateway.refund(transactionId, amount);
  }

  private luhnCheck(cardNumber: string): boolean {
    // Implementação do algoritmo de Luhn
    let sum = 0;
    let alternate = false;
    for (let i = cardNumber.length - 1; i >= 0; i--) {
      let n = parseInt(cardNumber[i], 10);
      if (alternate) { n *= 2; if (n > 9) n -= 9; }
      sum += n;
      alternate = !alternate;
    }
    return sum % 10 === 0;
  }
}

// Registry — o PaymentService NUNCA é modificado para adicionar novo meio
class PaymentService {
  private strategies = new Map<string, PaymentStrategy>();

  register(strategy: PaymentStrategy): void {
    this.strategies.set(strategy.name, strategy);
  }

  async processPayment(
    method: string,
    amount: number,
    metadata: PaymentMetadata
  ): Promise<PaymentResult> {
    const strategy = this.strategies.get(method);
    if (!strategy) throw new UnsupportedPaymentError(method);

    const validation = strategy.validate(amount, metadata);
    if (!validation.valid) throw new PaymentValidationError(validation.error);

    return strategy.process(amount, metadata);
  }
}

// Uso — adicionar BoletoPayment NÃO modifica PaymentService
const service = new PaymentService();
service.register(new PixPayment());
service.register(new CreditCardPayment());
// service.register(new BoletoPayment());  // Novo! Zero mudanças no existente.

Plugin Architecture — OCP em Escala

// Sistema de plugins para processamento de eventos
interface EventPlugin {
  readonly name: string;
  readonly version: string;
  readonly events: string[];
  handle(event: DomainEvent): Promise<void>;
  healthCheck(): Promise<HealthStatus>;
}

class EventBus {
  private plugins: Map<string, EventPlugin[]> = new Map();

  registerPlugin(plugin: EventPlugin): void {
    for (const eventType of plugin.events) {
      const existing = this.plugins.get(eventType) ?? [];
      existing.push(plugin);
      this.plugins.set(eventType, existing);
    }
  }

  async emit(event: DomainEvent): Promise<void> {
    const handlers = this.plugins.get(event.type) ?? [];
    await Promise.allSettled(
      handlers.map(plugin => plugin.handle(event))
    );
  }
}

// Cada plugin é um módulo independente — OCP puro
// O EventBus NUNCA precisa ser modificado para suportar novos eventos

Anti-Pattern: Switch/If Chain que Cresce

// Cada novo tipo = mudança neste código + risco de regressão
function calculateShipping(type: string, weight: number): number {
  if (type === 'sedex')       return weight * 5.0 + 15;
  else if (type === 'pac')    return weight * 2.5 + 8;
  else if (type === 'jadlog') return weight * 3.2 + 10;
  else if (type === 'loggi')  return weight * 2.8 + 12;
  // Cada PR adiciona mais um else if...
  // Ninguém testa os anteriores quando adiciona novo.
  throw new Error('Tipo desconhecido');
}

L — Liskov Substitution Principle (LSP)

O Formalismo de Liskov

Barbara Liskov e Jeannette Wing definiram (1994):

Se S é um subtipo de T, então objetos do tipo T podem ser substituídos por objetos do tipo S sem alterar as propriedades desejáveis do programa.

Isso implica três regras formais:

1. Contravariância de pré-condições:
   → Subtipos NÃO podem ter pré-condições MAIS fortes que o tipo base
   → Se o pai aceita qualquer número, o filho não pode exigir "apenas positivos"

2. Covariância de pós-condições:
   → Subtipos NÃO podem ter pós-condições MAIS fracas que o tipo base
   → Se o pai garante retorno não-nulo, o filho também deve garantir

3. Invariante de classe:
   → Subtipos devem preservar todas as invariantes do tipo base
   → Se o pai mantém saldo ≥ 0, o filho não pode violar isso

O Exemplo Clássico: Rectangle/Square

class Rectangle {
  constructor(protected _width: number, protected _height: number) {}

  get width(): number { return this._width; }
  set width(value: number) { this._width = value; }

  get height(): number { return this._height; }
  set height(value: number) { this._height = value; }

  area(): number { return this._width * this._height; }
}

// Violação de LSP: Square muda o comportamento esperado
class Square extends Rectangle {
  set width(value: number) {
    this._width = value;
    this._height = value; // Efeito colateral inesperado!
  }

  set height(value: number) {
    this._width = value;  // Efeito colateral inesperado!
    this._height = value;
  }
}

// Código que funciona com Rectangle QUEBRA com Square
function doubleWidth(rect: Rectangle): void {
  const originalHeight = rect.height;
  rect.width = rect.width * 2;
  // Invariante esperada: height não mudou
  console.assert(rect.height === originalHeight); // FALHA para Square!
}

Exemplo Real: Violação de LSP em Repositórios

interface UserRepository {
  findById(id: string): Promise<User | null>;
  save(user: User): Promise<User>;
  delete(id: string): Promise<void>;
}

// Implementação padrão — funciona conforme esperado
class PostgresUserRepository implements UserRepository {
  async findById(id: string): Promise<User | null> {
    const row = await this.pool.query('SELECT * FROM users WHERE id = $1', [id]);
    return row.rows[0] ? this.mapToUser(row.rows[0]) : null;
  }

  async save(user: User): Promise<User> {
    await this.pool.query(
      'INSERT INTO users (id, name, email) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET name = $2, email = $3',
      [user.id, user.name, user.email]
    );
    return user;
  }

  async delete(id: string): Promise<void> {
    await this.pool.query('DELETE FROM users WHERE id = $1', [id]);
  }
}

// VIOLAÇÃO DE LSP: ReadOnlyRepository lança exceção em operações válidas
class ReadOnlyUserRepository implements UserRepository {
  async findById(id: string): Promise<User | null> {
    return this.cache.get(id);
  }

  async save(user: User): Promise<User> {
    throw new Error('Operação não suportada em repositório read-only');
    // Fortaleceu a pré-condição! O contrato diz que save() funciona.
  }

  async delete(id: string): Promise<void> {
    throw new Error('Operação não suportada em repositório read-only');
    // Mesmo problema — viola a expectativa do consumidor.
  }
}

A Correção: Interface Segregation + LSP

// Separe as interfaces para respeitar LSP
interface ReadableRepository<T> {
  findById(id: string): Promise<T | null>;
  findAll(filter?: Partial<T>): Promise<T[]>;
}

interface WritableRepository<T> {
  save(entity: T): Promise<T>;
  delete(id: string): Promise<void>;
}

interface UserRepository extends ReadableRepository<User>, WritableRepository<User> {}

// Agora ReadOnlyUserRepository implementa APENAS ReadableRepository
class ReadOnlyUserRepository implements ReadableRepository<User> {
  async findById(id: string): Promise<User | null> {
    return this.cache.get(id);
  }

  async findAll(filter?: Partial<User>): Promise<User[]> {
    return this.cache.getAll(filter);
  }
  // Não precisa implementar save/delete — não faz parte do contrato!
}

I — Interface Segregation Principle (ISP)

O Problema das Fat Interfaces

“Nenhum cliente deve ser forçado a depender de métodos que não usa.”

// Fat interface — força implementação de métodos desnecessários
interface Animal {
  eat(food: Food): void;
  sleep(hours: number): void;
  fly(altitude: number): void;     // Cachorro não voa!
  swim(depth: number): void;       // Águia não nada!
  dig(depth: number): void;        // Peixe não cava!
  climb(height: number): void;     // Baleia não escala!
}

// Implementações ficam cheias de métodos vazios ou throws
class Dog implements Animal {
  eat(food: Food): void { /* ok */ }
  sleep(hours: number): void { /* ok */ }
  fly(altitude: number): void { throw new Error('Cães não voam'); }
  swim(depth: number): void { /* ok, alguns cães nadam */ }
  dig(depth: number): void { /* ok */ }
  climb(height: number): void { throw new Error('Cães não escalam'); }
}

Role Interfaces — A Solução

// Interfaces pequenas e focadas em capacidades (roles)
interface Feedable {
  eat(food: Food): void;
}

interface Sleepable {
  sleep(hours: number): void;
}

interface Flyable {
  fly(altitude: number): void;
  land(): void;
}

interface Swimmable {
  swim(depth: number): void;
  surface(): void;
}

// Cada classe compõe APENAS as interfaces relevantes
class Dog implements Feedable, Sleepable, Swimmable {
  eat(food: Food): void { /* ... */ }
  sleep(hours: number): void { /* ... */ }
  swim(depth: number): void { /* ... */ }
  surface(): void { /* ... */ }
}

class Eagle implements Feedable, Sleepable, Flyable {
  eat(food: Food): void { /* ... */ }
  sleep(hours: number): void { /* ... */ }
  fly(altitude: number): void { /* ... */ }
  land(): void { /* ... */ }
}

// Funções consomem APENAS o que precisam
function feedAnimal(animal: Feedable): void {
  animal.eat(new GenericFood());
}

function launchIntoSky(flyer: Flyable): void {
  flyer.fly(1000);
}

Exemplo Real: Middleware com Interfaces Segregadas

// Em vez de uma interface monolítica de Logger:
interface Logger {
  debug(msg: string, meta?: Record<string, unknown>): void;
  info(msg: string, meta?: Record<string, unknown>): void;
  warn(msg: string, meta?: Record<string, unknown>): void;
  error(msg: string, error?: Error, meta?: Record<string, unknown>): void;
  setLevel(level: LogLevel): void;
  addTransport(transport: Transport): void;
  flush(): Promise<void>;
  child(bindings: Record<string, unknown>): Logger;
}

// Segregue por papel do consumidor:
interface LogWriter {
  debug(msg: string, meta?: Record<string, unknown>): void;
  info(msg: string, meta?: Record<string, unknown>): void;
  warn(msg: string, meta?: Record<string, unknown>): void;
  error(msg: string, error?: Error, meta?: Record<string, unknown>): void;
}

interface LogConfigurator {
  setLevel(level: LogLevel): void;
  addTransport(transport: Transport): void;
}

interface LogLifecycle {
  flush(): Promise<void>;
  child(bindings: Record<string, unknown>): LogWriter;
}

// O handler HTTP só precisa de LogWriter
// O bootstrap da app precisa de LogConfigurator
// O graceful shutdown precisa de LogLifecycle

D — Dependency Inversion Principle (DIP)

As Duas Regras

  1. Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações.
  2. Abstrações não devem depender de detalhes. Detalhes devem depender de abstrações.

Inversão de Controle (IoC) na Prática

// SEM inversão — alto nível depende de baixo nível
class OrderService {
  private repo = new PostgresOrderRepository();    // Acoplado!
  private notifier = new SendGridEmailNotifier();   // Acoplado!
  private logger = new WinstonLogger();             // Acoplado!

  async placeOrder(dto: CreateOrderDto): Promise<Order> {
    const order = Order.create(dto);
    await this.repo.save(order);
    await this.notifier.notify(order);
    this.logger.info('Order placed', { orderId: order.id });
    return order;
  }
}

// COM inversão — alto nível depende de abstrações
interface OrderRepository {
  save(order: Order): Promise<void>;
  findById(id: string): Promise<Order | null>;
}

interface OrderNotifier {
  notify(order: Order): Promise<void>;
}

interface Logger {
  info(msg: string, meta?: Record<string, unknown>): void;
  error(msg: string, error?: Error): void;
}

class OrderService {
  constructor(
    private readonly repo: OrderRepository,
    private readonly notifier: OrderNotifier,
    private readonly logger: Logger
  ) {}

  async placeOrder(dto: CreateOrderDto): Promise<Order> {
    const order = Order.create(dto);
    await this.repo.save(order);
    await this.notifier.notify(order);
    this.logger.info('Order placed', { orderId: order.id });
    return order;
  }
}

Dependency Injection Container

// Container simples (na prática, use tsyringe, InversifyJS ou NestJS)
class Container {
  private bindings = new Map<string, () => unknown>();

  bind<T>(token: string, factory: () => T): void {
    this.bindings.set(token, factory);
  }

  resolve<T>(token: string): T {
    const factory = this.bindings.get(token);
    if (!factory) throw new Error(`Nenhum binding para: ${token}`);
    return factory() as T;
  }
}

// Configuração (composition root)
const container = new Container();

container.bind<OrderRepository>('OrderRepository', () =>
  new PostgresOrderRepository(container.resolve('DatabasePool'))
);

container.bind<OrderNotifier>('OrderNotifier', () =>
  new SendGridNotifier(container.resolve('SendGridConfig'))
);

container.bind<OrderService>('OrderService', () =>
  new OrderService(
    container.resolve('OrderRepository'),
    container.resolve('OrderNotifier'),
    container.resolve('Logger')
  )
);

// Em testes — troca implementações sem mudar o serviço
const testContainer = new Container();
testContainer.bind<OrderRepository>('OrderRepository', () =>
  new InMemoryOrderRepository()
);
testContainer.bind<OrderNotifier>('OrderNotifier', () =>
  new FakeNotifier() // Não envia email de verdade
);

Anti-Pattern: Service Locator (o falso DIP)

// Service Locator PARECE DIP, mas é um anti-pattern
class OrderService {
  async placeOrder(dto: CreateOrderDto): Promise<Order> {
    // Dependências são ocultas — impossível saber o que este serviço precisa
    const repo = ServiceLocator.get<OrderRepository>('OrderRepository');
    const notifier = ServiceLocator.get<OrderNotifier>('OrderNotifier');

    // Problemas:
    // 1. Dependências não aparecem no construtor — são invisíveis
    // 2. Testes precisam configurar o ServiceLocator global
    // 3. Erros de resolução só aparecem em runtime
    // 4. Impossível fazer análise estática de dependências
  }
}

SOLID vs Pragmatismo: Quando Relaxar

Princípio    | Quando relaxar
-------------|--------------------------------------------------
SRP          | Scripts únicos, lambdas pequenas, protótipos
             | descartáveis. Se o módulo tem < 100 linhas e
             | uma razão para existir, não force separação.

OCP          | Quando há apenas 2-3 variantes E é improvável
             | que novas apareçam. Um if/else simples é
             | mais legível que um Strategy com 2 implementações.

LSP          | Quase nunca relaxe LSP. Violações de LSP
             | causam bugs sutis e difíceis de rastrear.

ISP          | Em APIs internas com poucos consumidores
             | conhecidos. O overhead de muitas interfaces
             | pequenas pode ser pior que uma interface média.

DIP          | Em módulos folha (sem dependentes), scripts,
             | CLIs simples. Se ninguém depende de você,
             | DIP é overhead sem benefício.

O Teste Pragmático

Antes de aplicar qualquer princípio SOLID, pergunte:

1. Qual é o custo de mudança se eu NÃO aplicar?
   → Se o código tem vida curta ou poucos dependentes, o custo é baixo.

2. Quantas vezes este código já precisou mudar?
   → Se nunca mudou, não otimize para mudança hipotética (YAGNI).

3. Quantas pessoas trabalham neste módulo?
   → SRP e ISP são mais críticos quando múltiplos times tocam o mesmo código.

4. Existe teste que garanta que a refatoração não quebrou nada?
   → Sem testes, refatoração para SOLID é risco, não melhoria.

Referências e Leitura Aprofundada

- "Agile Software Development" — Robert C. Martin (a fonte original de SOLID)
- "A Behavioral Notion of Subtyping" — Liskov & Wing, 1994 (LSP formal)
- "Design Principles and Design Patterns" — Robert C. Martin, 2000
- "Clean Architecture" — Robert C. Martin (SOLID no contexto de arquitetura)
- Métricas LCOM: Chidamber & Kemerer, "A Metrics Suite for OOD", 1994
- "Dependency Injection Principles, Practices, and Patterns" — van Deursen et al.