skip to content
luminary.blog
by Oz Akan
car sketch

Object-Oriented Programming Refresher

Refresher OOP concepts with interview quesions

/ 20 min read

Table of Contents

What is Object-Oriented Programming?

Object-Oriented Programming (OOP) is a programming paradigm based on the concept of “objects” - data structures that contain data (attributes) and code (methods). OOP organizes software design around objects rather than functions and logic.

Key Benefits of OOP

  • Code Reusability: Inherit and extend existing code
  • Modularity: Break complex problems into smaller, manageable pieces
  • Maintainability: Easier to modify and extend code
  • Abstraction: Hide complex implementation details
  • Real-world modeling: Map real-world entities to code objects

Core OOP Concepts

1. Classes and Objects

Class

A class is a blueprint or template for creating objects. It defines the structure and behavior that objects will have.

class Car:
# Class variable (shared by all instances)
wheels = 4
# Constructor method
def __init__(self, make, model, year):
# Instance variables (unique to each object)
self.make = make
self.model = model
self.year = year
self.is_running = False
# Instance method
def start_engine(self):
self.is_running = True
return f"{self.make} {self.model} engine started!"
def stop_engine(self):
self.is_running = False
return f"{self.make} {self.model} engine stopped!"
# String representation
def __str__(self):
return f"{self.year} {self.make} {self.model}"

Object

An object is an instance of a class - a concrete realization of the class blueprint.

# Creating objects (instances)
car1 = Car("Toyota", "Camry", 2023)
car2 = Car("Honda", "Civic", 2022)
# Accessing attributes
print(car1.make) # Toyota
print(car1.wheels) # 4 (class variable)
# Calling methods
print(car1.start_engine()) # Toyota Camry engine started!
print(car1) # 2023 Toyota Camry

Java Example

public class Car {
// Class variable
public static final int WHEELS = 4;
// Instance variables
private String make;
private String model;
private int year;
private boolean isRunning;
// Constructor
public Car(String make, String model, int year) {
this.make = make;
this.model = model;
this.year = year;
this.isRunning = false;
}
// Methods
public String startEngine() {
this.isRunning = true;
return this.make + " " + this.model + " engine started!";
}
public String getMake() {
return this.make;
}
@Override
public String toString() {
return this.year + " " + this.make + " " + this.model;
}
}

The Four Pillars of OOP

1. Encapsulation

Encapsulation is the bundling of data and methods that operate on that data within a single unit (class), and restricting access to internal implementation details.

Python Example

class BankAccount:
def __init__(self, account_number, initial_balance=0):
self.account_number = account_number
self.__balance = initial_balance # Private attribute (name mangling)
self._transaction_history = [] # Protected attribute (convention)
# Public method to access private data
def get_balance(self):
return self.__balance
# Public method to modify private data
def deposit(self, amount):
if amount > 0:
self.__balance += amount
self._add_transaction(f"Deposited ${amount}")
return True
return False
def withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount
self._add_transaction(f"Withdrew ${amount}")
return True
return False
# Private method (internal implementation)
def _add_transaction(self, description):
from datetime import datetime
self._transaction_history.append({
'date': datetime.now(),
'description': description,
'balance': self.__balance
})
# Property decorator for controlled access
@property
def balance(self):
return self.__balance
@balance.setter
def balance(self, value):
if value >= 0:
self.__balance = value
# Usage
account = BankAccount("123456", 1000)
print(account.get_balance()) # 1000
account.deposit(500)
print(account.balance) # 1500 (using property)
# This won't work - private attribute
# print(account.__balance) # AttributeError

Java Example

public class BankAccount {
private String accountNumber;
private double balance;
private List<String> transactionHistory;
public BankAccount(String accountNumber, double initialBalance) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
this.transactionHistory = new ArrayList<>();
}
// Getter (accessor)
public double getBalance() {
return this.balance;
}
// Setter (mutator) with validation
public void setBalance(double balance) {
if (balance >= 0) {
this.balance = balance;
}
}
public boolean deposit(double amount) {
if (amount > 0) {
this.balance += amount;
addTransaction("Deposited $" + amount);
return true;
}
return false;
}
private void addTransaction(String description) {
this.transactionHistory.add(description);
}
}

