Bird
Raised Fist0
LLDsystem_design~15 mins

Object-oriented design principles in LLD - Deep Dive

Choose your learning style10 modes available
Overview - Object-oriented design principles
What is it?
Object-oriented design principles are guidelines that help programmers create software that is easy to understand, maintain, and extend. They focus on organizing code using objects, which are like real-world things with properties and actions. These principles ensure that objects work well together and the system stays flexible as it grows. They help avoid messy code and bugs by promoting clear responsibilities and relationships.
Why it matters
Without these principles, software becomes hard to change or fix because parts are tightly connected and confusing. Imagine a messy toolbox where tools are mixed up and broken; it slows down work and causes mistakes. Object-oriented design principles keep code organized like a well-arranged toolbox, making it easier to add new features, fix problems, and work with others. This saves time, reduces errors, and improves software quality.
Where it fits
Before learning these principles, you should understand basic programming concepts like variables, functions, and classes. After mastering them, you can learn design patterns, software architecture, and advanced topics like SOLID principles and clean code practices. This topic is a foundation for writing professional, scalable software.
Mental Model
Core Idea
Good object-oriented design means creating clear, simple objects that each do one job well and work together smoothly.
Think of it like...
It's like building a team where each member has a clear role and communicates well, so the team works efficiently without confusion or overlap.
┌───────────────┐   uses   ┌───────────────┐
│   Object A    │────────▶│   Object B    │
└───────────────┘         └───────────────┘

Each object has:
- Clear responsibility
- Defined interface
- Minimal knowledge of others
Build-Up - 8 Steps
1
FoundationUnderstanding Objects and Classes
🤔
Concept: Learn what objects and classes are and how they represent real-world things in code.
An object is like a thing with properties (data) and actions (methods). A class is a blueprint to create many similar objects. For example, a 'Car' class can create many car objects, each with its own color and speed. This helps organize code by grouping related data and behavior.
Result
You can create objects that model real things, making code easier to understand and reuse.
Understanding objects and classes is the base for all object-oriented design; without this, design principles have no meaning.
2
FoundationEncapsulation: Hiding Details
🤔
Concept: Learn how to keep an object's internal data safe and only expose what is necessary.
Encapsulation means wrapping data and methods inside an object and hiding the inner details from the outside. For example, a TV object hides how it changes channels internally and only offers a 'changeChannel' method. This prevents outside code from messing with internal data directly.
Result
Objects protect their data, reducing bugs and making code easier to change without breaking others.
Encapsulation creates clear boundaries, so changes inside an object don't ripple everywhere, making maintenance safer.
3
IntermediateSingle Responsibility Principle
🤔Before reading on: do you think one object should handle many tasks or just one? Commit to your answer.
Concept: Each object or class should have only one reason to change, meaning it should do one job well.
If a class handles multiple tasks, changing one task might break others. For example, a 'User' class should manage user data, not handle database saving. Separating responsibilities keeps code focused and easier to fix or extend.
Result
Classes become simpler, more reliable, and easier to test because they do one thing.
Knowing that one responsibility per class reduces complexity and prevents tangled code.
4
IntermediateOpen/Closed Principle
🤔Before reading on: should you change existing code to add features or extend it without changes? Commit to your answer.
Concept: Software entities should be open for extension but closed for modification.
This means you can add new features by adding new code, not by changing existing code. For example, adding a new payment method should not require changing the payment processing class but adding a new class that fits the system. This avoids breaking working code.
Result
Systems become more stable and easier to grow without introducing bugs.
Understanding this principle helps keep code safe from accidental errors during updates.
5
IntermediateLiskov Substitution Principle
🤔Before reading on: can a subclass replace its parent class everywhere without problems? Commit to your answer.
Concept: Subclasses should be replaceable for their base classes without changing the correctness of the program.
If a subclass changes behavior in unexpected ways, it breaks the system. For example, if a 'Square' class inherits from 'Rectangle' but changes how width and height work, it can cause errors. Subclasses must follow the rules of their parents.
Result
Inheritance works correctly, and polymorphism can be used safely.
Knowing this prevents subtle bugs when using inheritance and polymorphism.
6
AdvancedInterface Segregation Principle
🤔Before reading on: is it better to have one big interface or many small ones? Commit to your answer.
Concept: Clients should not be forced to depend on interfaces they do not use.
Instead of one large interface with many methods, split it into smaller, specific interfaces. For example, a printer interface can be split into 'Print' and 'Scan' interfaces. Devices that only print don't need to implement scanning methods.
Result
Code becomes more flexible and easier to maintain because classes only implement what they need.
Understanding this reduces unnecessary code and improves clarity in large systems.
7
AdvancedDependency Inversion Principle
🤔Before reading on: should high-level modules depend on low-level modules or abstractions? Commit to your answer.
Concept: High-level modules should not depend on low-level modules; both should depend on abstractions.
This means code should depend on interfaces or abstract classes, not concrete implementations. For example, a payment processor depends on a 'PaymentMethod' interface, not a specific credit card class. This allows easy swapping of implementations without changing high-level code.
Result
Systems become more modular, testable, and adaptable to change.
Knowing this principle helps build loosely coupled systems that are easier to evolve.
8
ExpertBalancing Principles in Real Systems
🤔Before reading on: do you think strictly following all principles always leads to the best design? Commit to your answer.
Concept: Applying design principles requires balance and context; overusing them can cause complexity.
In real projects, blindly following every principle can lead to too many small classes or interfaces, making the system hard to understand. Experts weigh trade-offs, sometimes combining responsibilities or relaxing rules for simplicity and performance. They also consider team skills and project needs.
Result
Designs that are both principled and practical, avoiding over-engineering.
Understanding when to bend principles is key to creating maintainable and efficient software in the real world.
Under the Hood
Object-oriented design principles guide how code is structured at the class and object level. Internally, they influence how data and methods are grouped, how inheritance hierarchies are formed, and how dependencies are managed. Encapsulation controls access to data via access modifiers. Polymorphism allows objects to be treated as instances of their parent types. Interfaces and abstract classes define contracts that classes implement, enabling loose coupling. These principles shape the flow of control and data, reducing tight coupling and increasing modularity.
Why designed this way?
These principles emerged from decades of software development challenges where code became hard to maintain and extend. Early software often had tightly coupled components that broke easily when changed. The principles were designed to promote separation of concerns, reduce dependencies, and make code more understandable and flexible. Alternatives like procedural programming lacked these modularity benefits. The principles balance flexibility with simplicity, aiming to reduce bugs and improve collaboration.
┌───────────────┐       ┌───────────────┐
│   Client      │──────▶│  Interface    │
└───────────────┘       └───────────────┘
                             ▲
                             │
                   ┌─────────┴─────────┐
                   │                   │
           ┌───────────────┐   ┌───────────────┐
           │ Implementation│   │ Implementation│
           └───────────────┘   └───────────────┘

