O Open Closed Principle OCP · How it works
1 min readRapid 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.