Benefits of Encapsulation:

  • Data Protection: Prevent unauthorized access to internal data
  • Validation: Control how data is modified
  • Flexibility: Change internal implementation without affecting external code
  • Debugging: Easier to track data changes

2. Inheritance

Inheritance allows a class (child/derived) to inherit properties and methods from another class (parent/base), promoting code reuse and establishing hierarchical relationships.

Python Example

# Base class (Parent)
class Animal:
def __init__(self, name, species):
self.name = name
self.species = species
self.is_alive = True
def eat(self, food):
return f"{self.name} is eating {food}"
def sleep(self):
return f"{self.name} is sleeping"
def make_sound(self):
return "Some generic animal sound"
# Derived class (Child)
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name, "Canine") # Call parent constructor
self.breed = breed
# Method overriding
def make_sound(self):
return f"{self.name} says Woof!"
# Additional method specific to Dog
def fetch(self, item):
return f"{self.name} fetched the {item}!"
class Cat(Animal):
def __init__(self, name, breed):
super().__init__(name, "Feline")
self.breed = breed
def make_sound(self):
return f"{self.name} says Meow!"
def climb(self):
return f"{self.name} climbed up the tree!"
# Usage
dog = Dog("Buddy", "Golden Retriever")
cat = Cat("Whiskers", "Persian")
print(dog.eat("dog food")) # Inherited method
print(dog.make_sound()) # Overridden method
print(dog.fetch("ball")) # Dog-specific method
print(cat.make_sound()) # Overridden method
print(cat.climb()) # Cat-specific method

Multiple Inheritance Example

class Flyable:
def fly(self):
return "Flying through the air!"
class Swimmable:
def swim(self):
return "Swimming in water!"
class Duck(Animal, Flyable, Swimmable):
def __init__(self, name):
super().__init__(name, "Bird")
def make_sound(self):
return f"{self.name} says Quack!"
# Usage
duck = Duck("Donald")
print(duck.fly()) # From Flyable
print(duck.swim()) # From Swimmable
print(duck.eat("bread")) # From Animal

Java Example (Single Inheritance)

// Base class
public class Animal {
protected String name;
protected String species;
protected boolean isAlive;
public Animal(String name, String species) {
this.name = name;
this.species = species;
this.isAlive = true;
}
public String eat(String food) {
return this.name + " is eating " + food;
}
public String makeSound() {
return "Some generic animal sound";
}
}
// Derived class
public class Dog extends Animal {
private String breed;
public Dog(String name, String breed) {
super(name, "Canine"); // Call parent constructor
this.breed = breed;
}
@Override
public String makeSound() {
return this.name + " says Woof!";
}
public String fetch(String item) {
return this.name + " fetched the " + item + "!";
}
}

Types of Inheritance:

  • Single Inheritance: One parent class
  • Multiple Inheritance: Multiple parent classes (Python supports, Java doesn’t)
  • Multilevel Inheritance: Child class becomes parent to another class
  • Hierarchical Inheritance: Multiple child classes from one parent

3. Polymorphism

Polymorphism allows objects of different classes to be treated as objects of a common base class, while still maintaining their own specific behavior.

Runtime Polymorphism (Method Overriding)

class Shape:
def area(self):
raise NotImplementedError("Subclass must implement area()")
def perimeter(self):
raise NotImplementedError("Subclass must implement perimeter()")
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
def perimeter(self):
return 2 * 3.14159 * self.radius
class Triangle(Shape):
def __init__(self, base, height, side1, side2):
self.base = base
self.height = height
self.side1 = side1
self.side2 = side2
def area(self):
return 0.5 * self.base * self.height
def perimeter(self):
return self.base + self.side1 + self.side2
# Polymorphic function
def print_shape_info(shape):
print(f"Shape: {type(shape).__name__}")
print(f"Area: {shape.area()}")
print(f"Perimeter: {shape.perimeter()}")
print("-" * 20)
# Usage - same function works with different object types
shapes = [
Rectangle(5, 3),
Circle(4),
Triangle(6, 4, 5, 5)
]
for shape in shapes:
print_shape_info(shape) # Polymorphic behavior