This shows Dependency Inversion: Client depends on Interface, not implementations.
Myth Busters - 4 Common Misconceptions
Quick: Does encapsulation mean you can never access an object's data directly? Commit yes or no.
Common Belief:Encapsulation means you cannot access any data inside an object from outside.
Tap to reveal reality
Reality:Encapsulation means controlling access, usually via methods, but objects often provide controlled ways to read or update data safely.
Why it matters:Believing this can lead to overcomplicated code with unnecessary getters/setters or avoiding useful direct access, reducing clarity.
Quick: Should subclasses always inherit all behavior from parents without change? Commit yes or no.
Common Belief:Subclasses can freely override any behavior of their parent classes.
Tap to reveal reality
Reality:Subclasses must follow the parent's expected behavior to avoid breaking the system (Liskov Substitution Principle).
Why it matters:Ignoring this causes bugs when subclasses behave unexpectedly, breaking polymorphism and code reliability.
Quick: Is it always best to split every class into the smallest possible pieces? Commit yes or no.
Common Belief:More, smaller classes always mean better design.
Tap to reveal reality
Reality:Too many tiny classes can make code harder to understand and maintain; balance is needed.
Why it matters:Over-splitting leads to complexity and confusion, slowing development and increasing errors.
Quick: Does following all SOLID principles guarantee perfect software design? Commit yes or no.
Common Belief:Strictly following all design principles always results in the best software.
Tap to reveal reality
Reality:Principles are guidelines, not strict rules; context and trade-offs matter.
Why it matters:Blindly applying principles can cause over-engineering, wasted effort, and reduced productivity.
Expert Zone
1
Design principles interact; improving one aspect can affect others, so balance is key.
2
Real-world constraints like deadlines and team skills often require pragmatic compromises.
3
Understanding the domain deeply helps tailor principles effectively rather than applying them blindly.
When NOT to use
In small, simple projects or prototypes, strict adherence to all principles may slow development unnecessarily. Alternatives like procedural or functional programming might be better for certain problems, especially where state and objects add complexity without benefit.
Production Patterns
In production, principles guide modular service design, API contracts, and plugin architectures. For example, Dependency Injection frameworks implement Dependency Inversion to swap components easily. Microservices use Single Responsibility Principle to keep services focused. Interface Segregation helps create clear API boundaries between teams.
Connections
Functional Programming
Contrasting paradigm with different approaches to code organization and state management.
Knowing object-oriented principles helps appreciate functional programming's focus on immutability and pure functions, offering alternative ways to manage complexity.
Human Teamwork
Shares the idea of clear roles and responsibilities to work efficiently together.
Understanding how teams succeed by dividing tasks and communicating clearly helps grasp why design principles emphasize single responsibility and loose coupling.
Modular Furniture Design
Both involve creating independent parts that fit together to form a flexible whole.
Seeing software objects like furniture pieces that can be rearranged or replaced clarifies the value of encapsulation and interface segregation.
Common Pitfalls
#1Making classes do too many things at once.
Wrong approach:class User { void login() {} void saveToDatabase() {} void sendEmail() {} }
Correct approach:class User { void login() {} } class UserRepository { void save(User user) {} } class EmailService { void sendEmail(User user) {} }
Root cause:Misunderstanding Single Responsibility Principle and mixing unrelated responsibilities.
#2Tightly coupling high-level code to specific implementations.
Wrong approach:class PaymentProcessor { CreditCardPayment payment; void process() { payment.pay(); } }
Correct approach:interface PaymentMethod { void pay(); } class PaymentProcessor { PaymentMethod payment; void process() { payment.pay(); } }
Root cause:Ignoring Dependency Inversion Principle and depending on concrete classes.
#3Overusing inheritance and breaking Liskov Substitution Principle.
Wrong approach:class Rectangle { int width, height; void setWidth(int w) {} void setHeight(int h) {} } class Square extends Rectangle { void setWidth(int w) { width = height = w; } void setHeight(int h) { width = height = h; } }
Correct approach:class Shape {} class Rectangle extends Shape { int width, height; } class Square extends Shape { int side; }
Root cause:Misusing inheritance for code reuse instead of proper abstraction.
Key Takeaways
Object-oriented design principles help create software that is clear, flexible, and easy to maintain by organizing code into focused, well-defined objects.
Encapsulation protects data and hides complexity, making systems safer and easier to change without breaking.
Following principles like Single Responsibility and Dependency Inversion reduces bugs and improves code adaptability.
Design principles are guidelines, not strict rules; expert developers balance them with practical needs to avoid over-engineering.
Understanding these principles deeply enables building scalable, robust systems and collaborating effectively in software teams.

