Kaique Mitsuo Silva Yamamoto
Arquitetura softwareFrontend webTypeScriptClean CodeSOLID

O — Open/Closed Principle

Aberta para extensão, fechada para modificação. Exemplos full-stack com strategy pattern, middleware e componentes React.

O — Open/Closed Principle (OCP)

"Entidades de software (classes, módulos, funções) devem ser abertas para extensão, mas fechadas para modificação." — Bertrand Meyer (1988)

O que é, em português claro

Imagine uma tomada elétrica brasileira. Ela aceita qualquer dispositivo que siga o padrão NBR 14136 — ventilador, carregador, TV. Se você quiser usar um novo dispositivo, não precisa reformar a parede. Basta plugá-lo.

Agora imagine se a tomada só aceitasse o ventilador original. Para ligar uma TV, você teria que abrir a parede e trocar a fiação. Isso é código fechado para extensão — você precisa modificar o que já existe para adicionar algo novo.

OCP diz: desenhe para aceitar novos comportamentos sem tocar no código existente.

Onde acontece na vida real

// ❌ Para cada novo formato, preciso MODIFICAR essa função
function sendNotification(type: string, message: string) {
  if (type === 'email') {
    // envia email
  } else if (type === 'sms') {
    // envia sms
  } else if (type === 'push') {
    // envia push notification
  } else if (type === 'whatsapp') {
    // PRECISO ADICIONAR MAIS UM ELSE IF
  }
  // Cada novo canal = modificar função existente = risco de quebrar os anteriores
}

Exemplo prático — Frontend (React)

❌ Componente fechado para extensão

// PaymentForm.tsx — cada novo método de pagamento = modificar o componente
function PaymentForm({ method }: { method: string }) {
  if (method === 'credit_card') {
    return (
      <form>
        <input placeholder="Número do cartão" />
        <input placeholder="Validade" />
        <input placeholder="CVV" />
        <button>Pagar com cartão</button>
      </form>
    );
  } else if (method === 'pix') {
    return (
      <form>
        <input placeholder="Chave PIX" />
        <button>Gerar QR Code</button>
      </form>
    );
  } else if (method === 'boleto') {
    return (
      <form>
        <input placeholder="CPF" />
        <button>Gerar boleto</button>
      </form>
    );
  }
  // Adicionar "crypto"? Preciso abrir este arquivo e mexer no código existente.
  return null;
}

✅ Aberto para extensão via composição

// Tipagem do contrato
interface PaymentMethodForm {
  label: string;
  component: React.FC<{ onSuccess: () => void }>;
}

// Cada método é um componente independente
function CreditCardForm({ onSuccess }: { onSuccess: () => void }) {
  return (
    <form onSubmit={onSuccess}>
      <input placeholder="Número do cartão" />
      <input placeholder="Validade" />
      <input placeholder="CVV" />
      <button>Pagar com cartão</button>
    </form>
  );
}

function PixForm({ onSuccess }: { onSuccess: () => void }) {
  return (
    <form onSubmit={onSuccess}>
      <input placeholder="Chave PIX" />
      <button>Gerar QR Code</button>
    </form>
  );
}

// Registro — adicionar um novo método NÃO altera os existentes
const paymentMethods: Record<string, PaymentMethodForm> = {
  credit_card: { label: 'Cartão de Crédito', component: CreditCardForm },
  pix: { label: 'PIX', component: PixForm },
};

// Componente que consome — não muda quando novos métodos são adicionados
function PaymentSelector() {
  const [method, setMethod] = useState('credit_card');
  const SelectedForm = paymentMethods[method].component;

  return (
    <div>
      {Object.entries(paymentMethods).map(([key, { label }]) => (
        <button key={key} onClick={() => setMethod(key)}>{label}</button>
      ))}
      <SelectedForm onSuccess={() => alert('Pagamento realizado!')} />
    </div>
  );
}

Resultado: Adicionar "Crypto" ou "Boleto" significa criar um novo arquivo e registrá-lo em paymentMethods. O PaymentSelector não muda. Os outros formulários não mudam.

Exemplo prático — Backend (NestJS/Express)

❌ Service com if/else para cada caso

// notification.service.ts
class NotificationService {
  async send(type: string, to: string, message: string) {
    if (type === 'email') {
      const transporter = nodemailer.createTransport({ /* config */ });
      await transporter.sendMail({ to, text: message });
    } else if (type === 'sms') {
      await twilioClient.messages.create({ to, body: message });
    } else if (type === 'push') {
      await firebaseAdmin.messaging().send({ token: to, notification: { body: message } });
    }
    // Adicionar WhatsApp? Slack? Telegram? Cada um = else if novo
  }
}

✅ Strategy pattern + interface

// contracts/notification.strategy.ts
interface NotificationStrategy {
  readonly channel: string;
  send(to: string, message: string): Promise<void>;
}
// strategies/email.strategy.ts
class EmailNotification implements NotificationStrategy {
  readonly channel = 'email';

  constructor(private transporter: Transporter) {}

  async send(to: string, message: string): Promise<void> {
    await this.transporter.sendMail({ to, text: message });
  }
}
// strategies/sms.strategy.ts
class SmsNotification implements NotificationStrategy {
  readonly channel = 'sms';

  constructor(private twilio: TwilioClient) {}

  async send(to: string, message: string): Promise<void> {
    await this.twilio.messages.create({ to, body: message });
  }
}
// strategies/push.strategy.ts
class PushNotification implements NotificationStrategy {
  readonly channel = 'push';

  async send(to: string, message: string): Promise<void> {
    await firebaseAdmin.messaging().send({ token: to, notification: { body: message } });
  }
}
// notification.service.ts — não muda quando novos canais são adicionados
class NotificationService {
  private strategies = new Map<string, NotificationStrategy>();

  register(strategy: NotificationStrategy): void {
    this.strategies.set(strategy.channel, strategy);
  }

  async send(channel: string, to: string, message: string): Promise<void> {
    const strategy = this.strategies.get(channel);
    if (!strategy) {
      throw new Error(`Canal não suportado: ${channel}`);
    }
    await strategy.send(to, message);
  }
}

// bootstrap (main.ts ou module)
const notifications = new NotificationService();
notifications.register(new EmailNotification(transporter));
notifications.register(new SmsNotification(twilio));
notifications.register(new PushNotification());

Resultado: Adicionar "WhatsApp" significa criar WhatsAppNotification implements NotificationStrategy e registrá-lo. Nenhuma linha do NotificationService muda.

Middleware como OCP no Express

O Express já aplica OCP nativamente — cada middleware estende o pipeline sem modificar os outros:

// Cada middleware é uma "extensão" — adicionar não modifica os existentes
app.use(cors());           // extensão 1
app.use(helmet());         // extensão 2
app.use(rateLimit());      // extensão 3
app.use('/api', apiRouter); // extensão 4

// Adicionar um novo middleware de compressão:
app.use(compression());    // não toca em nenhum dos anteriores

Regra prática

  • Se você está editando uma função/classe existente para adicionar um novo caso → violação de OCP.
  • Se você está criando um novo arquivo e registrando-o → OCP respeitado.
  • Pergunte: "Para adicionar X, preciso modificar código que já funciona?"

Referências

On this page