Compile-time Polymorphism (Method Overloading)

class Calculator:
def add(self, a, b=None, c=None):
"""Method overloading simulation using default parameters"""
if c is not None:
return a + b + c
elif b is not None:
return a + b
else:
return a
# Using multiple dispatch (requires functools.singledispatch)
from functools import singledispatch
@singledispatch
@staticmethod
def multiply(x):
raise NotImplementedError("Unsupported type")
@multiply.register
@staticmethod
def _(x: int, y: int):
return x * y
@multiply.register
@staticmethod
def _(x: float, y: float):
return x * y
@multiply.register
@staticmethod
def _(x: str, y: int):
return x * y
# Usage
calc = Calculator()
print(calc.add(1, 2)) # 3
print(calc.add(1, 2, 3)) # 6

Java Polymorphism Example

// Base class
abstract class Shape {
public abstract double area();
public abstract double perimeter();
}
class Rectangle extends Shape {
private double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
@Override
public double perimeter() {
return 2 * (width + height);
}
}
class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public double perimeter() {
return 2 * Math.PI * radius;
}
}
// Polymorphic usage
public class ShapeDemo {
public static void printShapeInfo(Shape shape) {
System.out.println("Area: " + shape.area());
System.out.println("Perimeter: " + shape.perimeter());
}
public static void main(String[] args) {
Shape[] shapes = {
new Rectangle(5, 3),
new Circle(4)
};
for (Shape shape : shapes) {
printShapeInfo(shape); // Polymorphic behavior
}
}
}

4. Abstraction

Abstraction hides complex implementation details and shows only the essential features of an object. It focuses on what an object does rather than how it does it.

Abstract Base Classes

from abc import ABC, abstractmethod
class Vehicle(ABC):
def __init__(self, make, model):
self.make = make
self.model = model
@abstractmethod
def start_engine(self):
pass
@abstractmethod
def stop_engine(self):
pass
@abstractmethod
def get_fuel_efficiency(self):
pass
# Concrete method (shared implementation)
def get_info(self):
return f"{self.make} {self.model}"
class Car(Vehicle):
def __init__(self, make, model, fuel_type):
super().__init__(make, model)
self.fuel_type = fuel_type
self.engine_running = False
def start_engine(self):
self.engine_running = True
return f"{self.get_info()} engine started"
def stop_engine(self):
self.engine_running = False
return f"{self.get_info()} engine stopped"
def get_fuel_efficiency(self):
return "25 MPG" # Simplified
class ElectricCar(Vehicle):
def __init__(self, make, model, battery_capacity):
super().__init__(make, model)
self.battery_capacity = battery_capacity
self.motor_running = False
def start_engine(self):
self.motor_running = True
return f"{self.get_info()} motor started silently"
def stop_engine(self):
self.motor_running = False
return f"{self.get_info()} motor stopped"
def get_fuel_efficiency(self):
return "100 MPGe" # Miles per gallon equivalent
# Usage
vehicles = [
Car("Toyota", "Camry", "Gasoline"),
ElectricCar("Tesla", "Model 3", "75 kWh")
]
for vehicle in vehicles:
print(vehicle.start_engine())
print(f"Efficiency: {vehicle.get_fuel_efficiency()}")
print(vehicle.stop_engine())
print("-" * 30)

Interface-like Abstraction

class Drawable:
"""Interface-like class defining drawing contract"""
def draw(self):
raise NotImplementedError("Must implement draw()")
def resize(self, factor):
raise NotImplementedError("Must implement resize()")
class Circle(Drawable):
def __init__(self, radius):
self.radius = radius
def draw(self):
return f"Drawing circle with radius {self.radius}"
def resize(self, factor):
self.radius *= factor
return f"Circle resized, new radius: {self.radius}"
class Square(Drawable):
def __init__(self, side):
self.side = side
def draw(self):
return f"Drawing square with side {self.side}"
def resize(self, factor):
self.side *= factor
return f"Square resized, new side: {self.side}"
def render_shapes(shapes):
"""Function that works with any Drawable object"""
for shape in shapes:
print(shape.draw())
print(shape.resize(1.5))