Practice

(1/5)
1. Which of the following best describes the Single Responsibility Principle (SRP) in object-oriented design?
easy
A. Classes should be tightly coupled to improve performance.
B. A class should inherit from multiple classes to reuse code.
C. Objects should be able to change their behavior at runtime.
D. A class should have only one reason to change, meaning it should have only one job.

Solution

  1. Step 1: Understand SRP definition

    The Single Responsibility Principle states that a class should have only one reason to change, meaning it should focus on a single job or responsibility.
  2. Step 2: Evaluate options against SRP

    A class should have only one reason to change, meaning it should have only one job. matches this definition exactly. Options A, B, and C describe other concepts or incorrect ideas.
  3. Final Answer:

    A class should have only one reason to change, meaning it should have only one job. -> Option D
  4. Quick Check:

    Single Responsibility Principle = One class, one job [OK]
Hint: SRP means one class, one responsibility [OK]
Common Mistakes:
  • Confusing SRP with inheritance rules
  • Thinking SRP allows multiple unrelated jobs in one class
  • Believing tight coupling is good design
2. Which syntax correctly demonstrates the Open/Closed Principle in class design?
easy
A. class Shape { void draw() {} } class Circle { void draw() { /* draw circle */ } }
B. class Shape { void draw() {} } class Circle extends Shape { void draw() { /* draw circle */ } }
C. class Shape { void draw() {} } class Circle extends Shape { void draw() { super.draw(); } }
D. class Shape { void draw() {} } class Circle extends Shape { void draw() { /* draw circle */ } } class Square extends Circle {}

Solution

  1. Step 1: Understand Open/Closed Principle (OCP)

    OCP means software entities like classes should be open for extension but closed for modification. This means you can add new behavior by extending classes without changing existing code.
  2. Step 2: Analyze options for OCP compliance

    class Shape { void draw() {} } class Circle extends Shape { void draw() { /* draw circle */ } } shows a base class Shape and a subclass Circle that extends Shape and overrides draw(), adding new behavior without modifying Shape. This follows OCP. class Shape { void draw() {} } class Circle { void draw() { /* draw circle */ } } lacks inheritance, so no extension. class Shape { void draw() {} } class Circle extends Shape { void draw() { super.draw(); } } calls super.draw() but does not add new behavior. class Shape { void draw() {} } class Circle extends Shape { void draw() { /* draw circle */ } } class Square extends Circle {} extends Circle with Square, which is allowed but less clear and may break OCP if Square changes Circle behavior.
  3. Final Answer:

    class Shape { void draw() {} } class Circle extends Shape { void draw() { /* draw circle */ } } -> Option B
  4. Quick Check:

    Open/Closed = Extend by subclassing, no base change [OK]
