0
0
LLDsystem_design~15 mins

Liskov Substitution Principle in LLD - Deep Dive

Choose your learning style9 modes available
Overview - Liskov Substitution Principle
What is it?
The Liskov Substitution Principle (LSP) is a rule in software design that says objects of a parent class should be replaceable with objects of a child class without changing how the program works. It means that subclasses must behave in ways that do not surprise or break the expectations set by their parent classes. This helps keep code easy to understand, maintain, and extend.
Why it matters
Without LSP, replacing a parent class object with a child class object might cause bugs or unexpected behavior. This can make software fragile and hard to fix or improve. LSP ensures that new code fits smoothly into existing systems, making software more reliable and easier to grow over time.
Where it fits
Before learning LSP, you should understand basic object-oriented programming concepts like classes, inheritance, and polymorphism. After LSP, you can explore other design principles like the Open/Closed Principle and Dependency Inversion Principle to write even better software.
Mental Model
Core Idea
A child class should be able to stand in for its parent class without breaking the program's behavior.
Think of it like...
Imagine you have a universal remote control that works with all your devices. If you buy a new device, it should work with the same remote without needing a new one or changing how you use it.
Parent Class (Shape)
  │
  ├── Child Class (Rectangle)
  │     └─ Behaves like Shape
  └── Child Class (Square)
        └─ Behaves like Shape

If Square replaces Rectangle, the program still works as expected.
Build-Up - 7 Steps
1
FoundationUnderstanding Classes and Inheritance
🤔
Concept: Learn what classes and inheritance mean in programming.
Classes are blueprints for creating objects. Inheritance lets a new class (child) use properties and behaviors of an existing class (parent). For example, a 'Bird' class can be a parent, and 'Sparrow' can be a child that inherits from 'Bird'.
Result
You can create new classes that reuse code from existing ones, making programming easier.
Understanding inheritance is key because LSP is about how child classes relate to parent classes.
2
FoundationWhat is Polymorphism?
🤔
Concept: Polymorphism allows objects of different classes to be treated as objects of a common parent class.
If a function expects a 'Bird', you can pass it a 'Sparrow' or 'Eagle' because they inherit from 'Bird'. The function works with any child class without knowing the exact type.
Result
Code becomes flexible and reusable because it works with general types, not specific ones.
Polymorphism sets the stage for LSP by allowing child objects to replace parent objects.
3
IntermediateDefining the Liskov Substitution Principle
🤔Before reading on: do you think any child class can replace its parent without issues? Commit to yes or no.
Concept: LSP states that child classes must be substitutable for their parent classes without changing program correctness.
If a program uses a parent class, replacing it with a child class should not cause errors or unexpected results. The child class must honor the parent's promises, like method behavior and expected outcomes.
Result
Software remains correct and predictable even when using subclasses.
Knowing LSP prevents subtle bugs caused by child classes that behave differently than expected.
4
IntermediateCommon Violations of LSP
🤔Before reading on: can a child class change the behavior of a parent method in a way that breaks the program? Commit to yes or no.
Concept: Some child classes break LSP by changing or restricting behaviors inherited from parents.
For example, if a parent class method allows any input, but the child class rejects some inputs or throws errors, it breaks LSP. Another example is changing method effects so the program behaves incorrectly.
Result
Violations cause bugs and make code hard to maintain or extend.
Recognizing violations helps you design child classes that fit smoothly into existing code.
5
IntermediateDesigning Classes to Follow LSP
🤔
Concept: Use careful design to ensure child classes honor parent class contracts and expectations.
Define clear rules (contracts) for what methods do and what inputs they accept. Child classes should follow these rules without narrowing or changing them. Use interfaces or abstract classes to specify expected behavior.
Result
Child classes can replace parents safely, making code more robust.
Understanding contracts between classes is essential to applying LSP correctly.
6
AdvancedLSP in Real-World Systems
🤔Before reading on: do you think LSP violations can cause system crashes or just minor bugs? Commit to your answer.
Concept: In large systems, LSP violations can cause serious failures and maintenance headaches.
For example, in a payment system, if a subclass changes how transactions are processed unexpectedly, it can cause incorrect charges or data loss. Following LSP ensures components can be swapped or extended without breaking the system.
Result
Systems become more reliable, easier to test, and simpler to extend.
Knowing the impact of LSP in production helps prioritize good design practices.
7
ExpertSurprising LSP Challenges and Solutions
🤔Before reading on: do you think all inheritance hierarchies naturally follow LSP? Commit to yes or no.
Concept: Not all inheritance fits LSP; sometimes composition or interfaces are better choices.
For example, a 'Square' class inheriting from 'Rectangle' can break LSP because squares restrict width and height to be equal, changing behavior. Experts solve this by using composition (has-a) instead of inheritance (is-a) or by redesigning abstractions.
Result
Better designs avoid subtle bugs and make systems easier to evolve.
Understanding when inheritance breaks LSP leads to smarter architecture decisions.
Under the Hood
LSP works by enforcing behavioral contracts between parent and child classes. At runtime, when a child object replaces a parent object, the program expects the same method signatures, input ranges, output types, and side effects. If the child class changes these, it breaks assumptions, causing errors or unexpected behavior.
Why designed this way?
LSP was introduced by Barbara Liskov in 1987 to formalize safe inheritance. Before LSP, developers often misused inheritance, causing fragile code. LSP guides developers to design class hierarchies that preserve correctness and predictability, balancing reuse with safety.
┌───────────────┐
│ Parent Class  │
│  - method()   │
└──────┬────────┘
       │
       │ inherits
       ▼