Java Interface Example

// Interface
interface Drawable {
void draw();
void resize(double factor);
}
// Implementation
class Circle implements Drawable {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public void draw() {
System.out.println("Drawing circle with radius " + radius);
}
@Override
public void resize(double factor) {
this.radius *= factor;
System.out.println("Circle resized, new radius: " + radius);
}
}

Advanced OOP Concepts

Composition vs Inheritance

Composition (“Has-a” relationship)

class Engine:
def __init__(self, horsepower, fuel_type):
self.horsepower = horsepower
self.fuel_type = fuel_type
self.running = False
def start(self):
self.running = True
return f"{self.horsepower}HP {self.fuel_type} engine started"
def stop(self):
self.running = False
return f"Engine stopped"
class Transmission:
def __init__(self, type_name, gears):
self.type = type_name
self.gears = gears
self.current_gear = 1
def shift_up(self):
if self.current_gear < self.gears:
self.current_gear += 1
return f"Shifted to gear {self.current_gear}"
class Car:
def __init__(self, make, model, engine, transmission):
self.make = make
self.model = model
self.engine = engine # Composition
self.transmission = transmission # Composition
def start(self):
return self.engine.start()
def accelerate(self):
if self.engine.running:
return self.transmission.shift_up()
return "Start the engine first!"
# Usage
engine = Engine(200, "Gasoline")
transmission = Transmission("Manual", 6)
car = Car("Honda", "Civic", engine, transmission)
print(car.start()) # Delegates to engine
print(car.accelerate()) # Uses transmission

When to use Composition vs Inheritance:

  • Inheritance: “Is-a” relationship (Dog IS-A Animal)
  • Composition: “Has-a” relationship (Car HAS-A Engine)
  • Prefer composition for flexibility and loose coupling

Design Patterns in OOP

1. Singleton Pattern

class DatabaseConnection:
_instance = None
_connection = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if self._connection is None:
self._connection = self._create_connection()
def _create_connection(self):
# Simulate database connection
return "Connected to database"
def get_connection(self):
return self._connection
# Usage
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2) # True - same instance

2. Factory Pattern

class AnimalFactory:
@staticmethod
def create_animal(animal_type, name):
if animal_type.lower() == "dog":
return Dog(name, "Mixed")
elif animal_type.lower() == "cat":
return Cat(name, "Mixed")
elif animal_type.lower() == "duck":
return Duck(name)
else:
raise ValueError(f"Unknown animal type: {animal_type}")
# Usage
animals = [
AnimalFactory.create_animal("dog", "Buddy"),
AnimalFactory.create_animal("cat", "Whiskers"),
AnimalFactory.create_animal("duck", "Donald")
]
for animal in animals:
print(animal.make_sound())

3. Observer Pattern

class Subject:
def __init__(self):
self._observers = []
self._state = None
def attach(self, observer):
self._observers.append(observer)
def detach(self, observer):
self._observers.remove(observer)
def notify(self):
for observer in self._observers:
observer.update(self)
def set_state(self, state):
self._state = state
self.notify()
def get_state(self):
return self._state
class Observer:
def update(self, subject):
raise NotImplementedError
class EmailNotifier(Observer):
def update(self, subject):
print(f"Email sent: State changed to {subject.get_state()}")
class SMSNotifier(Observer):
def update(self, subject):
print(f"SMS sent: State changed to {subject.get_state()}")
# Usage
subject = Subject()
email_notifier = EmailNotifier()
sms_notifier = SMSNotifier()
subject.attach(email_notifier)
subject.attach(sms_notifier)
subject.set_state("Order Shipped") # Both notifiers will be updated

4. Strategy Pattern

class PaymentStrategy:
def pay(self, amount):
raise NotImplementedError
class CreditCardPayment(PaymentStrategy):
def __init__(self, card_number):
self.card_number = card_number
def pay(self, amount):
return f"Paid ${amount} using Credit Card ending in {self.card_number[-4:]}"
class PayPalPayment(PaymentStrategy):
def __init__(self, email):
self.email = email
def pay(self, amount):
return f"Paid ${amount} using PayPal account {self.email}"
class BitcoinPayment(PaymentStrategy):
def __init__(self, wallet_address):
self.wallet_address = wallet_address
def pay(self, amount):
return f"Paid ${amount} using Bitcoin wallet {self.wallet_address[:8]}..."
class ShoppingCart:
def __init__(self):
self.items = []
self.payment_strategy = None
def add_item(self, item, price):
self.items.append((item, price))
def set_payment_strategy(self, strategy):
self.payment_strategy = strategy
def checkout(self):
total = sum(price for item, price in self.items)
if self.payment_strategy:
return self.payment_strategy.pay(total)
return "No payment method selected"
# Usage
cart = ShoppingCart()
cart.add_item("Laptop", 999.99)
cart.add_item("Mouse", 29.99)
# Use different payment strategies
cart.set_payment_strategy(CreditCardPayment("1234567890123456"))
print(cart.checkout())
cart.set_payment_strategy(PayPalPayment("user@example.com"))
print(cart.checkout())

Class Relationships

1. Association

Objects are related but independent.

class Student:
def __init__(self, name):
self.name = name
self.courses = []
def enroll(self, course):
self.courses.append(course)
course.add_student(self)
class Course:
def __init__(self, name):
self.name = name
self.students = []
def add_student(self, student):
self.students.append(student)

2. Aggregation

“Has-a” relationship where child can exist without parent.

class Department:
def __init__(self, name):
self.name = name
self.employees = []
def add_employee(self, employee):
self.employees.append(employee)
class Employee:
def __init__(self, name):
self.name = name
# Employee can exist without Department

3. Composition

“Has-a” relationship where child cannot exist without parent.

class House:
def __init__(self):
self.rooms = [Room("Living Room"), Room("Bedroom")]
def __del__(self):
# When house is destroyed, rooms are destroyed too
del self.rooms
class Room:
def __init__(self, name):
self.name = name
# Room cannot exist without House

SOLID Principles

For more more details and TypeScript examples, check SOLID Principles with Examples article.

1. Single Responsibility Principle (SRP)

A class should have only one reason to change.

# Bad: Multiple responsibilities
class User:
def __init__(self, name, email):
self.name = name
self.email = email
def save_to_database(self):
# Database logic
pass
def send_email(self):
# Email logic
pass
# Good: Single responsibility
class User:
def __init__(self, name, email):
self.name = name
self.email = email
class UserRepository:
def save(self, user):
# Database logic
pass
class EmailService:
def send_email(self, user, message):
# Email logic
pass

2. Open/Closed Principle (OCP)

Classes should be open for extension but closed for modification.

class AreaCalculator:
def calculate(self, shapes):
total_area = 0
for shape in shapes:
total_area += shape.area() # Extensible without modification
return total_area
# Adding new shapes doesn't require modifying AreaCalculator
class Pentagon(Shape):
def __init__(self, side):
self.side = side
def area(self):
return 1.72 * self.side ** 2

3. Liskov Substitution Principle (LSP)

Objects of a superclass should be replaceable with objects of its subclasses.

class Bird:
def fly(self):
return "Flying"
class Sparrow(Bird):
def fly(self):
return "Sparrow flying"
class Penguin(Bird):
def fly(self):
raise Exception("Penguins can't fly") # Violates LSP
# Better design
class Bird:
def move(self):
raise NotImplementedError
class FlyingBird(Bird):
def fly(self):
return "Flying"
def move(self):
return self.fly()
class Penguin(Bird):
def swim(self):
return "Swimming"
def move(self):
return self.swim()

4. Interface Segregation Principle (ISP)

Clients should not be forced to depend on interfaces they don’t use.

# Bad: Fat interface
class Worker:
def work(self):
pass
def eat(self):
pass
class Robot(Worker):
def work(self):
return "Robot working"
def eat(self):
pass # Robots don't eat - violates ISP
# Good: Segregated interfaces
class Workable:
def work(self):
pass
class Eatable:
def eat(self):
pass
class Human(Workable, Eatable):
def work(self):
return "Human working"
def eat(self):
return "Human eating"
class Robot(Workable):
def work(self):
return "Robot working"

5. Dependency Inversion Principle (DIP)

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

# Bad: High-level depends on low-level
class EmailService:
def send(self, message):
# Send email
pass
class NotificationManager:
def __init__(self):
self.email_service = EmailService() # Tight coupling
def send_notification(self, message):
self.email_service.send(message)
# Good: Both depend on abstraction
class NotificationService:
def send(self, message):
raise NotImplementedError
class EmailService(NotificationService):
def send(self, message):
return f"Email sent: {message}"
class SMSService(NotificationService):
def send(self, message):
return f"SMS sent: {message}"
class NotificationManager:
def __init__(self, notification_service):
self.notification_service = notification_service # Dependency injection
def send_notification(self, message):
return self.notification_service.send(message)
# Usage
email_manager = NotificationManager(EmailService())
sms_manager = NotificationManager(SMSService())

OOP Interview Questions and Answers

Q1: What is the difference between a class and an object?

A1: A class is a blueprint or template that defines the structure and behavior of objects, including attributes and methods. An object is a concrete instance of a class - an actual realization created from the class blueprint with specific values. For example, Car is a class, while my_car = Car("Toyota", "Camry", 2023) creates an object instance.

Q2: Explain the four pillars of OOP.

A2: The four pillars are:

  1. Encapsulation: Bundling data and methods together while hiding internal implementation details
  2. Inheritance: Allowing a class to inherit properties and methods from another class for code reuse
  3. Polymorphism: Enabling objects of different classes to be treated uniformly while maintaining their specific behaviors
  4. Abstraction: Hiding complex implementation details and exposing only essential features

Q3: What is the difference between method overloading and method overriding?

A3: Method overloading (compile-time polymorphism) is having multiple methods with the same name but different parameters in the same class. Method overriding (runtime polymorphism) is when a subclass provides a specific implementation for a method already defined in its parent class. Overloading happens within one class; overriding happens across inheritance hierarchy.

Q4: When should you use composition over inheritance?

A4: Use composition when you have a “has-a” relationship rather than an “is-a” relationship. Composition provides better flexibility, loose coupling, and easier testing. For example, a Car “has-a” Engine (composition) rather than a Car “is-a” Engine (inheritance). Prefer composition when you need to change behavior at runtime or avoid tight coupling between classes.

Q5: What is an abstract class and when would you use it?

A5: An abstract class is a class that cannot be instantiated and may contain abstract methods (methods without implementation). It serves as a base class that defines a contract for subclasses. Use abstract classes when you want to provide common functionality to related classes while forcing them to implement specific methods. For example, a Vehicle abstract class can define common attributes while forcing Car and Motorcycle subclasses to implement their own start_engine() method.

Q6: Explain the Single Responsibility Principle.

A6: The Single Responsibility Principle states that a class should have only one reason to change - it should have only one job or responsibility. For example, a User class should only handle user data, not database operations or email sending. Separate concerns into different classes like UserRepository for database operations and EmailService for sending emails. This makes code more maintainable and testable.

Q7: What is the difference between private, protected, and public access modifiers?

A7:

  • Public: Accessible from anywhere (all classes)
  • Protected: Accessible within the class and its subclasses
  • Private: Accessible only within the class itself

In Python, these are conventions: _protected (single underscore) and __private (double underscore with name mangling). In Java, they are enforced keywords.

Q8: What is a design pattern? Name three common ones.

A8: A design pattern is a reusable solution to a commonly occurring problem in software design. Three common patterns are:

  1. Singleton: Ensures only one instance of a class exists
  2. Factory: Creates objects without specifying exact class to create
  3. Observer: Allows objects to notify other objects about state changes

Q9: Explain polymorphism with a real-world example.

A9: Polymorphism allows objects of different types to be treated uniformly. Real-world example: A payment system that accepts different payment methods (CreditCard, PayPal, Bitcoin). All implement a pay() method, but each executes it differently. The checkout system can call payment_method.pay(amount) without knowing the specific payment type - the correct implementation is called automatically based on the object type.