Hint: OCP means extend classes, don't modify them [OK]
Common Mistakes:
  • Ignoring inheritance for extension
  • Modifying base class instead of extending
  • Confusing method overriding with no added behavior
3. Consider the following Python code snippet applying the Liskov Substitution Principle (LSP):
class Bird:
    def fly(self):
        return "Flying"

class Penguin(Bird):
    def fly(self):
        return "Cannot fly"

bird = Bird()
penguin = Penguin()
print(bird.fly())
print(penguin.fly())

What is the output of this code?
medium
A. "Flying" followed by "Cannot fly"
B. Both print "Flying"
C. Runtime error because Penguin cannot fly
D. Both print "Cannot fly"

Solution

  1. Step 1: Understand code behavior

    Bird class has fly() returning "Flying". Penguin inherits Bird but overrides fly() to return "Cannot fly". bird.fly() prints "Flying". penguin.fly() prints "Cannot fly".
  2. Step 2: Predict output lines

    First print outputs "Flying". Second print outputs "Cannot fly" as per Penguin's override.
  3. Final Answer:

    "Flying" followed by "Cannot fly" -> Option A
  4. Quick Check:

    Subclass override changes output correctly [OK]
Hint: Subclass method overrides parent method output [OK]
Common Mistakes:
  • Assuming subclass must behave exactly like parent
  • Expecting runtime error for overridden method
  • Confusing method override with inheritance
4. Identify the problem in the following Java code violating the Dependency Inversion Principle (DIP):
class LightBulb {
    void turnOn() { System.out.println("Light on"); }
    void turnOff() { System.out.println("Light off"); }
}

class Switch {
    private LightBulb bulb = new LightBulb();
    void operate(boolean on) {
        if(on) bulb.turnOn();
        else bulb.turnOff();
    }
}
medium
A. Switch class should create multiple LightBulb instances.
B. LightBulb class should not have turnOn and turnOff methods.
C. Switch class directly depends on concrete LightBulb class instead of an abstraction.
D. Switch class should inherit from LightBulb.

Solution

  1. Step 1: Understand Dependency Inversion Principle (DIP)

    DIP states that high-level modules should not depend on low-level modules directly; both should depend on abstractions (interfaces or abstract classes).
  2. Step 2: Analyze code for DIP violation

    Switch class creates and uses a concrete LightBulb instance directly, tightly coupling Switch to LightBulb. This violates DIP because Switch depends on a concrete class, not an abstraction.
  3. Final Answer:

    Switch class directly depends on concrete LightBulb class instead of an abstraction. -> Option C
  4. Quick Check:

    DIP = Depend on abstractions, not concrete classes [OK]
Hint: High-level classes should depend on interfaces, not concrete classes [OK]
Common Mistakes:
  • Thinking methods in LightBulb are wrong
  • Believing multiple instances are needed
  • Confusing inheritance with dependency
5. You are designing a payment system that must support multiple payment methods (credit card, PayPal, crypto). Which object-oriented design principle helps you add new payment methods without changing existing code, and how should you apply it?
hard
A. Use the Open/Closed Principle by defining a Payment interface and implementing new payment classes for each method.
B. Use the Single Responsibility Principle by putting all payment logic in one PaymentProcessor class.
C. Use the Liskov Substitution Principle by making all payment classes inherit from a base Payment class but override methods differently.
D. Use the Dependency Inversion Principle by making payment classes depend on concrete implementations.

Solution

  1. Step 1: Identify the need for extensibility

    The system must allow adding new payment methods without modifying existing code, which is a classic use case for the Open/Closed Principle (OCP).
  2. Step 2: Apply OCP with interface and implementations

    Define a Payment interface with common methods. Implement each payment method (credit card, PayPal, crypto) as separate classes implementing this interface. This allows adding new methods by creating new classes without changing existing ones.
  3. Final Answer:

    Use the Open/Closed Principle by defining a Payment interface and implementing new payment classes for each method. -> Option A
  4. Quick Check:

    OCP = Add new classes, no existing code change [OK]
Hint: OCP means add new classes, don't modify old ones [OK]
Common Mistakes:
  • Putting all logic in one class violating SRP
  • Confusing inheritance with extensibility
  • Depending on concrete classes instead of abstractions
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard