O — Open/Closed Principle
Baixar PDFAberta 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 anterioresRegra 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
S — Single Responsibility Principle
Uma classe, uma função, um módulo — uma única razão para mudar. Exemplos full-stack em React, Next.js, NestJS e Express.
L — Liskov Substitution Principle
Subtipos devem ser substituíveis por seus tipos base sem quebrar o comportamento. O pato de borracha e outros exemplos full-stack.