O Open Closed Principle OCP · How it works

1 min read
Mid-level3 min read
Rapid overview

How it works

O — Open/Closed Principle (OCP)

"Software entities should be open for extension but closed for modification."

You should add new behavior without editing existing code.

✅ Example: Payment processors (React/TypeScript)

// paymentProcessors.ts - Define the abstraction
export interface PaymentProcessor {
  processPayment(amount: number, details: PaymentDetails): Promise<PaymentResult>;
  getName(): string;
}

// Existing implementation
export class StripeProcessor implements PaymentProcessor {
  async processPayment(amount: number, details: PaymentDetails): Promise<PaymentResult> {
    // Stripe API logic
    return { success: true, transactionId: 'stripe_123' };
  }

  getName(): string {
    return 'Stripe';
  }
}

export class PayPalProcessor implements PaymentProcessor {
  async processPayment(amount: number, details: PaymentDetails): Promise<PaymentResult> {
    // PayPal API logic
    return { success: true, transactionId: 'paypal_456' };
  }

  getName(): string {
    return 'PayPal';
  }
}

// Add new processor WITHOUT modifying existing code
export class CryptoProcessor implements PaymentProcessor {
  async processPayment(amount: number, details: PaymentDetails): Promise<PaymentResult> {
    // Crypto wallet logic
    return { success: true, transactionId: 'crypto_789' };
  }

  getName(): string {
    return 'Cryptocurrency';
  }
}

// React component using the abstraction
function CheckoutForm({ processor }: { processor: PaymentProcessor }) {
  const handleSubmit = async (amount: number, details: PaymentDetails) => {
    const result = await processor.processPayment(amount, details);
    if (result.success) {
      console.log(`Payment processed via ${processor.getName()}`);
    }
  };

  return <form onSubmit={...}>...</form>;
}

✅ Adding a new payment processor (like Apple Pay or Google Pay) just means creating another PaymentProcessor implementation — no code modification, only extension.

✅ Example: Form validators (React)

// Bad - violates OCP (requires modification for new validation rules)
function validateForm(data: FormData): ValidationErrors {
  const errors: ValidationErrors = {};

  if (!data.email.includes('@')) {
    errors.email = 'Invalid email';
  }

  if (data.password.length < 8) {
    errors.password = 'Password too short';
  }

  // Adding new validation requires editing this function!

  return errors;
}

// Good - follows OCP (extend without modification)
export interface Validator<T> {
  validate(value: T): ValidationError | null;
}

export class EmailValidator implements Validator<string> {
  validate(value: string): ValidationError | null {
    return value.includes('@') ? null : { message: 'Invalid email' };
  }
}

export class PasswordLengthValidator implements Validator<string> {
  constructor(private minLength: number) {}

  validate(value: string): ValidationError | null {
    return value.length >= this.minLength
      ? null
      : { message: `Password must be at least ${this.minLength} characters` };
  }
}

// Add new validators without modifying existing code
export class PasswordStrengthValidator implements Validator<string> {
  validate(value: string): ValidationError | null {
    const hasUpper = /[A-Z]/.test(value);
    const hasNumber = /\d/.test(value);
    return hasUpper && hasNumber
      ? null
      : { message: 'Password must contain uppercase and numbers' };
  }
}

// Compose validators
export class FormValidator {
  private validators: Map<string, Validator<any>[]> = new Map();

  addValidator(field: string, validator: Validator<any>): this {
    if (!this.validators.has(field)) {
      this.validators.set(field, []);
    }
    this.validators.get(field)!.push(validator);
    return this;
  }

  validate(data: Record<string, any>): ValidationErrors {
    const errors: ValidationErrors = {};

    for (const [field, validators] of this.validators) {
      for (const validator of validators) {
        const error = validator.validate(data[field]);
        if (error) {
          errors[field] = error.message;
          break;
        }
      }
    }

    return errors;
  }
}

// Usage - configure without modifying code
const formValidator = new FormValidator()
  .addValidator('email', new EmailValidator())
  .addValidator('password', new PasswordLengthValidator(8))
  .addValidator('password', new PasswordStrengthValidator());

