6.1 KiB
id, title, category, priority, tags, related
| id | title | category | priority | tags | related | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| solid-ocp-extension | SOLID - Open/Closed Principle (Extension) | solid-principles | critical |
|
|
Open/Closed Principle - Extension
Software entities should be open for extension but closed for modification. Add new functionality by adding new code, not by changing existing code.
Bad Example
// Anti-pattern: Modifying existing code to add new functionality
class PaymentProcessor {
processPayment(payment: Payment): PaymentResult {
switch (payment.method) {
case 'credit_card':
return this.processCreditCard(payment);
case 'paypal':
return this.processPayPal(payment);
case 'stripe':
return this.processStripe(payment);
// Every new payment method requires modifying this class
case 'apple_pay':
return this.processApplePay(payment);
case 'google_pay':
return this.processGooglePay(payment);
case 'crypto':
return this.processCrypto(payment);
default:
throw new Error(`Unknown payment method: ${payment.method}`);
}
}
private processCreditCard(payment: Payment): PaymentResult {
// Credit card logic
}
private processPayPal(payment: Payment): PaymentResult {
// PayPal logic
}
private processStripe(payment: Payment): PaymentResult {
// Stripe logic
}
private processApplePay(payment: Payment): PaymentResult {
// Apple Pay logic
}
private processGooglePay(payment: Payment): PaymentResult {
// Google Pay logic
}
private processCrypto(payment: Payment): PaymentResult {
// Crypto logic
}
}
// Adding a new payment method requires:
// 1. Adding a new case to the switch statement
// 2. Adding a new private method
// 3. Testing the entire class again
Good Example
// Correct approach: Open for extension, closed for modification
// Define the contract for payment handlers
interface PaymentHandler {
readonly methodType: string;
canHandle(payment: Payment): boolean;
process(payment: Payment): Promise<PaymentResult>;
validate(payment: Payment): ValidationResult;
}
// Payment processor that never needs modification
class PaymentProcessor {
private handlers: Map<string, PaymentHandler> = new Map();
registerHandler(handler: PaymentHandler): void {
this.handlers.set(handler.methodType, handler);
}
async processPayment(payment: Payment): Promise<PaymentResult> {
const handler = this.handlers.get(payment.method);
if (!handler) {
throw new UnsupportedPaymentMethodError(payment.method);
}
const validation = handler.validate(payment);
if (!validation.isValid) {
throw new PaymentValidationError(validation.errors);
}
return handler.process(payment);
}
getSupportedMethods(): string[] {
return Array.from(this.handlers.keys());
}
}
// Each payment method is a separate class that can be added without modifying PaymentProcessor
class CreditCardPaymentHandler implements PaymentHandler {
readonly methodType = 'credit_card';
canHandle(payment: Payment): boolean {
return payment.method === this.methodType;
}
validate(payment: Payment): ValidationResult {
const errors: string[] = [];
if (!payment.cardNumber || payment.cardNumber.length !== 16) {
errors.push('Invalid card number');
}
if (!payment.cvv || payment.cvv.length !== 3) {
errors.push('Invalid CVV');
}
return { isValid: errors.length === 0, errors };
}
async process(payment: Payment): Promise<PaymentResult> {
// Credit card processing logic
return { success: true, transactionId: generateId() };
}
}
class PayPalPaymentHandler implements PaymentHandler {
readonly methodType = 'paypal';
canHandle(payment: Payment): boolean {
return payment.method === this.methodType;
}
validate(payment: Payment): ValidationResult {
const errors: string[] = [];
if (!payment.paypalEmail) {
errors.push('PayPal email required');
}
return { isValid: errors.length === 0, errors };
}
async process(payment: Payment): Promise<PaymentResult> {
// PayPal processing logic
return { success: true, transactionId: generateId() };
}
}
// Adding a new payment method - no modification to existing code!
class CryptoPaymentHandler implements PaymentHandler {
readonly methodType = 'crypto';
canHandle(payment: Payment): boolean {
return payment.method === this.methodType;
}
validate(payment: Payment): ValidationResult {
const errors: string[] = [];
if (!payment.walletAddress) {
errors.push('Wallet address required');
}
if (!payment.cryptoCurrency) {
errors.push('Cryptocurrency type required');
}
return { isValid: errors.length === 0, errors };
}
async process(payment: Payment): Promise<PaymentResult> {
// Crypto processing logic
return { success: true, transactionId: generateId() };
}
}
// Application setup - register handlers
const processor = new PaymentProcessor();
processor.registerHandler(new CreditCardPaymentHandler());
processor.registerHandler(new PayPalPaymentHandler());
processor.registerHandler(new CryptoPaymentHandler());
// Add new payment methods by simply registering new handlers
Why
-
Stability: The
PaymentProcessorclass is stable and tested. Adding new payment methods doesn't risk breaking existing functionality. -
Team Scalability: Different developers can work on different payment handlers simultaneously without merge conflicts.
-
Plugin Architecture: Payment handlers can be loaded dynamically, even from external packages or plugins.
-
Testing Isolation: Each handler is tested independently. New handlers don't require retesting old ones.
-
Feature Flags: New payment methods can be enabled/disabled by simply registering or not registering the handler.
-
Compliance: When regulations change for one payment method, only that handler needs updating.
-
Rollback Safety: If a new payment handler has issues, remove it without touching proven code.