🚀 Oferta especial: 60% OFF no CrazyStack - Últimas vagas!Garantir vaga →
SOLID Principles

Aberto para extensão, fechado para modificação Open-Closed Principle

Domine o segundo princípio do SOLID com exemplos práticos em TypeScript

Tutorial Prático

O código que cresce sem quebrar Open-Closed Principle

Aprenda a criar sistemas que podem ser estendidos sem modificar código existente. Essencial para qualquer dev sênior.

Princípio SOLID
90%
Bugs evitados
5x
Mais testável
open-closed.ts
// ❌ Violando o princípio function calculateBonus(employee: Employee) { if (employee.role === 'developer') return salary * 0.1; if (employee.role === 'designer') return salary * 0.05; // Precisa modificar para adicionar novo cargo } // ✅ Respeitando o princípio abstract class Employee { abstract getBonus(): number; } class Developer extends Employee { getBonus() { return this.salary * 0.1; } }

Por que isso é importante

Código mais estável

Adicionar funcionalidades sem quebrar o que já funciona. Zero regressões.

Desenvolvimento mais rápido

Novas features sem mexer em código legado. Deploy com confiança.

Testes isolados

Cada classe testada independentemente. Cobertura real de testes.

Trabalho em equipe

Múltiplos devs trabalhando sem conflitos. Menos merge hell.

O Problema: Código que Quebra

Vamos ver como NÃO fazer. Este código viola o Open-Closed Principle.

❌ Implementação Problemática

// ❌ VIOLANDO o Open-Closed Principle
interface Employee {
  name: string;
  salary: number;
  role: 'developer' | 'designer' | 'manager';
}

// Esta função precisa ser MODIFICADA toda vez que
// adicionamos um novo tipo de funcionário
function calculateBonus(employee: Employee): number {
  switch (employee.role) {
    case 'developer':
      return employee.salary * 0.1; // 10% de bônus
    case 'designer':
      return employee.salary * 0.05; // 5% de bônus
    case 'manager':
      return employee.salary * 0.2; // 20% de bônus
    default:
      return 0;
  }
}

// Uso da função
const employees: Employee[] = [
  { name: 'João', salary: 8000, role: 'developer' },
  { name: 'Maria', salary: 6000, role: 'designer' },
  { name: 'Carlos', salary: 12000, role: 'manager' }
];

employees.forEach(emp => {
  console.log(`${emp.name}: R$ ${calculateBonus(emp)}`);
});

Problemas desta abordagem:

  • • Precisa modificar a função para cada novo cargo
  • • Risco de quebrar funcionalidades existentes
  • • Difícil de testar isoladamente
  • • Viola o princípio da responsabilidade única
  • • Código cresce de forma descontrolada

Cenário real:

  • • Empresa contrata estagiários
  • • Precisa adicionar 'intern' no tipo
  • • Modifica a função calculateBonus
  • • Risco de quebrar testes existentes
  • • Deploy com medo

⚠️ Resultado: Cada nova feature é um risco. Código frágil e difícil de manter.

A Solução: Open-Closed Principle

Agora vamos ver como fazer certo. Aberto para extensão, fechado para modificação.

✅ Implementação Correta

1. Classe Base Abstrata

// ✅ Classe base que define o contrato
abstract class Employee {
  constructor(
    public name: string,
    public salary: number
  ) {}

  // Método abstrato que DEVE ser implementado
  abstract getBonus(): number;
  
  // Métodos comuns a todos os funcionários
  getInfo(): string {
    return `${this.name} - Salário: R$ ${this.salary}`;
  }
  
  getTotalCompensation(): number {
    return this.salary + this.getBonus();
  }
}

2. Implementações Específicas

// ✅ Cada tipo de funcionário é uma classe
class Developer extends Employee {
  constructor(name: string, salary: number, public level: 'junior' | 'pleno' | 'senior') {
    super(name, salary);
  }
  
  getBonus(): number {
    const baseBonus = this.salary * 0.1;
    const levelMultiplier = {
      'junior': 1,
      'pleno': 1.2,
      'senior': 1.5
    };
    return baseBonus * levelMultiplier[this.level];
  }
}

class Designer extends Employee {
  constructor(name: string, salary: number, public specialty: 'ui' | 'ux' | 'graphic') {
    super(name, salary);
  }
  
  getBonus(): number {
    const baseBonus = this.salary * 0.05;
    // Designers UX ganham bônus extra
    return this.specialty === 'ux' ? baseBonus * 1.3 : baseBonus;
  }
}

class Manager extends Employee {
  constructor(name: string, salary: number, public teamSize: number) {
    super(name, salary);
  }
  
  getBonus(): number {
    const baseBonus = this.salary * 0.2;
    const teamBonus = this.teamSize * 500; // R$ 500 por pessoa na equipe
    return baseBonus + teamBonus;
  }
}

3. Uso Polimórfico