┌───────────────┐
│ Child Class   │
│  - method()   │
│  (same or    │
│   compatible) │
└───────────────┘

Program calls method() on Parent reference
→ Child method() runs without breaking expectations
Myth Busters - 4 Common Misconceptions
Quick: Does overriding a parent method always follow LSP? Commit to yes or no.
Common Belief:Overriding any parent method in a child class is always safe and follows LSP.
Tap to reveal reality
Reality:Overriding can break LSP if the new method changes expected behavior, input requirements, or output guarantees.
Why it matters:Ignoring this can cause subtle bugs that are hard to detect and fix.
Quick: Can a child class restrict input parameters more than the parent? Commit to yes or no.
Common Belief:Child classes can safely restrict inputs to methods compared to the parent class.
Tap to reveal reality
Reality:Restricting inputs breaks LSP because code expecting the parent class might pass inputs the child rejects.
Why it matters:This leads to runtime errors and breaks polymorphism.
Quick: Is inheritance always the best way to reuse code? Commit to yes or no.
Common Belief:Inheritance is always the best way to share code and behavior.
Tap to reveal reality
Reality:Sometimes inheritance breaks LSP and composition or interfaces are better choices.
Why it matters:Misusing inheritance leads to fragile designs and maintenance problems.
Quick: Does LSP only matter for large systems? Commit to yes or no.
Common Belief:LSP is only important in big, complex software projects.
Tap to reveal reality
Reality:LSP matters even in small projects because it prevents bugs and eases future changes.
Why it matters:Ignoring LSP early can cause technical debt that grows over time.
Expert Zone
1
LSP is about behavior, not just method signatures; subtle changes in side effects can break it.
2
Design by contract is a powerful way to enforce LSP by specifying preconditions and postconditions.
3
Sometimes, violating LSP intentionally is acceptable for performance or legacy reasons, but it must be documented and isolated.
When NOT to use
Avoid strict inheritance hierarchies when child classes need to change core behavior; prefer composition or interfaces. Also, avoid LSP when rapid prototyping or throwaway code where design rigor is less critical.
Production Patterns
Use interfaces or abstract base classes to define contracts. Apply unit tests that verify child classes behave like parents. Use composition over inheritance to avoid LSP violations. Refactor inheritance hierarchies when violations are detected.
Connections
Design by Contract
LSP builds on design by contract principles by enforcing behavioral contracts in inheritance.
Understanding design by contract helps enforce LSP by clearly defining what child classes must guarantee.
Composition over Inheritance
Composition is an alternative to inheritance that helps avoid LSP violations.
Knowing when to use composition instead of inheritance leads to more flexible and maintainable designs.
Legal Contracts
LSP is like legal contracts where all parties must fulfill agreed terms to avoid disputes.
Seeing LSP as a contract clarifies why child classes must honor parent class promises to keep software reliable.
Common Pitfalls
#1Child class changes method behavior to reject inputs parent accepted.
Wrong approach:class Rectangle { setWidth(w) { this.width = w; } setHeight(h) { this.height = h; } } class Square extends Rectangle { setWidth(w) { if (w !== this.height) throw new Error('Width must equal height'); this.width = w; } setHeight(h) { if (h !== this.width) throw new Error('Height must equal width'); this.height = h; } }
Correct approach:class Rectangle { setWidth(w) { this.width = w; } setHeight(h) { this.height = h; } } class Square { constructor(size) { this.size = size; } setSize(s) { this.size = s; } }
Root cause:Misunderstanding that Square is not a subtype of Rectangle because it restricts behavior, breaking LSP.
#2Overriding parent method with incompatible return type.
Wrong approach:class Bird { fly() { return true; } } class Penguin extends Bird { fly() { return 'cannot fly'; } }
Correct approach:class Bird { fly() { return true; } } class Penguin extends Bird { fly() { throw new Error('Penguins cannot fly'); } }
Root cause:Returning different types or changing method contracts breaks expectations and LSP.
#3Using inheritance to share code when behavior differs significantly.
Wrong approach:class Vehicle { startEngine() { /* start engine */ } } class Bicycle extends Vehicle { startEngine() { throw new Error('No engine'); } }
Correct approach:class Vehicle { startEngine() { /* start engine */ } } class Bicycle { pedal() { /* pedal bike */ } }
Root cause:Forcing inheritance when behaviors differ causes LSP violations and fragile code.
Key Takeaways
Liskov Substitution Principle ensures child classes can replace parent classes without breaking program behavior.
Following LSP prevents bugs and makes software easier to maintain and extend.
Violating LSP often happens when child classes change method behavior or restrict inputs unexpectedly.
Designing clear contracts and using composition can help maintain LSP in complex systems.
Understanding LSP helps write reliable, flexible, and scalable object-oriented software.