OOP Basics - Interview Preparation¶
Junior Level Questions¶
Q1: What are the 4 pillars of OOP?¶
Answer:
-
Encapsulation — Bundling data and methods together, hiding internal state behind a public interface. Example: a BankAccount class hides the balance field and only exposes deposit/withdraw methods.
-
Abstraction — Hiding complexity and showing only essential features. Example: you call
file.read()without knowing whether it reads from disk, network, or memory. -
Inheritance — Creating new types based on existing ones, reusing behavior. A
Dogclass inherits fromAnimal. (Go uses composition/embedding instead.) -
Polymorphism — Different types responding to the same interface. A function accepting a
Shapecan work withCircle,Rectangle, orTriangle— each computesarea()differently.
Q2: What is the difference between a class and an object?¶
Answer:
- A class is a blueprint/template that defines the structure (fields) and behavior (methods) of a type.
- An object (instance) is a specific realization of that class in memory with actual values.
// Class = blueprint
class Dog {
String name;
public Dog(String name) { this.name = name; }
public void bark() { System.out.println(name + ": Woof!"); }
}
// Objects = instances
Dog rex = new Dog("Rex"); // object 1
Dog buddy = new Dog("Buddy"); // object 2
rex.bark(); // "Rex: Woof!"
buddy.bark(); // "Buddy: Woof!"
In Go, there are no classes — structs are value types, and you create instances with struct literals or constructor functions.
Q3: What is a constructor? What happens if you don't define one?¶
Answer:
A constructor initializes a new object.
- Java: If you don't define one, Java provides a default no-arg constructor. If you define any constructor, the default disappears.
- Python:
__init__is the initializer. If omitted,object.__init__is used (does nothing). - Go: No constructors. Convention is to write
NewTypeName()factory functions.
// Java
public class User {
private String name;
// No constructor defined → Java provides: public User() {}
}
User u = new User(); // Works — default constructor
// u.name is null
Q4: Explain encapsulation with a practical example.¶
Answer:
Encapsulation means controlling access to internal data through methods.
# Without encapsulation — dangerous
class BankAccount:
def __init__(self):
self.balance = 0
account = BankAccount()
account.balance = -1000 # Anyone can set invalid state!
# With encapsulation — safe
class BankAccount:
def __init__(self):
self._balance = 0
def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError("Amount must be positive")
self._balance += amount
def withdraw(self, amount: float) -> bool:
if amount <= 0 or amount > self._balance:
return False
self._balance -= amount
return True
@property
def balance(self) -> float:
return self._balance # Read-only access
Q5: What is this / self / receiver? Why is it needed?¶
Answer:
It's a reference to the current instance, allowing a method to access its own fields and other methods.
- Java:
thisis implicit (automatically available) - Python:
selfis explicit (first parameter of every instance method) - Go: receiver variable (named by convention, e.g.,
sforServer)
type Server struct {
port int
}
func (s *Server) Start() {
fmt.Printf("Starting on port %d\n", s.port) // 's' is the receiver
}
Without it, methods wouldn't know which instance's data to operate on.
Middle Level Questions¶
Q6: Interface vs Abstract Class — when to use which?¶
Answer:
| Aspect | Interface | Abstract Class |
|---|---|---|
| Implementation | No state, only method signatures | Can have state and partial implementation |
| Multiple | A class can implement multiple | Java: single inheritance only |
| Use when | Defining a contract | Sharing code between related classes |
| Coupling | Low coupling | Higher coupling |
| Go equivalent | interface (only option) | Interface + embedded base struct |
Rule of thumb: Start with an interface. Use an abstract class only when you need shared state or implementation across a family of related types.
// Interface: when unrelated types share behavior
interface Serializable {
byte[] serialize();
}
// User, Order, Product can all implement Serializable
// Abstract class: when related types share implementation
abstract class HttpHandler {
protected final Logger logger;
public HttpHandler(Logger logger) {
this.logger = logger;
}
// Shared behavior
public void handle(Request req, Response res) {
logger.info("Handling: " + req.getPath());
doHandle(req, res); // Template Method
}
protected abstract void doHandle(Request req, Response res);
}
Q7: Explain composition vs inheritance. When would you prefer each?¶
Answer:
- Inheritance (IS-A):
Dog extends Animal— Dog IS an Animal - Composition (HAS-A):
Car has an Engine— Car HAS an Engine
Prefer composition when: - You need behavior from multiple sources - The relationship is "has-a" or "uses-a" - You want to change behavior at runtime - You want loose coupling
Use inheritance when: - True "is-a" relationship with single level - Template Method pattern
// Go enforces composition — no inheritance available
type Logger struct{ prefix string }
func (l Logger) Log(msg string) { fmt.Printf("[%s] %s\n", l.prefix, msg) }
type Cache struct{ data map[string]any }
func (c *Cache) Get(key string) any { return c.data[key] }
type Service struct {
Logger // embedded: Service HAS a Logger
cache *Cache
}
// Service can directly call s.Log("hello") — promoted method
Q8: What are the SOLID principles? Give a one-line summary of each.¶
Answer:
- S — Single Responsibility: A class should have only one reason to change
- O — Open/Closed: Open for extension, closed for modification
- L — Liskov Substitution: Subtypes must be replaceable for base types
- I — Interface Segregation: Don't force clients to depend on unused methods
- D — Dependency Inversion: Depend on abstractions, not concrete implementations
Q9: What is the difference between method overloading and method overriding?¶
Answer:
| Aspect | Overloading | Overriding |
|---|---|---|
| Definition | Same name, different params | Same name, same params, in subclass |
| Resolution | Compile-time (static) | Runtime (dynamic dispatch) |
| Java | Yes | Yes |
| Python | No (last definition wins) | Yes |
| Go | No | Via interface + different types |
// Overloading (compile-time)
class Math {
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
}
// Overriding (runtime)
class Animal {
void speak() { System.out.println("..."); }
}
class Dog extends Animal {
@Override
void speak() { System.out.println("Woof"); }
}
Animal a = new Dog();
a.speak(); // "Woof" — resolved at runtime
Senior Level Questions¶
Q10: Walk through the Strategy pattern. When would you use it?¶
Answer:
Strategy pattern encapsulates interchangeable algorithms behind a common interface.
Use when: - You have multiple ways to perform an operation - You want to switch algorithms at runtime - You want to avoid if/else chains for selecting behavior
type Compressor interface {
Compress(data []byte) ([]byte, error)
}
type GzipCompressor struct{}
func (g GzipCompressor) Compress(data []byte) ([]byte, error) { /* gzip */ return nil, nil }
type ZstdCompressor struct{}
func (z ZstdCompressor) Compress(data []byte) ([]byte, error) { /* zstd */ return nil, nil }
type NoCompressor struct{}
func (n NoCompressor) Compress(data []byte) ([]byte, error) { return data, nil }
type FileUploader struct {
compressor Compressor
}
func (fu *FileUploader) Upload(data []byte) error {
compressed, err := fu.compressor.Compress(data)
if err != nil { return err }
// upload compressed data...
_ = compressed
return nil
}
// Switch strategy based on file size
func chooseCompressor(fileSize int) Compressor {
if fileSize > 10_000_000 {
return ZstdCompressor{}
} else if fileSize > 1000 {
return GzipCompressor{}
}
return NoCompressor{}
}
Q11: When should you NOT use OOP?¶
Answer:
- Simple scripts/utilities — a 50-line script doesn't need classes
- Pure data transformations — functional programming (map, filter, reduce) is cleaner
- Performance-critical inner loops — virtual dispatch has overhead; use concrete types
- Stateless operations — pure functions are simpler than stateless classes
- When it leads to God objects — one massive class doing everything
- Deep hierarchies — more than 2-3 levels usually signals a design problem
# BAD: Unnecessary OOP
class StringHelper:
@staticmethod
def reverse(s: str) -> str:
return s[::-1]
result = StringHelper.reverse("hello")
# GOOD: Just a function
def reverse_string(s: str) -> str:
return s[::-1]
result = reverse_string("hello")
Q12: Explain dependency injection. Why does it matter?¶
Answer:
DI means passing dependencies to an object instead of creating them internally.
Why it matters: 1. Testability — inject mocks in tests 2. Flexibility — swap implementations without changing code 3. Decoupling — components don't know about each other's internals
// Without DI — hard to test
class OrderService {
private final PostgresDB db = new PostgresDB(); // hardcoded
// Can't test without a real database!
}
// With DI — easy to test
class OrderService {
private final Database db;
public OrderService(Database db) {
this.db = db; // injected from outside
}
}
// In production
new OrderService(new PostgresDB("prod_url"));
// In tests
new OrderService(new InMemoryDB());
Professional Level Questions¶
Q13: How does a vtable work? Explain virtual dispatch.¶
Answer:
A vtable (virtual method table) is an array of function pointers, one per virtual method. Each class has one vtable shared by all instances.
Object layout:
[vtable pointer] → [method1_ptr, method2_ptr, ...]
[field1]
[field2]
...
Call resolution:
obj.method()
→ load obj's vtable pointer
→ index into vtable for method's slot
→ call the function at that address
Cost: one pointer dereference + indirect function call
vs direct call: just a function call (can be inlined)
In Go, interface dispatch uses itab (interface table) instead of vtable. The itab is computed at the point of interface assignment and cached globally.
Q14: Explain Python's MRO. What is C3 linearization?¶
Answer:
MRO (Method Resolution Order) determines the order in which Python searches classes when resolving a method call.
C3 linearization algorithm guarantees: - A class appears before its parents - Parents maintain their declared order - The result is consistent (no contradictions)
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
# MRO: D → B → C → A → object
# NOT D → B → A → C → A (which would visit A twice)
# super() follows MRO, not the parent class:
class B(A):
def method(self):
super().method() # Calls C.method, NOT A.method!
# Because in D's MRO, C comes after B
If C3 cannot produce a valid linearization (contradictory hierarchy), Python raises TypeError at class creation time.
Q15: What is the formal definition of Liskov Substitution Principle?¶
Answer:
If S is a subtype of T, then for every program P that uses objects of type T, the behavior of P is unchanged when objects of type S are used instead.
Behavioral conditions: - Preconditions of S's methods must be equal to or weaker than T's - Postconditions of S's methods must be equal to or stronger than T's - Invariants of T must be preserved by S - No new exceptions that T's clients don't expect
Classic violation: Square extending Rectangle — setting width on a Square also changes height, breaking Rectangle's contract that width and height are independent.
Coding Challenges¶
Challenge 1: Design a Shape Hierarchy¶
Design a shape system with Circle, Rectangle, and Triangle. Each shape must compute area() and perimeter(). Write a function that takes a list of shapes and returns the one with the largest area.
Go:
package main
import (
"fmt"
"math"
)
type Shape interface {
Area() float64
Perimeter() float64
String() string
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}
func (c Circle) String() string {
return fmt.Sprintf("Circle(r=%.2f)", c.Radius)
}
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
func (r Rectangle) String() string {
return fmt.Sprintf("Rectangle(%.2f x %.2f)", r.Width, r.Height)
}
type Triangle struct {
A, B, C float64 // side lengths
}
func (t Triangle) Area() float64 {
s := (t.A + t.B + t.C) / 2
return math.Sqrt(s * (s - t.A) * (s - t.B) * (s - t.C))
}
func (t Triangle) Perimeter() float64 {
return t.A + t.B + t.C
}
func (t Triangle) String() string {
return fmt.Sprintf("Triangle(%.2f, %.2f, %.2f)", t.A, t.B, t.C)
}
func LargestShape(shapes []Shape) Shape {
if len(shapes) == 0 {
return nil
}
largest := shapes[0]
for _, s := range shapes[1:] {
if s.Area() > largest.Area() {
largest = s
}
}
return largest
}
func main() {
shapes := []Shape{
Circle{Radius: 5},
Rectangle{Width: 10, Height: 8},
Triangle{A: 3, B: 4, C: 5},
}
for _, s := range shapes {
fmt.Printf("%s → Area=%.2f, Perimeter=%.2f\n", s, s.Area(), s.Perimeter())
}
largest := LargestShape(shapes)
fmt.Printf("\nLargest: %s (area=%.2f)\n", largest, largest.Area())
}
Java:
import java.util.*;
interface Shape {
double area();
double perimeter();
}
class Circle implements Shape {
private final 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; }
@Override public String toString() { return String.format("Circle(r=%.2f)", radius); }
}
class Rectangle implements Shape {
private final double width, height;
public Rectangle(double w, double h) { this.width = w; this.height = h; }
@Override public double area() { return width * height; }
@Override public double perimeter() { return 2 * (width + height); }
@Override public String toString() { return String.format("Rect(%.2f x %.2f)", width, height); }
}
class Triangle implements Shape {
private final double a, b, c;
public Triangle(double a, double b, double c) { this.a = a; this.b = b; this.c = c; }
@Override
public double area() {
double s = (a + b + c) / 2;
return Math.sqrt(s * (s - a) * (s - b) * (s - c));
}
@Override public double perimeter() { return a + b + c; }
@Override public String toString() { return String.format("Tri(%.2f,%.2f,%.2f)", a, b, c); }
}
public class ShapeDemo {
public static Shape largestShape(List<Shape> shapes) {
return shapes.stream()
.max(Comparator.comparingDouble(Shape::area))
.orElse(null);
}
public static void main(String[] args) {
List<Shape> shapes = List.of(
new Circle(5),
new Rectangle(10, 8),
new Triangle(3, 4, 5)
);
for (Shape s : shapes) {
System.out.printf("%s → Area=%.2f, Perimeter=%.2f%n", s, s.area(), s.perimeter());
}
Shape largest = largestShape(shapes);
System.out.printf("%nLargest: %s (area=%.2f)%n", largest, largest.area());
}
}
Python:
from abc import ABC, abstractmethod
import math
class Shape(ABC):
@abstractmethod
def area(self) -> float: pass
@abstractmethod
def perimeter(self) -> float: pass
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return math.pi * self.radius ** 2
def perimeter(self) -> float:
return 2 * math.pi * self.radius
def __repr__(self) -> str:
return f"Circle(r={self.radius:.2f})"
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width, self.height = width, height
def area(self) -> float:
return self.width * self.height
def perimeter(self) -> float:
return 2 * (self.width + self.height)
def __repr__(self) -> str:
return f"Rect({self.width:.2f} x {self.height:.2f})"
class Triangle(Shape):
def __init__(self, a: float, b: float, c: float):
self.a, self.b, self.c = a, b, c
def area(self) -> float:
s = (self.a + self.b + self.c) / 2
return math.sqrt(s * (s - self.a) * (s - self.b) * (s - self.c))
def perimeter(self) -> float:
return self.a + self.b + self.c
def __repr__(self) -> str:
return f"Tri({self.a:.2f}, {self.b:.2f}, {self.c:.2f})"
def largest_shape(shapes: list[Shape]) -> Shape:
return max(shapes, key=lambda s: s.area())
# Usage
shapes = [Circle(5), Rectangle(10, 8), Triangle(3, 4, 5)]
for s in shapes:
print(f"{s} -> Area={s.area():.2f}, Perimeter={s.perimeter():.2f}")
print(f"\nLargest: {largest_shape(shapes)}")
Challenge 2: Implement the Observer Pattern¶
Build an event system where publishers emit events and subscribers react to them.
Go:
package main
import "fmt"
type EventType string
const (
UserCreated EventType = "user.created"
UserDeleted EventType = "user.deleted"
OrderPlaced EventType = "order.placed"
)
type Event struct {
Type EventType
Data map[string]any
}
type Handler func(Event)
type EventBus struct {
handlers map[EventType][]Handler
}
func NewEventBus() *EventBus {
return &EventBus{handlers: make(map[EventType][]Handler)}
}
func (eb *EventBus) Subscribe(eventType EventType, handler Handler) {
eb.handlers[eventType] = append(eb.handlers[eventType], handler)
}
func (eb *EventBus) Publish(event Event) {
for _, handler := range eb.handlers[event.Type] {
handler(event)
}
}
func main() {
bus := NewEventBus()
// Subscribe handlers
bus.Subscribe(UserCreated, func(e Event) {
fmt.Printf("[LOG] User created: %v\n", e.Data["name"])
})
bus.Subscribe(UserCreated, func(e Event) {
fmt.Printf("[EMAIL] Welcome email to: %v\n", e.Data["email"])
})
bus.Subscribe(OrderPlaced, func(e Event) {
fmt.Printf("[INVENTORY] Processing order: %v\n", e.Data["order_id"])
})
// Publish events
bus.Publish(Event{
Type: UserCreated,
Data: map[string]any{"name": "Alice", "email": "alice@example.com"},
})
bus.Publish(Event{
Type: OrderPlaced,
Data: map[string]any{"order_id": "ORD-001", "total": 99.99},
})
}
Java:
import java.util.*;
import java.util.function.Consumer;
public class EventBus {
private final Map<String, List<Consumer<Map<String, Object>>>> handlers = new HashMap<>();
public void subscribe(String eventType, Consumer<Map<String, Object>> handler) {
handlers.computeIfAbsent(eventType, k -> new ArrayList<>()).add(handler);
}
public void publish(String eventType, Map<String, Object> data) {
List<Consumer<Map<String, Object>>> list = handlers.getOrDefault(eventType, List.of());
for (var handler : list) {
handler.accept(data);
}
}
public static void main(String[] args) {
EventBus bus = new EventBus();
bus.subscribe("user.created", data ->
System.out.println("[LOG] User created: " + data.get("name")));
bus.subscribe("user.created", data ->
System.out.println("[EMAIL] Welcome email to: " + data.get("email")));
bus.publish("user.created", Map.of("name", "Alice", "email", "alice@example.com"));
}
}
Python:
from collections import defaultdict
from typing import Any, Callable
class EventBus:
def __init__(self):
self._handlers: dict[str, list[Callable]] = defaultdict(list)
def subscribe(self, event_type: str, handler: Callable[[dict[str, Any]], None]) -> None:
self._handlers[event_type].append(handler)
def publish(self, event_type: str, data: dict[str, Any]) -> None:
for handler in self._handlers[event_type]:
handler(data)
# Usage
bus = EventBus()
bus.subscribe("user.created", lambda d: print(f"[LOG] User created: {d['name']}"))
bus.subscribe("user.created", lambda d: print(f"[EMAIL] Welcome: {d['email']}"))
bus.subscribe("order.placed", lambda d: print(f"[INVENTORY] Order: {d['order_id']}"))
bus.publish("user.created", {"name": "Alice", "email": "alice@example.com"})
bus.publish("order.placed", {"order_id": "ORD-001", "total": 99.99})
Challenge 3: Implement a Simple DI Container¶
Build a basic dependency injection container that can register and resolve dependencies.
Go:
package main
import (
"fmt"
"reflect"
"sync"
)
type Container struct {
mu sync.RWMutex
providers map[reflect.Type]any
}
func NewContainer() *Container {
return &Container{providers: make(map[reflect.Type]any)}
}
// Register a singleton instance
func Register[T any](c *Container, instance T) {
c.mu.Lock()
defer c.mu.Unlock()
t := reflect.TypeOf((*T)(nil)).Elem()
c.providers[t] = instance
}
// Resolve a registered dependency
func Resolve[T any](c *Container) (T, error) {
c.mu.RLock()
defer c.mu.RUnlock()
t := reflect.TypeOf((*T)(nil)).Elem()
val, ok := c.providers[t]
if !ok {
var zero T
return zero, fmt.Errorf("no provider for type %v", t)
}
return val.(T), nil
}
// Example interfaces and implementations
type Logger interface {
Log(msg string)
}
type ConsoleLogger struct{}
func (cl ConsoleLogger) Log(msg string) {
fmt.Println("[LOG]", msg)
}
type UserRepository interface {
FindByID(id int) string
}
type InMemoryUserRepo struct {
logger Logger
}
func (r *InMemoryUserRepo) FindByID(id int) string {
r.logger.Log(fmt.Sprintf("Finding user %d", id))
return fmt.Sprintf("User-%d", id)
}
func main() {
c := NewContainer()
// Register dependencies
Register[Logger](c, ConsoleLogger{})
// Resolve and wire
logger, _ := Resolve[Logger](c)
repo := &InMemoryUserRepo{logger: logger}
Register[UserRepository](c, repo)
// Use
userRepo, _ := Resolve[UserRepository](c)
fmt.Println(userRepo.FindByID(42))
}
Java:
import java.util.*;
import java.util.function.Supplier;
public class Container {
private final Map<Class<?>, Object> singletons = new HashMap<>();
private final Map<Class<?>, Supplier<?>> factories = new HashMap<>();
public <T> void registerSingleton(Class<T> type, T instance) {
singletons.put(type, instance);
}
public <T> void registerFactory(Class<T> type, Supplier<T> factory) {
factories.put(type, factory);
}
@SuppressWarnings("unchecked")
public <T> T resolve(Class<T> type) {
// Check singletons first
Object singleton = singletons.get(type);
if (singleton != null) return (T) singleton;
// Then check factories
Supplier<?> factory = factories.get(type);
if (factory != null) return (T) factory.get();
throw new RuntimeException("No provider for " + type.getName());
}
public static void main(String[] args) {
Container c = new Container();
// Register
c.registerSingleton(Logger.class, new ConsoleLogger());
c.registerFactory(UserRepository.class, () -> {
Logger logger = c.resolve(Logger.class);
return new InMemoryUserRepo(logger);
});
// Resolve and use
UserRepository repo = c.resolve(UserRepository.class);
System.out.println(repo.findById(42));
}
}
interface Logger {
void log(String msg);
}
class ConsoleLogger implements Logger {
public void log(String msg) { System.out.println("[LOG] " + msg); }
}
interface UserRepository {
String findById(int id);
}
class InMemoryUserRepo implements UserRepository {
private final Logger logger;
InMemoryUserRepo(Logger logger) { this.logger = logger; }
public String findById(int id) {
logger.log("Finding user " + id);
return "User-" + id;
}
}
Python:
from typing import Any, Callable, TypeVar, Type
T = TypeVar("T")
class Container:
def __init__(self):
self._singletons: dict[type, Any] = {}
self._factories: dict[type, Callable] = {}
def register_singleton(self, type_: type, instance: Any) -> None:
self._singletons[type_] = instance
def register_factory(self, type_: type, factory: Callable) -> None:
self._factories[type_] = factory
def resolve(self, type_: Type[T]) -> T:
if type_ in self._singletons:
return self._singletons[type_]
if type_ in self._factories:
return self._factories[type_]()
raise KeyError(f"No provider for {type_.__name__}")
# Example
from abc import ABC, abstractmethod
class Logger(ABC):
@abstractmethod
def log(self, msg: str) -> None: pass
class ConsoleLogger(Logger):
def log(self, msg: str) -> None:
print(f"[LOG] {msg}")
class UserRepository(ABC):
@abstractmethod
def find_by_id(self, user_id: int) -> str: pass
class InMemoryUserRepo(UserRepository):
def __init__(self, logger: Logger):
self.logger = logger
def find_by_id(self, user_id: int) -> str:
self.logger.log(f"Finding user {user_id}")
return f"User-{user_id}"
# Usage
container = Container()
container.register_singleton(Logger, ConsoleLogger())
container.register_factory(
UserRepository,
lambda: InMemoryUserRepo(container.resolve(Logger))
)
repo = container.resolve(UserRepository)
print(repo.find_by_id(42))
# [LOG] Finding user 42
# User-42
Interview Tips¶
- Start with the concept, then show code — interviewers want to see understanding, not just syntax
- Use the right language for the context — Go for simplicity, Java for classical OOP, Python for rapid prototyping
- Mention trade-offs — every design decision has pros and cons
- Draw diagrams — class diagrams, sequence diagrams help explain patterns
- Ask clarifying questions — "Should this support multiple implementations?" signals senior thinking
- Know when NOT to use OOP — this separates seniors from juniors