// ✅ Função que trabalha com qualquer Employee
function calculatePayroll(employees: Employee[]): void {
  let totalBonus = 0;
  
  employees.forEach(employee => {
    const bonus = employee.getBonus(); // Polimorfismo em ação!
    totalBonus += bonus;
    
    console.log(`${employee.getInfo()}`);
    console.log(`Bônus: R$ ${bonus}`);
    console.log(`Total: R$ ${employee.getTotalCompensation()}`);
    console.log('---');
  });
  
  console.log(`Total de bônus da empresa: R$ ${totalBonus}`);
}

// Criando funcionários
const employees: Employee[] = [
  new Developer('João', 8000, 'senior'),
  new Designer('Maria', 6000, 'ux'),
  new Manager('Carlos', 12000, 5)
];

// Calculando folha de pagamento
calculatePayroll(employees);

✅ Resultado: Código extensível sem modificação. Cada classe tem sua responsabilidade.

Extensão: Adicionando Novo Tipo

Veja como adicionar um estagiário SEM modificar código existente.

🚀 Extensão Sem Modificação

Nova classe: Estagiário

// ✅ NOVA classe - ZERO modificação no código existente
class Intern extends Employee {
  constructor(
    name: string, 
    salary: number, 
    public university: string,
    public semester: number
  ) {
    super(name, salary);
  }
  
  getBonus(): number {
    // Estagiários ganham bônus baseado no semestre
    const semesterBonus = this.semester >= 6 ? 300 : 200;
    return semesterBonus;
  }
  
  // Método específico para estagiários
  getAcademicInfo(): string {
    return `${this.university} - ${this.semester}º semestre`;
  }
}

// ✅ Usando a nova classe
const newEmployees: Employee[] = [
  new Developer('João', 8000, 'senior'),
  new Designer('Maria', 6000, 'ux'),
  new Manager('Carlos', 12000, 5),
  new Intern('Ana', 2000, 'UFMG', 7), // NOVO tipo!
  new Intern('Pedro', 1800, 'USP', 4)  // NOVO tipo!
];

// ✅ A função calculatePayroll funciona SEM modificação!
calculatePayroll(newEmployees);

Mais extensões possíveis:

// ✅ Freelancer
class Freelancer extends Employee {
  constructor(name: string, salary: number, public projectsCompleted: number) {
    super(name, salary);
  }
  
  getBonus(): number {
    return this.projectsCompleted * 1000; // R$ 1000 por projeto
  }
}

// ✅ Consultor
class Consultant extends Employee {
  constructor(name: string, salary: number, public expertise: string[]) {
    super(name, salary);
  }
  
  getBonus(): number {
    const expertiseBonus = this.expertise.length * 2000;
    return this.salary * 0.15 + expertiseBonus;
  }
}

// ✅ CEO
class CEO extends Employee {
  constructor(name: string, salary: number, public companyValue: number) {
    super(name, salary);
  }
  
  getBonus(): number {
    return this.companyValue * 0.001; // 0.1% do valor da empresa
  }
}

🎯 Magia: Adicionamos 5 novos tipos de funcionário sem tocar em UMA linha do código original!

Vantagens Práticas

Por que todo dev sênior usa este princípio.

🧪 Testes Isolados

// ✅ Teste específico para Developer
describe('Developer', () => {
  it('should calculate bonus correctly', () => {
    const dev = new Developer('João', 8000, 'senior');
    expect(dev.getBonus()).toBe(1200); // 8000 * 0.1 * 1.5
  });
});

// ✅ Teste específico para Intern
describe('Intern', () => {
  it('should give semester bonus', () => {
    const intern = new Intern('Ana', 2000, 'UFMG', 7);
    expect(intern.getBonus()).toBe(300);
  });
});

Cada classe testada independentemente. Cobertura real.

🚀 Deploy Seguro

  • • Nova feature = nova classe
  • • Zero risco de regressão
  • • Rollback simples
  • • CI/CD confiável
  • • Deploy com confiança

👥 Trabalho em Equipe

  • • Cada dev trabalha em sua classe
  • • Menos conflitos de merge
  • • Code review focado
  • • Responsabilidades claras
  • • Onboarding mais fácil

⚡ Performance

  • • Sem switch/case gigantes
  • • Polimorfismo é mais rápido
  • • Menos condicionais
  • • Código mais limpo
  • • Menos bugs

Exemplo Avançado: Sistema de Notificações

Aplicação real do Open-Closed Principle em um sistema complexo.

📱 Sistema Extensível de Notificações

Interface base:

// ✅ Interface que define o contrato
interface NotificationChannel {
  send(message: string, recipient: string): Promise<boolean>;
  validateRecipient(recipient: string): boolean;
  getChannelName(): string;
}

// ✅ Classe base com funcionalidades comuns
abstract class BaseNotification implements NotificationChannel {
  constructor(protected config: any) {}
  
  abstract send(message: string, recipient: string): Promise<boolean>;
  abstract validateRecipient(recipient: string): boolean;
  abstract getChannelName(): string;
  