Q10: What is the Liskov Substitution Principle?

A10: The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of its subclasses without breaking the application. Subclasses must honor the contract of the parent class. For example, if you have a Bird class with a fly() method, creating a Penguin subclass that throws an error in fly() violates LSP because penguins can’t fly. Better design: separate FlyingBird and FlightlessBird classes.

Q11: What is the difference between aggregation and composition?

A11: Both represent “has-a” relationships, but differ in ownership:

  • Aggregation: Child can exist independently of parent (e.g., Department has Employees; employees can exist without the department)
  • Composition: Child cannot exist without parent (e.g., House has Rooms; rooms don’t exist without the house)

Composition implies stronger ownership and lifecycle dependency.

Q12: Explain the Open/Closed Principle with an example.

A12: The Open/Closed Principle states that classes should be open for extension but closed for modification. Example: An AreaCalculator class that calculates total area of shapes should not need modification when adding new shape types. Instead, new shapes inherit from a Shape base class and implement their own area() method. The calculator works with any shape without code changes.

Q13: What is multiple inheritance and what problems can it cause?

A13: Multiple inheritance allows a class to inherit from multiple parent classes. It can cause the “diamond problem” - when two parent classes have a method with the same name, which one should the child inherit? Python supports multiple inheritance and uses Method Resolution Order (MRO) to resolve conflicts. Java avoids this by not supporting multiple inheritance for classes (but allows multiple interface implementation).

Q14: What is dependency injection and why is it useful?

A14: Dependency injection is passing dependencies to a class from outside rather than creating them internally. Instead of class A { b = new B() }, use class A { constructor(b) { this.b = b } }. Benefits include:

  • Loose coupling between classes
  • Easier testing (can inject mock objects)
  • Better flexibility (can swap implementations)
  • Follows Dependency Inversion Principle

Q15: Explain the difference between static and instance methods.

A15:

  • Instance methods: Operate on instance data, require an object to be called, access instance variables via self (Python) or this (Java)
  • Static methods: Belong to the class itself, don’t require an instance, can’t access instance variables, used for utility functions related to the class

Example: Car.get_wheel_count() (static) vs my_car.start_engine() (instance).

Q16: What is the Interface Segregation Principle?

A16: The Interface Segregation Principle states that clients should not be forced to depend on interfaces they don’t use. Instead of one large interface, create multiple smaller, specific interfaces. Example: Don’t create a Worker interface with work() and eat() methods that forces Robot class to implement eat(). Instead, create separate Workable and Eatable interfaces.

Q17: What is the difference between association and dependency?

A17:

  • Association: A structural relationship where objects are connected and can exist independently (e.g., Student and Course - both can exist independently but are related)
  • Dependency: A weaker relationship where one class temporarily uses another (e.g., a method parameter or local variable)

Association is “knows-a” or “has-a”, dependency is “uses-a” temporarily.

Q18: Explain the Strategy Pattern and when to use it.

A18: The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Use it when you have multiple ways of performing an operation and want to switch between them at runtime. Example: A ShoppingCart that can accept different payment strategies (CreditCard, PayPal, Bitcoin) without changing cart logic. The payment method can be set dynamically: cart.set_payment_strategy(PayPalPayment()).

Q19: What is tight coupling vs loose coupling?

A19:

  • Tight coupling: Classes are highly dependent on each other; changes in one class require changes in another
  • Loose coupling: Classes have minimal dependencies; changes in one class don’t affect others

Loose coupling is achieved through interfaces, dependency injection, and abstraction. Example: Instead of class A { b = new ConcreteB() } (tight), use class A { constructor(interface_b) } (loose).

Q20: What is the difference between an abstract class and an interface?

A20: Abstract Class:

  • Can have both abstract and concrete methods
  • Can have instance variables and constructors
  • Supports single inheritance (in most languages)
  • Use for “is-a” relationships with shared implementation

Interface:

  • Only method signatures (traditionally; modern languages allow default methods)
  • No instance variables (only constants)
  • Supports multiple implementation
  • Use for “can-do” capabilities and contracts

Example: Animal (abstract class with shared code) vs Flyable (interface defining flying capability).