✅ Example: Chart renderers (Angular)

// chart.types.ts - Abstraction
export interface ChartRenderer {
  render(data: ChartData, container: HTMLElement): void;
  destroy(): void;
}

// Existing implementations
@Injectable()
export class BarChartRenderer implements ChartRenderer {
  render(data: ChartData, container: HTMLElement): void {
    // D3 bar chart rendering
  }

  destroy(): void {
    // Cleanup
  }
}

@Injectable()
export class LineChartRenderer implements ChartRenderer {
  render(data: ChartData, container: HTMLElement): void {
    // D3 line chart rendering
  }

  destroy(): void {
    // Cleanup
  }
}

// Extend without modification - add new chart type
@Injectable()
export class PieChartRenderer implements ChartRenderer {
  render(data: ChartData, container: HTMLElement): void {
    // D3 pie chart rendering
  }

  destroy(): void {
    // Cleanup
  }
}

// Component uses the abstraction
@Component({
  selector: 'app-chart',
  template: '<div #chartContainer></div>'
})
export class ChartComponent implements OnInit, OnDestroy {
  @ViewChild('chartContainer', { static: true }) container!: ElementRef;
  @Input() data!: ChartData;
  @Input() renderer!: ChartRenderer; // Injected by parent

  ngOnInit() {
    this.renderer.render(this.data, this.container.nativeElement);
  }

  ngOnDestroy() {
    this.renderer.destroy();
  }
}

// Usage - switch chart types without modifying ChartComponent
<app-chart [data]="salesData" [renderer]="barRenderer"></app-chart>
<app-chart [data]="salesData" [renderer]="lineRenderer"></app-chart>
<app-chart [data]="salesData" [renderer]="pieRenderer"></app-chart>

✅ Example: Middleware/Plugin architecture

// middleware.ts - Open for extension
export interface Middleware {
  process(context: RequestContext, next: () => Promise<void>): Promise<void>;
}

export class LoggingMiddleware implements Middleware {
  async process(context: RequestContext, next: () => Promise<void>): Promise<void> {
    console.log(`Request: ${context.url}`);
    await next();
    console.log(`Response: ${context.status}`);
  }
}

export class AuthMiddleware implements Middleware {
  async process(context: RequestContext, next: () => Promise<void>): Promise<void> {
    if (!context.headers.authorization) {
      context.status = 401;
      return;
    }
    await next();
  }
}

// Add new middleware without modifying existing code
export class RateLimitMiddleware implements Middleware {
  private requests = new Map<string, number>();

  async process(context: RequestContext, next: () => Promise<void>): Promise<void> {
    const count = this.requests.get(context.ip) || 0;
    if (count > 100) {
      context.status = 429;
      return;
    }
    this.requests.set(context.ip, count + 1);
    await next();
  }
}

// Pipeline composes middleware without modification
export class MiddlewarePipeline {
  private middlewares: Middleware[] = [];

  use(middleware: Middleware): this {
    this.middlewares.push(middleware);
    return this;
  }

  async execute(context: RequestContext): Promise<void> {
    let index = 0;

    const next = async (): Promise<void> => {
      if (index < this.middlewares.length) {
        const middleware = this.middlewares[index++];
        await middleware.process(context, next);
      }
    };

    await next();
  }
}

// Configure pipeline by adding new middleware
const pipeline = new MiddlewarePipeline()
  .use(new LoggingMiddleware())
  .use(new AuthMiddleware())
  .use(new RateLimitMiddleware());

💡 In frontend applications:

  • Use interfaces/abstract classes to define contracts (payment processors, validators, renderers, middleware).
  • Implement strategy pattern for swappable behaviors (sorting strategies, filtering strategies).
  • Use plugin/middleware architectures for extensible pipelines.
  • React: Higher-order components (HOCs) and render props enable extension without modification.
  • Angular: Dependency injection and providers make it easy to swap implementations.
  • Component libraries: Export component interfaces/props that consumers can extend via composition.

See also