  // Método comum para logging
  protected log(action: string, recipient: string, success: boolean): void {
    console.log(`[${this.getChannelName()}] ${action} to ${recipient}: ${success ? 'SUCCESS' : 'FAILED'}`);
  }
}

Implementações específicas:

// ✅ Email
class EmailNotification extends BaseNotification {
  async send(message: string, recipient: string): Promise<boolean> {
    if (!this.validateRecipient(recipient)) return false;
    
    try {
      // Simulação de envio de email
      await this.sendEmail(message, recipient);
      this.log('Email sent', recipient, true);
      return true;
    } catch (error) {
      this.log('Email failed', recipient, false);
      return false;
    }
  }
  
  validateRecipient(email: string): boolean {
    return /^[^s@]+@[^s@]+.[^s@]+$/.test(email);
  }
  
  getChannelName(): string { return 'EMAIL'; }
  
  private async sendEmail(message: string, email: string): Promise<void> {
    // Integração com serviço de email
    console.log(`Sending email to ${email}: ${message}`);
  }
}

// ✅ SMS
class SMSNotification extends BaseNotification {
  async send(message: string, recipient: string): Promise<boolean> {
    if (!this.validateRecipient(recipient)) return false;
    
    try {
      await this.sendSMS(message, recipient);
      this.log('SMS sent', recipient, true);
      return true;
    } catch (error) {
      this.log('SMS failed', recipient, false);
      return false;
    }
  }
  
  validateRecipient(phone: string): boolean {
    return /^+?[1-9]d{1,14}$/.test(phone);
  }
  
  getChannelName(): string { return 'SMS'; }
  
  private async sendSMS(message: string, phone: string): Promise<void> {
    console.log(`Sending SMS to ${phone}: ${message}`);
  }
}

// ✅ Push Notification
class PushNotification extends BaseNotification {
  async send(message: string, recipient: string): Promise<boolean> {
    if (!this.validateRecipient(recipient)) return false;
    
    try {
      await this.sendPush(message, recipient);
      this.log('Push sent', recipient, true);
      return true;
    } catch (error) {
      this.log('Push failed', recipient, false);
      return false;
    }
  }
  
  validateRecipient(deviceToken: string): boolean {
    return deviceToken.length >= 64; // Token mínimo
  }
  
  getChannelName(): string { return 'PUSH'; }
  
  private async sendPush(message: string, token: string): Promise<void> {
    console.log(`Sending push to ${token}: ${message}`);
  }
}

Sistema de notificação:

// ✅ Gerenciador que usa polimorfismo
class NotificationManager {
  private channels: Map<string, NotificationChannel> = new Map();
  
  registerChannel(name: string, channel: NotificationChannel): void {
    this.channels.set(name, channel);
  }
  
  async sendNotification(
    channelName: string, 
    message: string, 
    recipient: string
  ): Promise<boolean> {
    const channel = this.channels.get(channelName);
    if (!channel) {
      console.log(`Channel ${channelName} not found`);
      return false;
    }
    
    return await channel.send(message, recipient);
  }
  
  async broadcast(message: string, recipients: Record<string, string>): Promise<void> {
    const promises = Object.entries(recipients).map(([channel, recipient]) => 
      this.sendNotification(channel, message, recipient)
    );
    
    await Promise.all(promises);
  }
  
  getAvailableChannels(): string[] {
    return Array.from(this.channels.keys());
  }
}

// ✅ Uso do sistema
const notificationManager = new NotificationManager();

// Registrando canais
notificationManager.registerChannel('email', new EmailNotification({}));
notificationManager.registerChannel('sms', new SMSNotification({}));
notificationManager.registerChannel('push', new PushNotification({}));

// Enviando notificações
await notificationManager.broadcast('Seu pedido foi aprovado!', {
  email: 'user@example.com',
  sms: '+5511999999999',
  push: 'device_token_here'
});

🎯 Resultado: Sistema que pode adicionar WhatsApp, Telegram, Discord sem modificar código existente!

Checklist de Implementação

Como aplicar o Open-Closed Principle no seu código.

✅ Faça Isso

  • Use classes abstratas ou interfaces
  • Implemente polimorfismo
  • Crie uma classe por responsabilidade
  • Use dependency injection
  • Teste cada classe isoladamente
  • Documente contratos claramente

❌ Evite Isso

  • Switch/case gigantes
  • If/else em cadeia
  • Modificar código existente
  • Funções com muitas responsabilidades
  • Hardcoding de tipos
  • Acoplamento forte

🎯 Resumo do Open-Closed Principle

Aberto
Para Extensão
Novas funcionalidades
Fechado
Para Modificação
Código existente intocado
Resultado
Código Estável
Zero regressões

"Software entities should be open for extension, but closed for modification" - Robert C. Martin

Domine TypeScript e OOP

Aprenda orientação a objetos do zero ao avançado com projetos reais.