Suggest an editImprove this articleRefine the answer for “SOLID principles”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**SOLID** - five object-oriented design principles that make code easier to change without breaking unrelated parts. - **S**RP: one reason to change per class - **O**CP: extend behavior without editing existing code - **L**SP: subtypes must substitute the base type without changing expected behavior - **I**SP: clients depend only on what they actually use - **D**IP: depend on abstractions, not concrete classes **Key:** each principle targets a specific way code breaks under change.Shown above the full answer for quick recall.Answer (EN)Image**SOLID** - five object-oriented design principles (Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) that make code easier to change, test, and extend without breaking unrelated parts. ## Theory ### TL;DR - Five principles: SRP, OCP, LSP, ISP, DIP - each targets a different way code breaks under change - Without them, editing one class tends to ripple into five others - Kitchen analogy: each chef (class) handles one station (responsibility), tools are interchangeable (DIP), any cook can substitute another without chaos (LSP) - Apply when a class has more than one reason to change, or adding a feature keeps breaking existing tests - Formulated by Robert C. Martin ("Uncle Bob") ### Quick example ```javascript // Bad: UserService has two reasons to change (DB schema AND email template) class UserService { save(user) { /* DB save */ } email(user) { /* send email */ } } // Good: split by responsibility (SRP) + inject dependencies (DIP) class UserRepository { save(user) { /* DB */ } } class EmailService { send(user) { /* email */ } } class UserController { constructor(repo, mailer) { this.repo = repo; this.mailer = mailer; } register(user) { this.repo.save(user); this.mailer.send(user); } } ``` Change the email template? Touch only `EmailService`. Switch databases? Swap `UserRepository`. Neither change touches the other class. ### Single Responsibility Principle (SRP) A class should have one reason to change. Not one method, not one file - one *reason*. If both a DB schema change and an email template change could force you to open the same file, that file has two responsibilities. The typical trap here is over-splitting. `GetUser` and `SaveUser` as separate classes is not SRP, that is fragmentation. `UserRepository` with `get()` and `save()` is fine - both methods change for the same reason: database logic. ```javascript // Bad: Circle renders itself - two reasons to change class Circle { constructor(radius) { this.radius = radius; } area() { return Math.PI * this.radius ** 2; } print() { console.log(`Area: ${this.area()}`); } // rendering lives here } // Good: rendering is separate class Circle { constructor(radius) { this.radius = radius; } area() { return Math.PI * this.radius ** 2; } } const print = (shape) => console.log(`Area: ${shape.area()}`); // Output: "Area: 78.54" - same result, isolated responsibility ``` ### Open-Closed Principle (OCP) Classes should be open for extension, closed for modification. The goal is adding behavior without editing existing code. Strategy pattern is the cleanest way to achieve this in JavaScript. Instead of a growing chain of `if/else`, each variant becomes its own class. ```javascript // Bad: add a new user type? Edit this function. function getDiscount(user) { if (user.type === 'regular') return 5; if (user.type === 'vip') return 10; // every new type lands here } // Good: add a new type by adding a new class class RegularUser { getDiscount() { return 5; } } class VipUser { getDiscount() { return 10; } } class ProUser { getDiscount() { return 15; } } // new type, zero edits elsewhere function showDiscount(user) { return user.getDiscount(); // never changes } ``` From practice: OCP via inheritance alone tends to create hidden LSP violations down the line. Subclasses that override parent behavior can break callers in unexpected ways. Strategy or composition is usually the safer path. ### Liskov Substitution Principle (LSP) If code expects a `Rectangle`, it should work correctly when given a `Square` instead - without knowing the difference. That is the complete idea. The classic counterexample is exactly this. A `Square` that extends `Rectangle` overrides `setWidth` to also set height. This breaks any code that sets width and height independently then checks the area. ```javascript class Rectangle { constructor(w, h) { this.w = w; this.h = h; } setWidth(w) { this.w = w; } setHeight(h) { this.h = h; } area() { return this.w * this.h; } } class Square extends Rectangle { setWidth(w) { this.w = this.h = w; } // LSP violation setHeight(h) { this.w = this.h = h; } } // Client code that assumes Rectangle behavior: const shape = new Square(5, 5); shape.setWidth(5); shape.setHeight(4); console.log(shape.area()); // Expected 20, got 25 - silent bug ``` Fix: don't make `Square` extend `Rectangle`. Use a shared `Shape` base with only `area()`, or use composition. Another common LSP violation: a `Bird` base class with `fly()`, then `Ostrich` that throws from `fly()`. The moment you do `birds.forEach(b => b.fly())`, ostriches break it. ### Interface Segregation Principle (ISP) Clients should not depend on interfaces they don't use. In JavaScript this means: don't force a class to carry methods it has no use for. The fat-interface problem shows up in Express middleware and React prop types. A component that receives a huge `UIProps` object but reads two fields is technically coupled to everything in that object. ```javascript // Bad: Animal "interface" - Dog can't fly, Bird doesn't need swim class Animal { eat() {} walk() {} swim() {} fly() {} } // Good: split by capability class Eatable { eat() {} } class Walkable { walk() {} } class Flyable { fly() {} } class Dog extends Walkable { /* inherits Eatable if needed */ } class Bird extends Flyable { /* no swim(), no walk() */ } ``` In React terms: `ButtonProps` with `onClick` and `label` beats a giant `UIElementProps` interface that includes modal state, form values, and animation config. ### Dependency Inversion Principle (DIP) High-level modules should not depend on low-level modules. Both should depend on abstractions. In practice: don't instantiate dependencies inside a class. Pass them in. JavaScript's structural typing makes this simple - no formal interface declaration needed. Any object with the right methods works. ```javascript // Bad: UserService is locked to MySQL class UserService { constructor() { this.db = new MySQLDatabase(); // hard-coded } getUsers() { return this.db.query('SELECT * FROM users'); } } // Good: inject whatever matches the contract class UserService { constructor(database) { this.database = database; } getUsers() { return this.database.query('SELECT * FROM users'); } } // Production const service = new UserService(new MySQLDatabase()); // Swap to Mongo: const mongoService = new UserService(new MongoDatabase()); // In tests - plain mock object works: const testService = new UserService({ query: () => [{ id: 1, name: 'Alice' }] }); ``` This is exactly how NestJS controllers work. They receive repos via constructor injection and never instantiate concrete classes themselves. Switching from Postgres to DynamoDB becomes a one-line config change. ### Decision rules - Class has 2+ reasons to change: SRP, split into focused classes - Adding a feature requires editing existing logic: OCP, extract to strategy - A subclass changes expected parent behavior: LSP, restructure the hierarchy - A class implements methods it never calls: ISP, split the interface - A class creates its own dependencies with `new`: DIP, inject them instead ### Common mistakes **Mistake 1: SRP means one method per class** Wrong. `HttpClient` has `get()`, `post()`, `put()`, `delete()` - multiple methods, one responsibility (HTTP communication). Over-splitting creates brittle micro-classes with no cohesion. ```javascript // Brittle - each operation is a separate class class GetUser { get(id) {} } class SaveUser { save(u) {} } // Correct - cohesion by responsibility class UserRepository { get(id) {} save(u) {} } ``` **Mistake 2: OCP through inheritance only** Subclasses that override parent logic often create hidden LSP violations. Strategy pattern or composition handles OCP without that risk. **Mistake 3: DIP means create interfaces for everything** In TypeScript/JavaScript, structural typing means any object with matching methods satisfies the contract. A plain `{ query: fn }` passed as `database` is valid DIP. Over-engineering every dependency behind a formal interface slows development without real benefit. **Mistake 4: LSP "works" so it's fine** The Square/Rectangle example passes JavaScript type checks. The bug surfaces at runtime. This is why LSP violations are the hardest to catch without tests. If a subclass overrides a method in a way that changes the expected output, it is a violation regardless of whether it currently crashes. **Mistake 5: ISP is about method count, not coupling** ISP is violated when a client *depends on* an object with methods it doesn't use - not simply when a class has many methods. A `UserService` with 15 methods is fine if its callers use all 15. ### Real-world usage - React: component props through small interfaces (ISP) - `ButtonProps` not `UIProps`; context providers inject shared state (DIP) - Express/NestJS: controllers receive repos via constructor injection (DIP); each middleware handles one concern (SRP) - Redux: one action creator per domain event (SRP); reducers extend behavior without modifying core logic (OCP) - Node.js `fs` module: pass streams as arguments instead of hardcoding files (DIP pattern) - Decision vs alternatives: SOLID for codebases with 10+ classes; YAGNI/GRASP for early prototypes where abstractions add noise ### Follow-up questions **Q:** Give a non-trivial SRP example from e-commerce. **A:** `OrderService` places orders. It should not send confirmation emails. When the email provider changes, you want to touch exactly one file: `NotificationService`. If both live in `OrderService`, any email provider migration requires retesting the entire order placement flow. **Q:** How does OCP work without inheritance? **A:** Strategy pattern. `payment.process(new StripeStrategy())` adds a new payment provider by adding a new class. The `Payment` class itself never changes. This is safer than subclassing because there is no shared state to accidentally break. **Q:** What is the correct fix for the Square/Rectangle LSP violation? **A:** Don't inherit `Square` from `Rectangle`. Use a shared `Shape` base with only `area()`, or use composition. The root problem is that a square and a rectangle have different invariants: a square requires `width === height`, a rectangle does not. Inheritance cannot reconcile that. **Q:** How do you apply ISP in a REST API? **A:** Split `UserPublicApi` (read-only endpoints) from `UserAdminApi` (mutations, deletions). Consumer clients implement only the read interface; admin panels implement both. No client carries dead-weight methods. **Q:** How do you apply DIP in vanilla JS without a DI container? **A:** Factory functions. `const service = new UserService(createRepo('postgres'))` where `createRepo` returns an object with the expected methods. Swap `'postgres'` for `'mongo'` and everything downstream adjusts. **Q (senior):** I have a `Bird` class with `fly()` and an `Ostrich` subclass that throws from `fly()`. Tests pass because nobody calls `fly()` on `Ostrich` yet. Is this an LSP violation? **A:** Yes. LSP is about substitutability, not current usage. The moment any code path calls `fly()` on a `Bird` reference, an `Ostrich` breaks it. Fix: remove `fly()` from the base `Bird` class and create a `FlyingBird` subclass. `Ostrich` extends `Bird` directly. ## Examples ### Basic: SRP in a user registration flow ```javascript // One class, three concerns class UserService { register(user) { if (!user.email) throw new Error('No email'); // validation db.insert('users', user); // persistence mailer.send(user.email, 'Welcome!'); // notification } } // Three concerns, three classes class UserValidator { validate(user) { if (!user.email) throw new Error('No email'); } } class UserRepository { save(user) { db.insert('users', user); } } class WelcomeMailer { send(user) { mailer.send(user.email, 'Welcome!'); } } class UserService { constructor(validator, repo, mailer) { this.validator = validator; this.repo = repo; this.mailer = mailer; } register(user) { this.validator.validate(user); this.repo.save(user); this.mailer.send(user); } } ``` Change the email template: touch only `WelcomeMailer`. Switch to PostgreSQL: touch only `UserRepository`. Add a new validation rule: touch only `UserValidator`. No other class needs to know. ### Intermediate: OCP with a payment system ```javascript // Bad: every new payment method requires editing processPayment function processPayment(order, method) { if (method === 'stripe') { /* Stripe logic */ } if (method === 'paypal') { /* PayPal logic */ } if (method === 'crypto') { /* Crypto - new requirement, editing here */ } } // Good: each strategy is isolated class StripePayment { process(order) { console.log(`Stripe: $${order.total}`); } } class PayPalPayment { process(order) { console.log(`PayPal: $${order.total}`); } } class CryptoPayment { // New requirement - new file, zero edits to existing code process(order) { console.log(`Crypto: ${order.total} BTC`); } } function processPayment(order, strategy) { strategy.process(order); } processPayment({ total: 100 }, new StripePayment()); // Stripe: $100 processPayment({ total: 100 }, new CryptoPayment()); // Crypto: 100 BTC ``` Adding a new payment method means adding a new file. `processPayment` never changes. This is OCP via the strategy pattern. ### Advanced: LSP violation that passes type checks ```javascript class Rectangle { constructor(w, h) { this.w = w; this.h = h; } setWidth(w) { this.w = w; } setHeight(h) { this.h = h; } area() { return this.w * this.h; } } class Square extends Rectangle { setWidth(w) { this.w = this.h = w; } // "smart" override - LSP violation setHeight(h) { this.w = this.h = h; } } function assertRectangleArea(rect) { rect.setWidth(5); rect.setHeight(4); console.log(rect.area()); // Caller expects 20 } assertRectangleArea(new Rectangle(1, 1)); // 20 - correct assertRectangleArea(new Square(1, 1)); // 25 - silent bug, no type error // Fix: no shared mutable hierarchy class Shape { area() {} } class Rectangle extends Shape { constructor(w, h) { super(); this.w = w; this.h = h; } area() { return this.w * this.h; } } class Square extends Shape { constructor(side) { super(); this.side = side; } area() { return this.side ** 2; } } // Both have area(), no shared mutable state, substitution works ``` No type checker catches the original bug - only a test does. This is why LSP violations are the most expensive to find in production.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.