Software entities should be open for extension but closed for modification - add new functionality without changing existing code. Bertrand Meyer's definition (1988): 'Open for extension' means behavior can be extended. 'Closed for modification' means source code unchanged. Robert Martin refined: Achieve via abstractions (interfaces, abstract classes), polymorphism, dependency injection. Example: PaymentProcessor interface with process() method. Add StripePayment, PayPalPayment implementations without modifying OrderService that depends on PaymentProcessor interface. Benefits: Reduces regression risk (existing code untouched), easier feature addition (no ripple effects), better testing (existing tests pass). Violation: switch(paymentType) scattered across codebase - adding new type modifies multiple places. Fix: Replace conditional with polymorphism. Note: OCP is design goal, not absolute rule. Balance extensibility with simplicity. Uncle Bob: Plugin architecture is 'apotheosis of OCP' - new plugins extend system without modifying core.
Solid Open Closed FAQ & Answers
50 expert Solid Open Closed answers researched from official documentation. Every answer cites authoritative sources you can verify.
unknown
50 questionsUse abstraction, dependency injection, and composition. Core pattern: (1) Define interface for behavior: interface PaymentProcessor {process(amount: number): Promise
Violation 1: Type checking with if/switch. Code: if (type === 'credit') chargeCreditCard(); else if (type === 'paypal') chargePayPal() - adding type modifies code. Fix: Replace conditional with polymorphism. Create PaymentProcessor interface, implementations per type. Violation 2: Scattered switch statements on same enum. Multiple locations check type, all need modification. Fix: Centralize via polymorphism or Strategy pattern. Violation 3: Modifying class for optional features. class Order {calculate() {let total = price; if (hasTax) total += tax; if (hasDiscount) total -= discount}} - adding features modifies class. Fix: Decorator pattern - TaxDecorator, DiscountDecorator wrap base calculator. Violation 4: Direct instantiation: new CreditCardProcessor() in logic. Fix: Dependency injection with factory or DI container. Detection: Ask 'Adding feature requires modifying 5+ classes?' = OCP violation. Refactoring limits conditionals to single place (factory/registry) instead of spreading throughout code. Modern: Use composition over inheritance for extensibility.
YAGNI (You Aren't Gonna Need It) is a higher-order metaprinciple that determines when to apply OCP. OCP appropriate for: (1) External-facing APIs - customer-facing APIs, 3rd party libraries where backward compatibility required. (2) Plugin systems - independent clients extending your code. (3) Hard-to-change decisions - fundamental architecture, public interfaces set in stone. OCP NOT appropriate for: (1) Internal application code - most application development, OCP leads to over-engineering. (2) Bug fixes - MUST be fixed in place, not extended. (3) Early development - Rule of Three: wait until third variation before abstracting (Martin Fowler). Two instances? Direct implementation. Three instances? Extract interface. (4) Performance improvements - internal refactoring without API changes. (5) Simple stable code - utility functions unlikely needing extension. Risks of premature abstraction: wrong abstraction harder to fix than duplication (2024 Ousterhout-Martin discussion), excessive indirection for simple operations, interfaces with single implementation. Example: Payment processing with two types? Direct if/else acceptable. Three types appearing? Time for PaymentProcessor interface. Context: Apply OCP where extension likely and cost justified, skip where simplicity > flexibility. Balance: Don't predict future needs (YAGNI), do provide extension points for known variation.
Strategy pattern is primary OCP implementation. Strategy defines family of algorithms, encapsulates each, makes them interchangeable - directly achieves 'open for extension, closed for modification'. Pattern structure: (1) Strategy interface: interface SortStrategy {sort
Plugin architecture is ultimate OCP expression - Uncle Bob calls it 'apotheosis of OCP'. System knows nothing about plugins, plugins fulfill contract to extend system. Core doesn't change when adding/removing plugins. Implementation techniques: (1) Interface-based contracts: interface Plugin {name: string; initialize(): void; execute(context: any): void}. System loads plugins implementing interface. (2) Reflection/discovery: Scan directory for files implementing plugin interface, dynamically import. Node.js: fs.readdir, dynamic import(). (3) Configuration-based: plugins.json lists plugins to load, system reads config and instantiates. (4) Event-driven: System emits events, plugins register handlers. Example: app.emit('beforeSave', data), plugins hook into lifecycle. (5) Dependency injection: Register plugins in DI container, inject where needed. Modern examples: GraphQL Federation (extend schema via new subgraphs without modifying existing), VSCode extensions (extend editor without core changes), Express middleware (app.use() adds plugins). Benefits: Zero coupling between core and plugins, plugins independently developed/tested/deployed, runtime extensibility without recompilation. Pattern: Core defines contracts, plugins implement contracts, system discovers/loads plugins dynamically.
In functional programming, OCP achieved via composition and higher-order functions instead of inheritance. Core concept: Functions are composable, extensible without modification. Techniques: (1) Higher-order functions: Accept functions as parameters, return functions. Example: const withLogging = (fn) => (...args) => {console.log('calling'); return fn(...args);} - extends behavior without modifying fn. (2) Function composition: const pipeline = compose(validate, transform, save) - add steps by composing, no modification. (3) Partial application: const processUser = partial(process, {role: 'admin'}) - create specialized versions without changing original. (4) Decorators/middleware: Express middleware chains extend behavior: app.use(auth, validate, handler). Strategy pattern is just higher-order function in FP: processData(data, sortFunction) - pass algorithm as parameter. Benefits: Natural extensibility (functions first-class), no inheritance hierarchy, easier testing (pure functions). FP doesn't need 'design' for extensibility - substituting functions at will is built-in. Example extending: const enhanced = pipe(original, newBehavior1, newBehavior2). Original function unchanged. TypeScript FP: Use generics for type-safe composition: compose<T, U, V>(f: (x: U) => V, g: (x: T) => U).
React component composition achieves OCP - components open for extension via props/children, closed for modification. Patterns: (1) Children prop:
Important: No static analysis tool directly detects SOLID violations - they identify indirect indicators. Key metrics: (1) Cyclomatic complexity - measures control flow branches (if/switch/loops). Threshold >10 per method suggests conditionals needing polymorphism. (2) Cognitive complexity (2025) - measures mental effort to understand code. Harder to calculate but more accurate for maintainability. (3) Bumpy Road code smell (CodeScene 2025) - functions with multiple chunks of nested conditionals, each bump represents missing abstraction. (4) Switch statement count - multiple switches on same enum = OCP violation. (5) Change coupling - classes changing together indicate missing abstraction layer. Tools and capabilities: (1) SonarQube - reports cognitive/cyclomatic complexity, code smells for maintainability, BUT no SOLID-specific rules. Indirect: high complexity flags candidates for refactoring. (2) CodeScene (2025) - detects Bumpy Road pattern, change coupling heatmaps, hotspot analysis. (3) ESLint plugins - complexity limits, no-switch rules for JS/TS. (4) IDE refactoring (VSCode, WebStorm) - 'Extract interface', 'Replace conditional with polymorphism' suggestions. Detection strategy: Search codebase for typeof/instanceof chains, enum-based switches in multiple locations, classes with 5+ if statements. Threshold: >3 switches on same type = extract polymorphism. Limitation: Tools detect symptoms not architectural violations - requires human judgment on abstraction value.
Replace switch/if-else chains with polymorphism using Strategy pattern. Before: function calculatePrice(product) {switch(product.type) {case 'book': return product.basePrice * 0.9; case 'electronics': return product.basePrice * 1.2; default: return product.basePrice;}} - adding new type modifies function. After: interface PricingStrategy {calculate(basePrice: number): number;} class BookPricing implements PricingStrategy {calculate(base: number) {return base * 0.9;}} class ElectronicsPricing implements PricingStrategy {calculate(base: number) {return base * 1.2;}} class Product {constructor(private pricing: PricingStrategy, private basePrice: number) {} getPrice() {return this.pricing.calculate(this.basePrice);}} Usage: const book = new Product(new BookPricing(), 50) - add FoodPricing without modifying Product. Benefits: Each strategy independently testable, no conditional logic in client, type-safe with TypeScript. Detection: Multiple switches on same enum/type across codebase = violation. Threshold: Three or more cases = extract polymorphism. Modern approach: For simple cases functional: const pricers = {book: (p) => p * 0.9, electronics: (p) => p * 1.2}; price = pricerstype. Classes for complex strategies with state/dependencies.
Decorator wraps objects to add behavior without modifying original - open for extension via wrapping, closed for modification. Structure: (1) Component interface: interface Component {execute(): string;} (2) Base implementation: class BaseNotifier implements Component {execute() {return 'Email sent';}} (3) Decorators wrap components: class SMSDecorator implements Component {constructor(private wrapped: Component) {} execute() {return this.wrapped.execute() + ', SMS sent';}} (4) Compose at runtime: let notifier: Component = new BaseNotifier(); notifier = new SMSDecorator(notifier); notifier = new PushDecorator(notifier); notifier.execute() // 'Email sent, SMS sent, Push sent'. Benefits: Add responsibilities dynamically without subclassing, compose behaviors in any order, each decorator independently testable. Violation: class Notifier {send(message, withSMS, withPush) {sendEmail(message); if(withSMS) sendSMS(); if(withPush) sendPush();}} - adding channel modifies class. Real-world: Express middleware (request/response decoration), React HOCs, Java I/O streams. TypeScript advantage: Type safety preserved through composition. Modern 2025: Functional decorators via higher-order functions common in React hooks.
Template Method defines algorithm skeleton in base class, subclasses override specific steps without changing structure - algorithm closed for modification, steps open for extension. Pattern: abstract class DataProcessor {process(data: any) {const validated = this.validate(data); const transformed = this.transform(validated); return this.save(transformed);} abstract validate(data: any): any; abstract transform(data: any): any; abstract save(data: any): any;} Concrete: class CSVProcessor extends DataProcessor {validate(data) {/* CSV validation /} transform(data) {/ CSV parsing /} save(data) {/ Save to DB /}} class JSONProcessor extends DataProcessor {validate(data) {/ JSON validation /} transform(data) {/ JSON parsing /} save(data) {/ Save to DB */}} Benefits: Common algorithm in one place (DRY), subclasses cannot change processing order, extension points explicit via abstract methods. Violation: if(type === 'CSV') {validateCSV(); parseCSV(); save();} else if(type === 'JSON') {validateJSON(); parseJSON(); save();} - algorithm duplicated. Real-world: React lifecycle methods (componentWillMount/Mount/Update hooks), build tools (prebuild/build/postbuild), testing frameworks (setup/test/teardown). Modern: Prefer composition over inheritance - use Strategy pattern with pipeline for better flexibility.
Generics create reusable type-safe components open for extension without modification. Without generics: class UserRepository {private users: User[] = []; add(user: User) {this.users.push(user);}} - need ProductRepository, OrderRepository copies. With generics: class Repository
Dependency injection allows changing behavior by injecting different implementations - open for extension via different injections, closed for modification. Without DI: class OrderService {process(order: Order) {const paypal = new PayPalProcessor(); paypal.charge(order.amount);}} - changing payment processor requires modifying OrderService. With DI: interface PaymentProcessor {charge(amount: number): Promise
Observer decouples subject from observers - add new observers without modifying subject. Pattern: interface Observer {update(data: any): void;} class Subject {private observers: Observer[] = []; attach(observer: Observer) {this.observers.push(observer);} notify(data: any) {this.observers.forEach(o => o.update(data));}} class Logger implements Observer {update(data: any) {console.log('Log:', data);}} class EmailNotifier implements Observer {update(data: any) {sendEmail(data);}} Usage: const subject = new Subject(); subject.attach(new Logger()); subject.attach(new EmailNotifier()); subject.notify('Order placed') - add SMSNotifier without modifying Subject. Benefits: Subject doesn't know concrete observers (low coupling), add/remove observers at runtime, each observer independently testable. Violation: class OrderService {placeOrder(order) {saveOrder(order); console.log('logged'); sendEmail(order); sendSMS(order);}} - adding notification channel modifies service. Modern alternatives: Event emitters: class EventBus {private handlers = new Map(); on(event, handler) {this.handlers.set(event, [...this.handlers.get(event) || [], handler]);} emit(event, data) {this.handlers.get(event)?.forEach(h => h(data));}}. Real-world: DOM events, RxJS observables, Node.js EventEmitter, Redux store subscriptions.
Externalize behavior to configuration - change behavior via config without code modification. Pattern: Load rules/strategies from config.json instead of hardcoding. Example: Validation rules in config: {validationRules: [{field: 'email', type: 'email', required: true}, {field: 'age', type: 'number', min: 18}]} Code: class Validator {constructor(private config: ValidationConfig) {} validate(data: any) {return this.config.validationRules.every(rule => this.applyRule(data, rule));} private applyRule(data: any, rule: Rule) {/* generic rule application */}} - add validation rules in config without modifying Validator. Benefits: Non-developers change behavior (product managers), A/B testing via config changes, feature flags for gradual rollouts. Real-world: Feature flags (LaunchDarkly, Unleash), workflow engines (config defines state machines), rule engines (business rules in JSON/YAML), plugin manifests (VSCode extensions.json). Structure: Generic engine executes config-defined behavior. Code knows how to interpret config, not specific behaviors. Modern 2025: Remote config (Firebase Remote Config), edge config (Vercel Edge Config) enable runtime updates without deployment. Limit: Complex logic still needs code - config for variations of existing behaviors, not fundamentally new features.
Middleware extends request/response processing without modifying core framework - open for extension via middleware chain, closed for modification. Express: app.use((req, res, next) => {/* middleware logic /; next();}). Chain: app.use(cors()); app.use(helmet()); app.use(bodyParser.json()); app.use(auth); app.use(logging); app.get('/api', handler) - each middleware extends behavior, framework unchanged. NestJS: @Injectable() class LoggingMiddleware implements NestMiddleware {use(req, res, next) {console.log('Request...'); next();}} apply in module: consumer.apply(LoggingMiddleware).forRoutes(''). Benefits: Compose cross-cutting concerns (auth, logging, caching, compression), order matters (auth before business logic), reusable across routes, independently testable. Violation: function handler(req, res) {if(!authorized) return res.status(401); log(req); const parsed = JSON.parse(req.body); /* business logic */} - concerns mixed, adding middleware requires modifying handler. Modern: Fastify decorators, Koa compose, serverless middleware (middy for AWS Lambda). Pattern: Chain of Responsibility - each middleware decides to process and/or pass to next. TypeScript: Typed middleware for type-safe req/res extensions.
Factory centralizes object creation - add new types by registering in factory, clients unchanged. Pattern: interface Product {use(): void;} class ProductFactory {private registry = new Map<string, () => Product>(); register(type: string, creator: () => Product) {this.registry.set(type, creator);} create(type: string): Product {const creator = this.registry.get(type); if(!creator) throw new Error('Unknown type'); return creator();}} Usage: const factory = new ProductFactory(); factory.register('A', () => new ConcreteProductA()); factory.register('B', () => new ConcreteProductB()); const product = factory.create('A') - add type 'C' by registering, no client modification. Benefits: Single place for creation logic, clients depend on interface not concrete classes, type registration at runtime. Violation: function createProduct(type: string) {if(type === 'A') return new ConcreteProductA(); if(type === 'B') return new ConcreteProductB();} - adding type modifies function. Abstract Factory: Factory of factories for product families. Real-world: React.createElement, Node.js require() module resolution, Dependency injection containers. Modern: Use DI container instead of manual factory for complex scenarios. TypeScript: Generic factory: class Factory
Custom hooks compose to extend functionality without modifying base hooks - open for extension via composition, closed for modification. Base hook: const useData = () => {const [data, setData] = useState([]); useEffect(() => {fetch('/api/data').then(r => r.json()).then(setData);}, []); return data;} - encapsulates data fetching. Extend via composition: const useFilteredData = (filter: (item: any) => boolean) => {const data = useData(); return useMemo(() => data.filter(filter), [data, filter]);} - extends useData without modification. Further extend: const usePaginatedData = (page: number, size: number) => {const filtered = useFilteredData(item => true); return useMemo(() => filtered.slice(page * size, (page + 1) * size), [filtered, page, size]);}. Benefits: Each hook single responsibility, compose hooks like functions, independently testable, reusable across components. Violation: const useData = (shouldFilter, filterFn, shouldPaginate, page, size) => {/* complex logic with many conditionals */} - monolithic hook. Modern 2025: Custom hooks for business logic (useAuth, useCart), third-party (react-query, swr) build composable data fetching. Pattern: Hooks are functional composition achieving OCP without classes. TypeScript: Generic hooks: const useAsync
Chain of Responsibility passes requests along chain of handlers - add new handlers without modifying existing ones or client. Pattern: interface Handler {setNext(handler: Handler): Handler; handle(request: any): any;} abstract class BaseHandler implements Handler {private next: Handler | null = null; setNext(handler: Handler): Handler {this.next = handler; return handler;} handle(request: any): any {if(this.next) return this.next.handle(request); return null;}} Concrete: class AuthHandler extends BaseHandler {handle(request: any) {if(!request.token) return {error: 'Unauthorized'}; return super.handle(request);}} class LogHandler extends BaseHandler {handle(request: any) {console.log('Request:', request); return super.handle(request);}} Usage: const auth = new AuthHandler(); const log = new LogHandler(); const validate = new ValidateHandler(); auth.setNext(log).setNext(validate); auth.handle(request) - add CacheHandler without modifying existing handlers. Benefits: Decouple sender from receivers, dynamic chain configuration, each handler independently testable. Real-world: Express/Koa middleware, Redux middleware, logging frameworks. Modern functional: const chain = [authMiddleware, logMiddleware, validateMiddleware].reduce((next, mw) => (req) => mw(req, next), finalHandler).
Event-driven decouples producers from consumers - add new consumers without modifying producers. Pattern: class EventBus {private handlers = new Map<string, Array<(data: any) => void>>(); on(event: string, handler: (data: any) => void) {const existing = this.handlers.get(event) || []; this.handlers.set(event, [...existing, handler]);} emit(event: string, data: any) {const handlers = this.handlers.get(event) || []; handlers.forEach(h => h(data));}} Usage: const bus = new EventBus(); bus.on('order.placed', (order) => sendEmail(order)); bus.on('order.placed', (order) => updateInventory(order)); bus.emit('order.placed', orderData) - add new listener without modifying emit logic. Benefits: Producers don't know consumers (low coupling), add handlers at runtime, multiple handlers per event, asynchronous processing. Violation: class OrderService {placeOrder(order) {saveOrder(order); emailService.send(order); inventoryService.update(order); analyticsService.track(order);}} - adding consumer modifies service. Real-world: Node.js EventEmitter, RxJS Subjects, Domain events in DDD, Message queues (RabbitMQ, Kafka), Webhooks. Modern 2025: Event sourcing (store events not state), CQRS (command/query separation via events). TypeScript: Typed events: interface Events {'order.placed': Order; 'user.registered': User;} type EventHandler
Visitor separates algorithms from object structure - add new operations without modifying element classes. Pattern: interface Element {accept(visitor: Visitor): void;} interface Visitor {visitConcreteElementA(el: ConcreteElementA): void; visitConcreteElementB(el: ConcreteElementB): void;} class ConcreteElementA implements Element {accept(visitor: Visitor) {visitor.visitConcreteElementA(this);}} class ExportVisitor implements Visitor {visitConcreteElementA(el) {/* export A as JSON /} visitConcreteElementB(el) {/ export B as JSON /}} class ValidateVisitor implements Visitor {visitConcreteElementA(el) {/ validate A /} visitConcreteElementB(el) {/ validate B */}} Usage: const element = new ConcreteElementA(); element.accept(new ExportVisitor()) - add PrintVisitor without modifying elements. Benefits: Add operations without modifying elements, operations grouped in visitor (related logic together), follows Single Responsibility. Trade-off: Adding new element types requires updating all visitors. Use when: Stable element structure, frequently adding new operations. Real-world: AST traversal (Babel, ESLint plugins), document processing (export to PDF/HTML/XML), reporting (different report formats). Modern: TypeScript discriminated unions sometimes better: type Shape = Circle | Square; function area(s: Shape) {switch(s.type) {case 'circle': return Math.PI * s.radius ** 2; case 'square': return s.side ** 2;}}.
API versioning allows introducing new API versions without modifying existing versions - new versions extend functionality, old versions unchanged for backward compatibility. Strategies: (1) URL versioning: /api/v1/users vs /api/v2/users - different routes, separate controllers. app.use('/api/v1', v1Router); app.use('/api/v2', v2Router). (2) Header versioning: Accept: application/vnd.api+json;version=1 - same route, route handler checks version header. (3) Query parameter: /api/users?version=2 - least preferred. Implementation preserving OCP: interface UserService {getUser(id: string): User;} class UserServiceV1 implements UserService {getUser(id) {return {id, name};}} class UserServiceV2 implements UserService {getUser(id) {return {id, name, email, createdAt};}} - v2 extends response without modifying v1. Benefits: Existing clients unaffected (closed for modification), new features in new versions (open for extension), gradual migration possible. Violation: Single endpoint with if(version === '1') ... else if(version === '2') scattered across codebase. Modern 2025: GraphQL avoids versioning via schema evolution (add fields, deprecate old ones), tRPC uses TypeScript types for contracts. Best practice: Semantic versioning for breaking changes, sunset policy for old versions, automated compatibility tests.
GraphQL evolves schema by adding fields and deprecating old ones without breaking existing queries - schema open for extension via new fields, existing queries unchanged (closed for modification). Evolution pattern: Old schema: type User {id: ID!; name: String!;} Add fields (non-breaking): type User {id: ID!; name: String!; email: String; profile: Profile;} - existing queries requesting id and name work unchanged. Deprecate fields: type User {id: ID!; name: String!; username: String @deprecated(reason: 'Use name instead');} - old queries work, clients warned to migrate. Benefits: Versionless API (no /v1, /v2), additive changes (new fields, new types, new arguments with defaults), clients request only needed fields, introspection shows deprecations. Violation: Removing fields breaks clients: type User {id: ID!; /* name removed */} - queries requesting name fail. Best practices: Never remove fields (deprecate instead), make new fields nullable or provide defaults, use @deprecated directive with migration instructions, monitor field usage before removal. Tools: GraphQL Code Generator (typed clients auto-update), Apollo Studio (track field usage). Modern 2025: Federation v2 (extend schema across subgraphs), schema composition (stitch schemas). Real-world: GitHub API (evolved over 7+ years without versions), Shopify API (deprecated fields remain for years).
Expand-contract pattern evolves database schema without breaking running applications - expand schema (add new), migrate data, contract (remove old) in separate deployments. Pattern for renaming column: (1) Expand: ALTER TABLE users ADD COLUMN email_address VARCHAR(255); Dual writes: UPDATE users SET email = ?, email_address = ? WHERE id = ?. Deploy application reading from both columns. (2) Migrate: UPDATE users SET email_address = email WHERE email_address IS NULL. Verify data consistency. (3) Contract: ALTER TABLE users DROP COLUMN email. Deploy application using only new column. Benefits: Zero-downtime migrations, old application version works during deployment, rollback possible at each stage, follows OCP (new schema extends old, old code unchanged until migration complete). Violation: Direct breaking change: ALTER TABLE users DROP COLUMN email immediately - running app crashes. Tools: Flyway, Liquibase (version migrations), online schema change (pt-online-schema-change for MySQL, pg_repack for PostgreSQL). Modern 2025: Blue-green deployments (two databases, switch after migration), shadow traffic (test new schema with copy of production traffic). Best practice: Backward-compatible migrations only, feature flags for schema-dependent code, automated migration testing.
Software entities should be open for extension but closed for modification - add new functionality without changing existing code. Bertrand Meyer's definition (1988): 'Open for extension' means behavior can be extended. 'Closed for modification' means source code unchanged. Robert Martin refined: Achieve via abstractions (interfaces, abstract classes), polymorphism, dependency injection. Example: PaymentProcessor interface with process() method. Add StripePayment, PayPalPayment implementations without modifying OrderService that depends on PaymentProcessor interface. Benefits: Reduces regression risk (existing code untouched), easier feature addition (no ripple effects), better testing (existing tests pass). Violation: switch(paymentType) scattered across codebase - adding new type modifies multiple places. Fix: Replace conditional with polymorphism. Note: OCP is design goal, not absolute rule. Balance extensibility with simplicity. Uncle Bob: Plugin architecture is 'apotheosis of OCP' - new plugins extend system without modifying core.
Use abstraction, dependency injection, and composition. Core pattern: (1) Define interface for behavior: interface PaymentProcessor {process(amount: number): Promise
Violation 1: Type checking with if/switch. Code: if (type === 'credit') chargeCreditCard(); else if (type === 'paypal') chargePayPal() - adding type modifies code. Fix: Replace conditional with polymorphism. Create PaymentProcessor interface, implementations per type. Violation 2: Scattered switch statements on same enum. Multiple locations check type, all need modification. Fix: Centralize via polymorphism or Strategy pattern. Violation 3: Modifying class for optional features. class Order {calculate() {let total = price; if (hasTax) total += tax; if (hasDiscount) total -= discount}} - adding features modifies class. Fix: Decorator pattern - TaxDecorator, DiscountDecorator wrap base calculator. Violation 4: Direct instantiation: new CreditCardProcessor() in logic. Fix: Dependency injection with factory or DI container. Detection: Ask 'Adding feature requires modifying 5+ classes?' = OCP violation. Refactoring limits conditionals to single place (factory/registry) instead of spreading throughout code. Modern: Use composition over inheritance for extensibility.
YAGNI (You Aren't Gonna Need It) is a higher-order metaprinciple that determines when to apply OCP. OCP appropriate for: (1) External-facing APIs - customer-facing APIs, 3rd party libraries where backward compatibility required. (2) Plugin systems - independent clients extending your code. (3) Hard-to-change decisions - fundamental architecture, public interfaces set in stone. OCP NOT appropriate for: (1) Internal application code - most application development, OCP leads to over-engineering. (2) Bug fixes - MUST be fixed in place, not extended. (3) Early development - Rule of Three: wait until third variation before abstracting (Martin Fowler). Two instances? Direct implementation. Three instances? Extract interface. (4) Performance improvements - internal refactoring without API changes. (5) Simple stable code - utility functions unlikely needing extension. Risks of premature abstraction: wrong abstraction harder to fix than duplication (2024 Ousterhout-Martin discussion), excessive indirection for simple operations, interfaces with single implementation. Example: Payment processing with two types? Direct if/else acceptable. Three types appearing? Time for PaymentProcessor interface. Context: Apply OCP where extension likely and cost justified, skip where simplicity > flexibility. Balance: Don't predict future needs (YAGNI), do provide extension points for known variation.
Strategy pattern is primary OCP implementation. Strategy defines family of algorithms, encapsulates each, makes them interchangeable - directly achieves 'open for extension, closed for modification'. Pattern structure: (1) Strategy interface: interface SortStrategy {sort
Plugin architecture is ultimate OCP expression - Uncle Bob calls it 'apotheosis of OCP'. System knows nothing about plugins, plugins fulfill contract to extend system. Core doesn't change when adding/removing plugins. Implementation techniques: (1) Interface-based contracts: interface Plugin {name: string; initialize(): void; execute(context: any): void}. System loads plugins implementing interface. (2) Reflection/discovery: Scan directory for files implementing plugin interface, dynamically import. Node.js: fs.readdir, dynamic import(). (3) Configuration-based: plugins.json lists plugins to load, system reads config and instantiates. (4) Event-driven: System emits events, plugins register handlers. Example: app.emit('beforeSave', data), plugins hook into lifecycle. (5) Dependency injection: Register plugins in DI container, inject where needed. Modern examples: GraphQL Federation (extend schema via new subgraphs without modifying existing), VSCode extensions (extend editor without core changes), Express middleware (app.use() adds plugins). Benefits: Zero coupling between core and plugins, plugins independently developed/tested/deployed, runtime extensibility without recompilation. Pattern: Core defines contracts, plugins implement contracts, system discovers/loads plugins dynamically.
In functional programming, OCP achieved via composition and higher-order functions instead of inheritance. Core concept: Functions are composable, extensible without modification. Techniques: (1) Higher-order functions: Accept functions as parameters, return functions. Example: const withLogging = (fn) => (...args) => {console.log('calling'); return fn(...args);} - extends behavior without modifying fn. (2) Function composition: const pipeline = compose(validate, transform, save) - add steps by composing, no modification. (3) Partial application: const processUser = partial(process, {role: 'admin'}) - create specialized versions without changing original. (4) Decorators/middleware: Express middleware chains extend behavior: app.use(auth, validate, handler). Strategy pattern is just higher-order function in FP: processData(data, sortFunction) - pass algorithm as parameter. Benefits: Natural extensibility (functions first-class), no inheritance hierarchy, easier testing (pure functions). FP doesn't need 'design' for extensibility - substituting functions at will is built-in. Example extending: const enhanced = pipe(original, newBehavior1, newBehavior2). Original function unchanged. TypeScript FP: Use generics for type-safe composition: compose<T, U, V>(f: (x: U) => V, g: (x: T) => U).
React component composition achieves OCP - components open for extension via props/children, closed for modification. Patterns: (1) Children prop:
Important: No static analysis tool directly detects SOLID violations - they identify indirect indicators. Key metrics: (1) Cyclomatic complexity - measures control flow branches (if/switch/loops). Threshold >10 per method suggests conditionals needing polymorphism. (2) Cognitive complexity (2025) - measures mental effort to understand code. Harder to calculate but more accurate for maintainability. (3) Bumpy Road code smell (CodeScene 2025) - functions with multiple chunks of nested conditionals, each bump represents missing abstraction. (4) Switch statement count - multiple switches on same enum = OCP violation. (5) Change coupling - classes changing together indicate missing abstraction layer. Tools and capabilities: (1) SonarQube - reports cognitive/cyclomatic complexity, code smells for maintainability, BUT no SOLID-specific rules. Indirect: high complexity flags candidates for refactoring. (2) CodeScene (2025) - detects Bumpy Road pattern, change coupling heatmaps, hotspot analysis. (3) ESLint plugins - complexity limits, no-switch rules for JS/TS. (4) IDE refactoring (VSCode, WebStorm) - 'Extract interface', 'Replace conditional with polymorphism' suggestions. Detection strategy: Search codebase for typeof/instanceof chains, enum-based switches in multiple locations, classes with 5+ if statements. Threshold: >3 switches on same type = extract polymorphism. Limitation: Tools detect symptoms not architectural violations - requires human judgment on abstraction value.
Replace switch/if-else chains with polymorphism using Strategy pattern. Before: function calculatePrice(product) {switch(product.type) {case 'book': return product.basePrice * 0.9; case 'electronics': return product.basePrice * 1.2; default: return product.basePrice;}} - adding new type modifies function. After: interface PricingStrategy {calculate(basePrice: number): number;} class BookPricing implements PricingStrategy {calculate(base: number) {return base * 0.9;}} class ElectronicsPricing implements PricingStrategy {calculate(base: number) {return base * 1.2;}} class Product {constructor(private pricing: PricingStrategy, private basePrice: number) {} getPrice() {return this.pricing.calculate(this.basePrice);}} Usage: const book = new Product(new BookPricing(), 50) - add FoodPricing without modifying Product. Benefits: Each strategy independently testable, no conditional logic in client, type-safe with TypeScript. Detection: Multiple switches on same enum/type across codebase = violation. Threshold: Three or more cases = extract polymorphism. Modern approach: For simple cases functional: const pricers = {book: (p) => p * 0.9, electronics: (p) => p * 1.2}; price = pricerstype. Classes for complex strategies with state/dependencies.
Decorator wraps objects to add behavior without modifying original - open for extension via wrapping, closed for modification. Structure: (1) Component interface: interface Component {execute(): string;} (2) Base implementation: class BaseNotifier implements Component {execute() {return 'Email sent';}} (3) Decorators wrap components: class SMSDecorator implements Component {constructor(private wrapped: Component) {} execute() {return this.wrapped.execute() + ', SMS sent';}} (4) Compose at runtime: let notifier: Component = new BaseNotifier(); notifier = new SMSDecorator(notifier); notifier = new PushDecorator(notifier); notifier.execute() // 'Email sent, SMS sent, Push sent'. Benefits: Add responsibilities dynamically without subclassing, compose behaviors in any order, each decorator independently testable. Violation: class Notifier {send(message, withSMS, withPush) {sendEmail(message); if(withSMS) sendSMS(); if(withPush) sendPush();}} - adding channel modifies class. Real-world: Express middleware (request/response decoration), React HOCs, Java I/O streams. TypeScript advantage: Type safety preserved through composition. Modern 2025: Functional decorators via higher-order functions common in React hooks.
Template Method defines algorithm skeleton in base class, subclasses override specific steps without changing structure - algorithm closed for modification, steps open for extension. Pattern: abstract class DataProcessor {process(data: any) {const validated = this.validate(data); const transformed = this.transform(validated); return this.save(transformed);} abstract validate(data: any): any; abstract transform(data: any): any; abstract save(data: any): any;} Concrete: class CSVProcessor extends DataProcessor {validate(data) {/* CSV validation /} transform(data) {/ CSV parsing /} save(data) {/ Save to DB /}} class JSONProcessor extends DataProcessor {validate(data) {/ JSON validation /} transform(data) {/ JSON parsing /} save(data) {/ Save to DB */}} Benefits: Common algorithm in one place (DRY), subclasses cannot change processing order, extension points explicit via abstract methods. Violation: if(type === 'CSV') {validateCSV(); parseCSV(); save();} else if(type === 'JSON') {validateJSON(); parseJSON(); save();} - algorithm duplicated. Real-world: React lifecycle methods (componentWillMount/Mount/Update hooks), build tools (prebuild/build/postbuild), testing frameworks (setup/test/teardown). Modern: Prefer composition over inheritance - use Strategy pattern with pipeline for better flexibility.
Generics create reusable type-safe components open for extension without modification. Without generics: class UserRepository {private users: User[] = []; add(user: User) {this.users.push(user);}} - need ProductRepository, OrderRepository copies. With generics: class Repository
Dependency injection allows changing behavior by injecting different implementations - open for extension via different injections, closed for modification. Without DI: class OrderService {process(order: Order) {const paypal = new PayPalProcessor(); paypal.charge(order.amount);}} - changing payment processor requires modifying OrderService. With DI: interface PaymentProcessor {charge(amount: number): Promise
Observer decouples subject from observers - add new observers without modifying subject. Pattern: interface Observer {update(data: any): void;} class Subject {private observers: Observer[] = []; attach(observer: Observer) {this.observers.push(observer);} notify(data: any) {this.observers.forEach(o => o.update(data));}} class Logger implements Observer {update(data: any) {console.log('Log:', data);}} class EmailNotifier implements Observer {update(data: any) {sendEmail(data);}} Usage: const subject = new Subject(); subject.attach(new Logger()); subject.attach(new EmailNotifier()); subject.notify('Order placed') - add SMSNotifier without modifying Subject. Benefits: Subject doesn't know concrete observers (low coupling), add/remove observers at runtime, each observer independently testable. Violation: class OrderService {placeOrder(order) {saveOrder(order); console.log('logged'); sendEmail(order); sendSMS(order);}} - adding notification channel modifies service. Modern alternatives: Event emitters: class EventBus {private handlers = new Map(); on(event, handler) {this.handlers.set(event, [...this.handlers.get(event) || [], handler]);} emit(event, data) {this.handlers.get(event)?.forEach(h => h(data));}}. Real-world: DOM events, RxJS observables, Node.js EventEmitter, Redux store subscriptions.
Externalize behavior to configuration - change behavior via config without code modification. Pattern: Load rules/strategies from config.json instead of hardcoding. Example: Validation rules in config: {validationRules: [{field: 'email', type: 'email', required: true}, {field: 'age', type: 'number', min: 18}]} Code: class Validator {constructor(private config: ValidationConfig) {} validate(data: any) {return this.config.validationRules.every(rule => this.applyRule(data, rule));} private applyRule(data: any, rule: Rule) {/* generic rule application */}} - add validation rules in config without modifying Validator. Benefits: Non-developers change behavior (product managers), A/B testing via config changes, feature flags for gradual rollouts. Real-world: Feature flags (LaunchDarkly, Unleash), workflow engines (config defines state machines), rule engines (business rules in JSON/YAML), plugin manifests (VSCode extensions.json). Structure: Generic engine executes config-defined behavior. Code knows how to interpret config, not specific behaviors. Modern 2025: Remote config (Firebase Remote Config), edge config (Vercel Edge Config) enable runtime updates without deployment. Limit: Complex logic still needs code - config for variations of existing behaviors, not fundamentally new features.
Middleware extends request/response processing without modifying core framework - open for extension via middleware chain, closed for modification. Express: app.use((req, res, next) => {/* middleware logic /; next();}). Chain: app.use(cors()); app.use(helmet()); app.use(bodyParser.json()); app.use(auth); app.use(logging); app.get('/api', handler) - each middleware extends behavior, framework unchanged. NestJS: @Injectable() class LoggingMiddleware implements NestMiddleware {use(req, res, next) {console.log('Request...'); next();}} apply in module: consumer.apply(LoggingMiddleware).forRoutes(''). Benefits: Compose cross-cutting concerns (auth, logging, caching, compression), order matters (auth before business logic), reusable across routes, independently testable. Violation: function handler(req, res) {if(!authorized) return res.status(401); log(req); const parsed = JSON.parse(req.body); /* business logic */} - concerns mixed, adding middleware requires modifying handler. Modern: Fastify decorators, Koa compose, serverless middleware (middy for AWS Lambda). Pattern: Chain of Responsibility - each middleware decides to process and/or pass to next. TypeScript: Typed middleware for type-safe req/res extensions.
Factory centralizes object creation - add new types by registering in factory, clients unchanged. Pattern: interface Product {use(): void;} class ProductFactory {private registry = new Map<string, () => Product>(); register(type: string, creator: () => Product) {this.registry.set(type, creator);} create(type: string): Product {const creator = this.registry.get(type); if(!creator) throw new Error('Unknown type'); return creator();}} Usage: const factory = new ProductFactory(); factory.register('A', () => new ConcreteProductA()); factory.register('B', () => new ConcreteProductB()); const product = factory.create('A') - add type 'C' by registering, no client modification. Benefits: Single place for creation logic, clients depend on interface not concrete classes, type registration at runtime. Violation: function createProduct(type: string) {if(type === 'A') return new ConcreteProductA(); if(type === 'B') return new ConcreteProductB();} - adding type modifies function. Abstract Factory: Factory of factories for product families. Real-world: React.createElement, Node.js require() module resolution, Dependency injection containers. Modern: Use DI container instead of manual factory for complex scenarios. TypeScript: Generic factory: class Factory
Custom hooks compose to extend functionality without modifying base hooks - open for extension via composition, closed for modification. Base hook: const useData = () => {const [data, setData] = useState([]); useEffect(() => {fetch('/api/data').then(r => r.json()).then(setData);}, []); return data;} - encapsulates data fetching. Extend via composition: const useFilteredData = (filter: (item: any) => boolean) => {const data = useData(); return useMemo(() => data.filter(filter), [data, filter]);} - extends useData without modification. Further extend: const usePaginatedData = (page: number, size: number) => {const filtered = useFilteredData(item => true); return useMemo(() => filtered.slice(page * size, (page + 1) * size), [filtered, page, size]);}. Benefits: Each hook single responsibility, compose hooks like functions, independently testable, reusable across components. Violation: const useData = (shouldFilter, filterFn, shouldPaginate, page, size) => {/* complex logic with many conditionals */} - monolithic hook. Modern 2025: Custom hooks for business logic (useAuth, useCart), third-party (react-query, swr) build composable data fetching. Pattern: Hooks are functional composition achieving OCP without classes. TypeScript: Generic hooks: const useAsync
Chain of Responsibility passes requests along chain of handlers - add new handlers without modifying existing ones or client. Pattern: interface Handler {setNext(handler: Handler): Handler; handle(request: any): any;} abstract class BaseHandler implements Handler {private next: Handler | null = null; setNext(handler: Handler): Handler {this.next = handler; return handler;} handle(request: any): any {if(this.next) return this.next.handle(request); return null;}} Concrete: class AuthHandler extends BaseHandler {handle(request: any) {if(!request.token) return {error: 'Unauthorized'}; return super.handle(request);}} class LogHandler extends BaseHandler {handle(request: any) {console.log('Request:', request); return super.handle(request);}} Usage: const auth = new AuthHandler(); const log = new LogHandler(); const validate = new ValidateHandler(); auth.setNext(log).setNext(validate); auth.handle(request) - add CacheHandler without modifying existing handlers. Benefits: Decouple sender from receivers, dynamic chain configuration, each handler independently testable. Real-world: Express/Koa middleware, Redux middleware, logging frameworks. Modern functional: const chain = [authMiddleware, logMiddleware, validateMiddleware].reduce((next, mw) => (req) => mw(req, next), finalHandler).
Event-driven decouples producers from consumers - add new consumers without modifying producers. Pattern: class EventBus {private handlers = new Map<string, Array<(data: any) => void>>(); on(event: string, handler: (data: any) => void) {const existing = this.handlers.get(event) || []; this.handlers.set(event, [...existing, handler]);} emit(event: string, data: any) {const handlers = this.handlers.get(event) || []; handlers.forEach(h => h(data));}} Usage: const bus = new EventBus(); bus.on('order.placed', (order) => sendEmail(order)); bus.on('order.placed', (order) => updateInventory(order)); bus.emit('order.placed', orderData) - add new listener without modifying emit logic. Benefits: Producers don't know consumers (low coupling), add handlers at runtime, multiple handlers per event, asynchronous processing. Violation: class OrderService {placeOrder(order) {saveOrder(order); emailService.send(order); inventoryService.update(order); analyticsService.track(order);}} - adding consumer modifies service. Real-world: Node.js EventEmitter, RxJS Subjects, Domain events in DDD, Message queues (RabbitMQ, Kafka), Webhooks. Modern 2025: Event sourcing (store events not state), CQRS (command/query separation via events). TypeScript: Typed events: interface Events {'order.placed': Order; 'user.registered': User;} type EventHandler
Visitor separates algorithms from object structure - add new operations without modifying element classes. Pattern: interface Element {accept(visitor: Visitor): void;} interface Visitor {visitConcreteElementA(el: ConcreteElementA): void; visitConcreteElementB(el: ConcreteElementB): void;} class ConcreteElementA implements Element {accept(visitor: Visitor) {visitor.visitConcreteElementA(this);}} class ExportVisitor implements Visitor {visitConcreteElementA(el) {/* export A as JSON /} visitConcreteElementB(el) {/ export B as JSON /}} class ValidateVisitor implements Visitor {visitConcreteElementA(el) {/ validate A /} visitConcreteElementB(el) {/ validate B */}} Usage: const element = new ConcreteElementA(); element.accept(new ExportVisitor()) - add PrintVisitor without modifying elements. Benefits: Add operations without modifying elements, operations grouped in visitor (related logic together), follows Single Responsibility. Trade-off: Adding new element types requires updating all visitors. Use when: Stable element structure, frequently adding new operations. Real-world: AST traversal (Babel, ESLint plugins), document processing (export to PDF/HTML/XML), reporting (different report formats). Modern: TypeScript discriminated unions sometimes better: type Shape = Circle | Square; function area(s: Shape) {switch(s.type) {case 'circle': return Math.PI * s.radius ** 2; case 'square': return s.side ** 2;}}.
API versioning allows introducing new API versions without modifying existing versions - new versions extend functionality, old versions unchanged for backward compatibility. Strategies: (1) URL versioning: /api/v1/users vs /api/v2/users - different routes, separate controllers. app.use('/api/v1', v1Router); app.use('/api/v2', v2Router). (2) Header versioning: Accept: application/vnd.api+json;version=1 - same route, route handler checks version header. (3) Query parameter: /api/users?version=2 - least preferred. Implementation preserving OCP: interface UserService {getUser(id: string): User;} class UserServiceV1 implements UserService {getUser(id) {return {id, name};}} class UserServiceV2 implements UserService {getUser(id) {return {id, name, email, createdAt};}} - v2 extends response without modifying v1. Benefits: Existing clients unaffected (closed for modification), new features in new versions (open for extension), gradual migration possible. Violation: Single endpoint with if(version === '1') ... else if(version === '2') scattered across codebase. Modern 2025: GraphQL avoids versioning via schema evolution (add fields, deprecate old ones), tRPC uses TypeScript types for contracts. Best practice: Semantic versioning for breaking changes, sunset policy for old versions, automated compatibility tests.
GraphQL evolves schema by adding fields and deprecating old ones without breaking existing queries - schema open for extension via new fields, existing queries unchanged (closed for modification). Evolution pattern: Old schema: type User {id: ID!; name: String!;} Add fields (non-breaking): type User {id: ID!; name: String!; email: String; profile: Profile;} - existing queries requesting id and name work unchanged. Deprecate fields: type User {id: ID!; name: String!; username: String @deprecated(reason: 'Use name instead');} - old queries work, clients warned to migrate. Benefits: Versionless API (no /v1, /v2), additive changes (new fields, new types, new arguments with defaults), clients request only needed fields, introspection shows deprecations. Violation: Removing fields breaks clients: type User {id: ID!; /* name removed */} - queries requesting name fail. Best practices: Never remove fields (deprecate instead), make new fields nullable or provide defaults, use @deprecated directive with migration instructions, monitor field usage before removal. Tools: GraphQL Code Generator (typed clients auto-update), Apollo Studio (track field usage). Modern 2025: Federation v2 (extend schema across subgraphs), schema composition (stitch schemas). Real-world: GitHub API (evolved over 7+ years without versions), Shopify API (deprecated fields remain for years).
Expand-contract pattern evolves database schema without breaking running applications - expand schema (add new), migrate data, contract (remove old) in separate deployments. Pattern for renaming column: (1) Expand: ALTER TABLE users ADD COLUMN email_address VARCHAR(255); Dual writes: UPDATE users SET email = ?, email_address = ? WHERE id = ?. Deploy application reading from both columns. (2) Migrate: UPDATE users SET email_address = email WHERE email_address IS NULL. Verify data consistency. (3) Contract: ALTER TABLE users DROP COLUMN email. Deploy application using only new column. Benefits: Zero-downtime migrations, old application version works during deployment, rollback possible at each stage, follows OCP (new schema extends old, old code unchanged until migration complete). Violation: Direct breaking change: ALTER TABLE users DROP COLUMN email immediately - running app crashes. Tools: Flyway, Liquibase (version migrations), online schema change (pt-online-schema-change for MySQL, pg_repack for PostgreSQL). Modern 2025: Blue-green deployments (two databases, switch after migration), shadow traffic (test new schema with copy of production traffic). Best practice: Backward-compatible migrations only, feature flags for schema-dependent code, automated migration testing.