These notes cover all essential concepts, principles, and patterns required for Low-Level Design (LLD). It includes explanations, examples, and diagrams to make the understanding and implementation process seamless.


1. Object-Oriented Programming (OOP) Principles

1.1 Encapsulation

Encapsulation hides the internal details of a class and exposes only what is necessary.

Example:

class BankAccount:
    def __init__(self, balance):
        self.__balance = balance  # Private variable

    def deposit(self, amount):
        self.__balance += amount

    def withdraw(self, amount):
        if amount > self.__balance:
            raise ValueError("Insufficient balance")
        self.__balance -= amount

    def get_balance(self):
        return self.__balance

1.2 Abstraction

Abstraction simplifies complex systems by modeling classes relevant to the context while hiding unnecessary details.

Example:

from abc import ABC, abstractmethod

class Payment(ABC):
    @abstractmethod
    def make_payment(self, amount):
        pass

class CreditCardPayment(Payment):
    def make_payment(self, amount):
        print(f"Paid {amount} using Credit Card")

class PayPalPayment(Payment):
    def make_payment(self, amount):
        print(f"Paid {amount} using PayPal")

1.3 Inheritance

Inheritance allows one class to acquire the properties and methods of another.

Example:

class Animal:
    def eat(self):
        print("This animal eats food.")

class Dog(Animal):
    def bark(self):
        print("The dog barks.")

# Usage
dog = Dog()
dog.eat()  # Inherited method
dog.bark()

1.4 Polymorphism

Polymorphism allows different classes to be treated as instances of the same base class.

Example:

class Bird:
    def sound(self):
        pass

class Sparrow(Bird):
    def sound(self):
        print("Chirp chirp")

class Crow(Bird):
    def sound(self):
        print("Caw caw")

# Polymorphism in action
def make_sound(bird):
    bird.sound()

make_sound(Sparrow())
make_sound(Crow())

2. SOLID Principles

2.1 Single Responsibility Principle (SRP)

A class should have only one responsibility.

Example:

class Invoice:
    def calculate_total(self):
        # Logic for calculating total
        pass

class InvoicePrinter:
    def print_invoice(self, invoice):
        # Logic for printing the invoice
        pass

2.2 Open/Closed Principle (OCP)

A class should be open for extension but closed for modification.

Example:

class Discount:
    def calculate(self, amount):
        return amount

class SeasonalDiscount(Discount):
    def calculate(self, amount):
        return amount * 0.9  # 10% off

class ClearanceDiscount(Discount):
    def calculate(self, amount):
        return amount * 0.7  # 30% off

# Extendable without modifying the base class

2.3 Liskov Substitution Principle (LSP)

Subtypes must be substitutable for their base types.

Example:

class Bird:
    def fly(self):
        print("This bird can fly")

class Sparrow(Bird):
    pass

class Ostrich(Bird):
    def fly(self):
        raise Exception("Ostriches can't fly")

# Violation: Ostrich does not conform to Bird's behavior

2.4 Interface Segregation Principle (ISP)

A class should not implement methods it does not use.

Example:

class Printer:
    def print_document(self):
        pass

class Scanner:
    def scan_document(self):
        pass

class MultiFunctionDevice(Printer, Scanner):
    def print_document(self):
        print("Printing document")

    def scan_document(self):
        print("Scanning document")

2.5 Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions.

Example:

class Keyboard:
    def input(self):
        return "User input from keyboard"

class Monitor:
    def display(self, content):
        print(content)

class Computer:
    def __init__(self, keyboard: Keyboard, monitor: Monitor):
        self.keyboard = keyboard
        self.monitor = monitor

    def compute(self):
        data = self.keyboard.input()
        self.monitor.display(data)

3. Design Patterns

3.1 Creational Patterns

  • Singleton: Ensures a class has only one instance.

Example:

class Singleton:
    _instance = None

    @staticmethod
    def get_instance():
        if Singleton._instance is None:
            Singleton._instance = Singleton()
        return Singleton._instance
  • Factory: Provides a way to create objects without specifying the exact class.

Example:

class ShapeFactory:
    @staticmethod
    def get_shape(type):
        if type == "circle":
            return Circle()
        elif type == "square":
            return Square()

3.2 Structural Patterns

  • Adapter: Allows incompatible interfaces to work together.

Example:

class EuropeanSocket:
    def voltage(self):
        return 220

class AmericanSocket:
    def voltage(self):
        return 110

class Adapter:
    def __init__(self, socket):
        self.socket = socket

    def get_voltage(self):
        return self.socket.voltage()
  • Decorator: Adds responsibilities to objects dynamically.

Example:

class Coffee:
    def cost(self):
        return 5

class MilkDecorator:
    def __init__(self, coffee):
        self.coffee = coffee

    def cost(self):
        return self.coffee.cost() + 2

coffee = MilkDecorator(Coffee())
print(coffee.cost())

3.3 Behavioral Patterns

  • Observer: Defines a dependency between objects so that when one changes, others are notified.

Example:

class Subject:
    def __init__(self):
        self._observers = []

    def attach(self, observer):
        self._observers.append(observer)

    def notify(self):
        for observer in self._observers:
            observer.update()

class Observer:
    def update(self):
        pass
  • Strategy: Encapsulates algorithms and allows them to be interchangeable.

Example:

class Strategy:
    def execute(self):
        pass

class StrategyA(Strategy):
    def execute(self):
        print("Strategy A")

class StrategyB(Strategy):
    def execute(self):
        print("Strategy B")

# Usage
strategy = StrategyA()
strategy.execute()

4. UML Diagrams

  • Class Diagram: Represents the structure of a system (classes, attributes, methods, and relationships).
  • Sequence Diagram: Illustrates the interaction between objects over time.
  • State Diagram: Describes state transitions of a system.

This comprehensive guide provides a complete understanding of Low-Level Design (LLD) with principles, patterns, and examples. Use this as a one-stop reference for interviews and implementation.