Suggest an editImprove this articleRefine the answer for “What is Open-Closed Principle (OCP)?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Open-Closed Principle (OCP)** - software entities should be open for extension but closed for modification. ```javascript class Shape { area() { throw new Error('Implement'); } } class Circle extends Shape { area() { return Math.PI * this.r ** 2; } } // Add Rectangle? New class. Zero changes to existing code. ``` **Key point:** extend via inheritance or interfaces, never by editing tested code.Shown above the full answer for quick recall.Answer (EN)Image**Open-Closed Principle (OCP)** - a SOLID rule that says software entities should be open for extension but closed for modification. ## Theory ### TL;DR - OCP is like a USB port: plug in new devices without rewiring the computer - Core idea: add new behavior by writing new code, not by editing old code - Abstract base class or interface = the stable contract; new types implement that contract - Decision rule: if adding a feature requires editing an existing file, the design breaks OCP - Tradeoff: polymorphic dispatch adds ~2-10ns per call, which matters in hot loops ### Quick example ```javascript // BAD: every new shape requires editing this file class AreaCalculator { calculate(shape) { if (shape.type === 'circle') return Math.PI * shape.r ** 2; if (shape.type === 'square') return shape.side ** 2; // New rectangle? Edit here. Triangle? Edit here again. } } // GOOD: new shapes plug in without touching existing code class Shape { area() { throw new Error('Implement area()'); } } class Circle extends Shape { constructor(r) { super(); this.r = r; } area() { return Math.PI * this.r ** 2; } } class Rectangle extends Shape { constructor(w, h) { super(); this.w = w; this.h = h; } area() { return this.w * this.h; } } // new Rectangle(2, 3).area() → 6 // Shape and Circle: untouched. ``` Each new shape extends `Shape`. The calculator calls `.area()` on whatever it receives, no `if/else` needed. ### Abstraction vs implementation OCP separates two things: the **stable contract** (the abstract interface) and the **extensible details** (concrete implementations). Existing code stays untouched because new types plug into the abstract contract via inheritance. That prevents regression bugs from touching already-tested code. One way to think about it: the `AreaCalculator` should never need to know what shapes exist. It only knows "this thing has an `area()` method." That contract is closed. New shapes are open. ### When to use OCP Use OCP when you can predict that behavior will grow in one direction: - **Multiple output formats** (loggers, printers, serializers): base class with `log()` or `serialize()`, extend for each format - **Plugin systems**: define a protocol or interface, third parties implement it without touching core - **Payment processors**: Strategy pattern with a shared interface, add Stripe or PayPal as a new class - **Middleware stacks**: Express `app.use()` is OCP in practice Skip OCP for single-use code or small utilities where the abstraction overhead exceeds the maintenance cost. YAGNI applies here. ### How V8 handles polymorphic calls JavaScript engines use **virtual method tables (vtables)** for polymorphic dispatch. When `shape.area()` runs, V8 looks up the concrete class's vtable at runtime and dispatches to the right implementation. No static type knowledge required, which is exactly what makes OCP possible in JavaScript. The cost is 1-2 indirect jumps per call, roughly 2-10ns. For 10,000 shapes per render cycle, that adds up. In performance-critical paths, batch operations or use concrete types directly. ### Common mistakes **Mistake 1: using inheritance for state instead of behavior** ```javascript // Wrong: Square IS-A Rectangle leads to broken setters class Rectangle { constructor(w, h) { this.w = w; this.h = h; } setWidth(w) { this.w = w; } } class Square extends Rectangle { setWidth(w) { this.w = this.h = w; } // modifies base behavior } ``` `Square` overriding `setWidth` breaks existing `Rectangle` users. This also violates Liskov Substitution, which kills OCP extensions down the line. Fix: use composition. `class Square { constructor(s) { this.rect = new Rectangle(s, s); } }`. **Mistake 2: abstract class with concrete default methods** ```javascript // Wrong: default logic becomes a modification magnet class Shape { area() { return 0; } } // seems harmless ``` When the default logic changes, every subclass is at risk. Use pure abstract methods: throw an error, or in TypeScript declare the method without a body. **Mistake 3: premature abstraction** ```javascript // Wrong: interface for every tiny thing // 50 implementations for what could be 3 functions interface ILogger { log(): void; } ``` Start concrete. Abstract only when you actually need to extend. Most codebases have more abstraction than real extension points. **Mistake 4: polymorphism in tight loops** ```javascript // Risky in hot paths shapes.forEach(shape => total += shape.area()); // vtable lookup on each call ``` V8 can devirtualize this sometimes, but not always. If profiling shows this path as a bottleneck, use typed arrays or concrete types there. ### Real-world usage - **React**: HOCs like `withRouter` in `react-router` extend components without touching source - **Express**: middleware via `app.use()` adds auth or logging without changing router logic - **Redux**: `combineReducers` adds features without touching existing reducer files - **NestJS**: `@Injectable` decorators enable module extension without base class changes - **Stripe Node SDK**: PaymentMethod classes extend a base via plugins, no core edits required ### Follow-up questions **Q:** How does OCP differ from the Stable Abstractions Principle? **A:** OCP says "don't modify." The Stable Abstractions Principle (SAP) quantifies how abstract a package should be relative to how much other packages depend on it. Robert C. Martin describes SAP as a metric for OCP health via fan-out/in ratios. Junior answer: "they're the same." Senior answer: "SAP gives you a number to measure whether your OCP is actually working." **Q:** Show an OCP violation and fix it in TypeScript using interfaces. **A:** Violation: a function with a `switch` on string literal types. Fix: `interface Shape { area(): number; }`, one class per shape type. The function accepting `Shape` never changes. **Q:** What are the tradeoffs of OCP in microservices vs a monolith? **A:** In a monolith, inheritance and shared base classes are straightforward. In microservices, shared base classes become a coupling problem across service boundaries. There you prefer event contracts or OpenAPI schemas as the "closed" boundary. Extensions happen in separate services, not subclasses. **Q:** In a legacy codebase with 100 `if/else` branches, what is your OCP migration plan? **A:** Strangler pattern. Extract the interface first, wrap legacy code in an adapter, migrate one branch at a time. Tests before every step. No full rewrites at once. ## Examples ### Basic: shape area calculator ```javascript class Shape { area() { throw new Error('Subclass must implement area()'); } } class Circle extends Shape { constructor(radius) { super(); this.radius = radius; } area() { return Math.PI * this.radius ** 2; } } class Rectangle extends Shape { constructor(width, height) { super(); this.width = width; this.height = height; } area() { return this.width * this.height; } } class Triangle extends Shape { constructor(base, height) { super(); this.base = base; this.height = height; } area() { return (this.base * this.height) / 2; } } const shapes = [new Circle(5), new Rectangle(4, 6), new Triangle(3, 8)]; shapes.forEach(s => console.log(s.area())); // 78.54, 24, 12 ``` Adding `Triangle` required zero changes to `Shape`, `Circle`, or `Rectangle`. New class, done. The existing code was never touched. ### Intermediate: Express request logger ```javascript // Base logger - closed for modification class Logger { log(request) { console.log(`${request.method} ${request.url}`); } } // Extend for JSON format - no changes to Logger class JSONLogger extends Logger { log(request) { console.log(JSON.stringify({ method: request.method, url: request.url, timestamp: new Date().toISOString() })); } } // Extend for Sentry - still no changes to Logger class SentryLogger extends Logger { log(request) { Sentry.captureMessage(`Access: ${request.method} ${request.url}`); } } const logger = new JSONLogger(); app.use((req, res, next) => { logger.log(req); next(); }); // Output: {"method":"GET","url":"/api/users","timestamp":"2025-01-15T10:00:00Z"} ``` Adding `DatadogLogger` tomorrow? One new class. `Logger`, `app.js`, and `JSONLogger` stay exactly as they are. ### Advanced: React HOC composition ```javascript // Closed HOC contract - provides featureEnabled prop function withFeature(WrappedComponent) { return class extends React.Component { render() { return <WrappedComponent {...this.props} featureEnabled={true} />; } }; } // Extension: adds analytics tracking - WrappedComponent is untouched function withAnalytics(WrappedComponent) { return class extends React.Component { componentDidMount() { analytics.track('ComponentMounted', this.props); } render() { return <WrappedComponent {...this.props} />; } }; } // Compose without modifying Button const TrackedButton = withAnalytics(withFeature(Button)); // Mounts Button, logs {page: 'home', featureEnabled: true} ``` HOC order matters. `withAnalytics` wraps `withFeature(Button)`, so analytics fires after `featureEnabled` is already in props. Reverse the order and `featureEnabled` disappears from the analytics payload. That is the senior-level gotcha with OCP and HOC composition.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.