Object-Oriented Programming
Object-Oriented Programming is a paradigm that organizes computation around objects — bundled data and behavior — enabling modularity, reuse, and expressive modeling of complex systems.
Overview
Object-Oriented Programming (OOP) emerged in the 1960s and 70s as programmers grappled with a fundamental problem: as software grew larger, it became incomprehensible. The procedural style — lists of instructions acting on shared data — produced "spaghetti code" that was nearly impossible to reason about, extend, or debug. OOP proposed a different mental model: organize your program as a society of autonomous agents (objects), each responsible for its own data and capable of receiving messages from others.
The insight is almost anthropological. The real world is full of entities — bank accounts, employees, geometric shapes, network connections — that have both state (what they are) and behavior (what they can do). OOP lets you model these entities directly in code, so the structure of your program mirrors the structure of the problem. This is why OOP became the dominant paradigm for large-scale software: it maps cleanly onto how humans already think about the world.
But OOP is not just about syntax (class, extends, this). At its core it is a set of design principles: encapsulation (hide complexity behind interfaces), inheritance (share structure across related types), and polymorphism (treat different things uniformly when they share a common role). Mastering OOP means learning when and how to apply these principles — and critically, when not to. The most dangerous half-knowledge in software engineering is inheritance misused; the most powerful tool is polymorphism properly understood.
This module covers OOP at the depth of MIT 6.009 / Stanford CS106B and beyond: from first principles through dynamic dispatch, vtables, composition versus inheritance, and the historical thread connecting Simula, Smalltalk, C++, Java, and Python. By the end you will be able to design class hierarchies, reason about virtual dispatch in memory, and explain why Alan Kay — who coined the term "object-oriented" — said the real revolution was message passing, not inheritance.
OOP underpins virtually every major software system you interact with: operating system GUI toolkits (every widget is an object), game engines (every entity in a scene graph), web frameworks (every HTTP request handler), compilers (every AST node), and databases (every query plan node). The Java standard library alone contains thousands of classes, and understanding its design requires fluency in OOP principles. Even languages that are not purely object-oriented — Python, JavaScript, C++ — have rich OOP support that their ecosystems use pervasively. You cannot effectively read or write production code in any of these languages without deeply understanding classes, inheritance, and polymorphism.
Beyond practical utility, OOP is intellectually significant because it is a theory of abstraction and modularity. The same principles that make a well-designed class hierarchy readable also appear in software architecture (microservices), database schema design, and even mathematics (category theory's morphisms echo OOP's message passing). Understanding OOP deeply — not just its syntax but its philosophy — trains a mode of thought that transfers across every complex system you will ever build. It also sharpens your critical thinking: once you understand why deep inheritance hierarchies become brittle, and why the Gang of Four Design Patterns exist to escape them, you gain a sophisticated, mature view of software design that separates engineers who can build systems from those who can merely write code.
Virtual Dispatch via vtable: How Polymorphism Works in Memory
Each object carries a hidden vptr to its class's vtable. Calling animal.speak() follows the vptr, reads slot 0, and calls the right implementation — this is dynamic dispatch. Animal::describe() is shared, but when it calls self.speak(), that call is also dispatched through the vtable, achieving polymorphism even from within a base-class method.
The concepts
01Objects and the Object Model
An object is a value that bundles together state (data, called fields or instance variables) and behavior (functions, called methods). You can think of an object like a vending machine: it has internal state (inventory, current balance) that you cannot directly touch, and it exposes a panel of buttons (methods) that are the only way to interact with it.
In memory, an object is typically a contiguous block of bytes. Its fields are stored at fixed offsets within that block. Its methods are not stored per-object — that would waste enormous memory — but rather in a shared table (the class), with each object holding a pointer to that table.
The object model asks: what is the smallest thing that can be an object? In Smalltalk, everything is an object — integers, booleans, even classes themselves. In Java, primitive types (int, double) are not objects; they exist outside the object system for performance. Python is close to Smalltalk: classes are objects, functions are objects, modules are objects. Understanding your language's object model is essential for reasoning about identity, equality, and memory.
# Python: every value is an object
x = 42
print(type(x)) # <class 'int'>
print(x.__class__) # <class 'int'>
print(isinstance(x, object)) # True — int inherits from object
# A simple object
class Point:
def __init__(self, x, y):
self.x = x # instance variables stored in the object
self.y = y
def distance_to_origin(self):
return (self.x**2 + self.y**2) ** 0.5
p = Point(3, 4)
print(p.distance_to_origin()) # 5.0
print(id(p)) # memory address of the object
02Classes: Blueprints for Objects
A class is a template or blueprint that describes the structure (fields) and behavior (methods) of all objects of a given type. An instance is one concrete object created from that blueprint. The relationship is exactly like an architectural drawing (class) versus a physical building (instance): you can construct many buildings from one drawing, each occupying its own location and potentially having a different interior state.
The process of creating an instance from a class is called instantiation. In most languages this is done with a constructor — a special method (__init__ in Python, the method with the class name in Java/C++) — that initializes the new object's fields.
Classes are also namespaces: Point.distance_to_origin refers to the method on the class, while p.distance_to_origin() calls it on a specific instance. The self (Python) or this (Java/C++) parameter is how a method gets a reference back to the specific instance it was called on — without it, there would be no way to know which point's coordinates to use.
Class variables (shared across all instances) versus instance variables (per-object) is a crucial distinction. Class variables are stored on the class object; instance variables are stored on each individual object.
class BankAccount:
bank_name = "First National" # class variable — shared by all accounts
def __init__(self, owner, balance=0):
self.owner = owner # instance variable
self._balance = balance # instance variable (convention: _ = internal)
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit must be positive")
self._balance += amount
def withdraw(self, amount):
if amount > self._balance:
raise ValueError("Insufficient funds")
self._balance -= amount
def balance(self):
return self._balance
a1 = BankAccount("Alice", 1000)
a2 = BankAccount("Bob", 500)
a1.deposit(200)
print(a1.balance()) # 1200
print(a2.balance()) # 500 — independent state
print(BankAccount.bank_name) # "First National"
03Encapsulation: Hiding the Machine
Encapsulation is the principle that an object's internal state should be hidden from the outside world, accessible only through a defined interface (the set of public methods). This creates a boundary — a contract — between the object's internals and everything else.
Why does this matter? Consider the BankAccount class. If you expose _balance directly, any code anywhere in a large system can set it to a negative number, bypassing all your validation. Encapsulation prevents this. The class becomes the single point of truth for its own invariants.
Encapsulation also enables evolution. If you later decide to store the balance in cents (integers) rather than dollars (floats) for precision, you can change the internal representation without breaking any code that uses the public interface — because that code never accessed _balance directly.
Languages enforce encapsulation differently: Java and C++ have private/protected/public access modifiers enforced by the compiler. Python uses a naming convention (_name = internal, __name = name-mangled) but does not technically prevent access. The principle is the same: callers should depend only on the public interface.
Information hiding (Parnas, 1972) is the deeper design principle: hide design decisions that are likely to change. The implementation is the secret; the interface is the promise.
// Java: compiler-enforced encapsulation
public class BankAccount {
private double balance; // only this class can access
private String owner;
public BankAccount(String owner, double initial) {
this.owner = owner;
this.balance = initial;
}
// Public interface: the only legal operations
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException();
balance += amount;
}
public double getBalance() { return balance; }
// --- in caller code ---
// account.balance = -999; // COMPILE ERROR: balance is private
// account.deposit(-999); // RuntimeException: caught by validation
}
04Inheritance: Specializing Behavior
Inheritance allows a class (the subclass or derived class) to acquire all the fields and methods of another class (the superclass or base class), and then extend or override them. The relationship is is-a: a SavingsAccount is a BankAccount, so it should inherit all bank account behavior and add savings-specific behavior (e.g., interest accrual).
Inheritance serves two related goals:
- Code reuse: avoid duplicating the
deposit/withdrawlogic in every account subclass. - Subtype polymorphism: code that expects a
BankAccountcan also work with aSavingsAccount(more on this next).
The inheritance chain forms a hierarchy. In single inheritance (Java, Python by default), each class has at most one direct parent. C++ and Python allow multiple inheritance — a class can inherit from several parents — which introduces complexity (the diamond problem).
A crucial warning: inheritance is a strong coupling mechanism. Subclasses depend deeply on their superclass internals, creating fragile code when superclasses change. The famous advice "favor composition over inheritance" (discussed below) exists precisely because inheritance is misused far more often than it is appropriate. Use inheritance only for genuine is-a relationships, not for code reuse alone.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
raise NotImplementedError("Subclasses must implement speak()")
def describe(self):
return f"I am {self.name} and I say: {self.speak()}"
class Dog(Animal): # Dog IS-A Animal
def speak(self):
return "Woof!"
class Cat(Animal): # Cat IS-A Animal
def speak(self):
return "Meow!"
class Duck(Animal):
def speak(self):
return "Quack!"
d = Dog("Rex")
print(d.describe()) # "I am Rex and I say: Woof!"
# describe() is inherited from Animal — not duplicated
# speak() is overridden in each subclass
05Polymorphism and Dynamic Dispatch
Polymorphism means "many forms": the ability to treat objects of different types uniformly through a common interface. When you call animal.speak() on a list of Animal objects — some Dogs, some Cats, some Ducks — each object responds to the same message with its own behavior. The caller does not need to know or check what type each object is.
The mechanism that makes runtime polymorphism work is dynamic dispatch (also called late binding or virtual dispatch). At compile time, the compiler knows only that animal is of type Animal. At runtime, the actual type of the object is consulted to find the right speak() implementation to call. This lookup is the core mechanism of OOP.
In C++, dynamic dispatch requires explicitly marking methods virtual. In Java and Python, all method calls are dynamically dispatched by default (except for final/static methods in Java).
There are two major kinds of polymorphism:
- Subtype polymorphism (runtime): what we described above — a subtype can be used wherever a supertype is expected.
- Parametric polymorphism (compile-time generics):
List<T>works for any typeT. This is a separate but related idea.
Dynamic dispatch is why OOP achieves the Open/Closed Principle: code is open for extension (add a new Animal subclass) but closed for modification (never touch describe() in Animal).
# Polymorphism in action: same code, different behavior
animals = [Dog("Rex"), Cat("Whiskers"), Duck("Donald")]
for animal in animals:
print(animal.describe()) # describe() calls speak() — dynamically dispatched
# Output:
# I am Rex and I say: Woof!
# I am Whiskers and I say: Meow!
# I am Donald and I say: Quack!
# No isinstance() checks needed — that would be an anti-pattern
# called "type switching" and defeats the purpose of polymorphism
# Java example: the compile-time type is Animal, runtime type varies
// Animal a = new Dog("Rex"); // legal because Dog IS-A Animal
// a.speak(); // calls Dog.speak() at runtime — dynamic dispatch
06Virtual Tables (vtables): How Dynamic Dispatch Works in Memory
How does the runtime actually know which speak() to call? The standard implementation (used by C++, Java JVM internals, many others) is the virtual function table (vtable).
Every class has a vtable: an array of function pointers, one per virtual method. Every object contains a hidden pointer (the vptr) pointing to its class's vtable. When you call animal->speak(), the runtime follows the vptr to the vtable, looks up the entry for speak, and calls the function pointed to there.
For Dog, the vtable entry for speak points to Dog::speak. For Cat, it points to Cat::speak. They share the same slot index (since speak is declared in Animal), so the dispatch code is simply: (*vptr[SPEAK_INDEX])(this).
This mechanism has constant-time overhead (one memory indirection) regardless of how deep the inheritance hierarchy is. It is extremely efficient — the main cost is one extra pointer per object and one extra pointer dereference per virtual call, which the CPU's branch predictor often hides.
Inlining and devirtualization: when the compiler can prove at compile time which concrete type an object has, it can replace the vtable lookup with a direct call — eliminating the overhead entirely. Modern C++ and JVM JIT compilers do this aggressively.
// C++: explicit virtual keyword triggers vtable mechanism
#include <iostream>
#include <string>
using namespace std;
struct Animal {
virtual string speak() = 0; // pure virtual — abstract method
virtual ~Animal() {} // always virtual destructor in base classes
};
struct Dog : Animal {
string speak() override { return "Woof!"; }
};
struct Cat : Animal {
string speak() override { return "Meow!"; }
};
// Memory layout of a Dog object:
// [ vptr → Dog_vtable ] (hidden, compiler-inserted)
// [ ...Dog fields... ]
// Dog_vtable = [ &Dog::speak, &~Dog ]
int main() {
Animal* a = new Dog(); // vptr → Dog_vtable
cout << a->speak(); // follows vptr, finds Dog::speak
delete a;
}
07Abstract Classes and Interfaces
An abstract class is a class that cannot be instantiated directly — it exists only to be subclassed. It typically declares one or more abstract methods: method signatures with no implementation, serving as a contract that all subclasses must fulfill. In C++, a pure virtual method (virtual void speak() = 0) makes the class abstract. In Java, the abstract keyword does the same. In Python, the abc module enforces this.
An interface (Java/C#) is like an abstract class with only abstract methods and no fields — a pure contract with no implementation at all. A class can implement many interfaces but (in Java) extend only one class. This is the escape hatch from the restrictions of single inheritance.
The design principle here is programming to an interface, not an implementation. If your code depends on List (the interface) rather than ArrayList (a concrete class), you can substitute any list implementation without changing the caller. This is the foundation of testability, dependency injection, and loose coupling.
Interfaces are also how you express that two unrelated classes share a capability. A Dog and a Robot might both be Speakable without any other relationship. The interface captures the shared capability without forcing a contrived inheritance relationship.
from abc import ABC, abstractmethod
class Shape(ABC):
"""Abstract base class — cannot be instantiated directly."""
@abstractmethod
def area(self) -> float:
"""Must be implemented by every concrete subclass."""
...
@abstractmethod
def perimeter(self) -> float: ...
def describe(self):
# Concrete method — uses abstract methods it does not implement
return f"Area={self.area():.2f}, Perimeter={self.perimeter():.2f}"
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return 3.14159 * self.r ** 2
def perimeter(self): return 2 * 3.14159 * self.r
class Rectangle(Shape):
def __init__(self, w, h): self.w, self.h = w, h
def area(self): return self.w * self.h
def perimeter(self): return 2 * (self.w + self.h)
# Shape() # TypeError: Can't instantiate abstract class
print(Circle(5).describe()) # Area=78.54, Perimeter=31.42
print(Rectangle(3,4).describe()) # Area=12.00, Perimeter=14.00
08Composition Over Inheritance
The mantra "favor composition over inheritance" (Gang of Four, 1994) is one of the most important design principles in software engineering. It says: instead of building behavior by subclassing, build it by assembling objects.
Inheritance creates a rigid is-a hierarchy at design time. If you need a FlyingFish (both Fish and Bird), single-inheritance languages force you to pick one parent. Worse, any change to the Animal base class — adding a field, changing a method — ripples down to all subclasses, creating fragile coupling known as the Fragile Base Class Problem.
Composition assembles behavior by giving an object references to other objects that provide the needed capabilities. A Duck does not inherit from both Swimmer and Flyer; it contains a SwimmingBehavior and a FlyingBehavior. You can swap behaviors at runtime, mix and match, and change any behavior without affecting others.
The pattern: define behavior as interfaces, provide concrete implementations of those interfaces, and inject the right implementation into each object. This is the foundation of the Strategy pattern, Dependency Injection, and most modern software architecture.
A practical rule: if you find yourself writing class B(A) primarily to reuse code from A rather than to express a genuine is-a relationship, stop and use composition instead.
# Composition: behaviors are injected objects, not inherited
from abc import ABC, abstractmethod
class FlyBehavior(ABC):
@abstractmethod
def fly(self): ...
class SwimBehavior(ABC):
@abstractmethod
def swim(self): ...
class FlyWithWings(FlyBehavior):
def fly(self): return "flap flap"
class NoFly(FlyBehavior):
def fly(self): return "cannot fly"
class SwimInWater(SwimBehavior):
def swim(self): return "splash splash"
class Duck:
def __init__(self, fly_behavior: FlyBehavior,
swim_behavior: SwimBehavior):
self._fly = fly_behavior # HAS-A, not IS-A
self._swim = swim_behavior
def perform_fly(self): return self._fly.fly()
def perform_swim(self): return self._swim.swim()
mallard = Duck(FlyWithWings(), SwimInWater())
rubber = Duck(NoFly(), SwimInWater())
print(mallard.perform_fly()) # flap flap
print(rubber.perform_fly()) # cannot fly
# Swap behavior at runtime:
mallard._fly = NoFly()
print(mallard.perform_fly()) # cannot fly
09Message Passing: Alan Kay's Original Vision
Alan Kay — who invented Smalltalk and coined the term "object-oriented programming" — later said that the key idea was message passing, not classes or inheritance. In Smalltalk's original model, objects are autonomous agents. You do not call a method; you send a message to an object, and the object decides how to respond.
This is a subtle but profound distinction. In a traditional method call, the caller knows the concrete method that will execute. In message passing, the caller only specifies the message name and arguments; the receiver decides the response. Objects are truly encapsulated: the caller cannot even know whether the message triggers code, a database lookup, or a remote procedure call.
This model anticipates modern distributed systems: an HTTP request is a message, a microservice is an object. The message-passing view maps directly onto Actor models (Erlang, Akka), where processes communicate only by sending messages and share no state — making concurrent systems dramatically easier to reason about.
In practical OOP languages, method dispatch approximates message passing, but most languages still expose too much of the concrete type. The Liskov Substitution Principle (LSP) formalizes the ideal: if S is a subtype of T, then objects of type T may be replaced with objects of type S without altering the correctness of the program. Violation of LSP is the most common sign that inheritance is being misused.
# Smalltalk-inspired message passing style in Python
# The sender knows only the message name, not the receiver's class
class Printer:
def render(self, content): print(f"[PRINT] {content}")
class EmailSender:
def render(self, content): print(f"[EMAIL] Sending: {content}")
class Logger:
def render(self, content): print(f"[LOG {__import__('datetime').date.today()}] {content}")
def publish(channel, message):
"""Sends the 'render' message — doesn't know or care what channel is."""
channel.render(message) # message passing: channel decides
publish(Printer(), "Hello World")
publish(EmailSender(), "Hello World")
publish(Logger(), "Hello World")
# All three respond to the same message differently
# Note: Python uses duck typing — no common base class required
10The SOLID Principles
The SOLID principles (Robert Martin, 2000s) are five design guidelines that characterize well-structured OOP code. They are not rules but heuristics — their violation is a smell, their application a judgment call.
S — Single Responsibility Principle: A class should have one reason to change. A class that handles both data validation and database persistence and email notification is doing three jobs; split it into three classes.
O — Open/Closed Principle: Open for extension, closed for modification. Add new behavior by adding new classes (subclasses, implementations), not by editing existing ones. Dynamic dispatch enables this directly.
L — Liskov Substitution Principle: Subclasses must be substitutable for their base class. A Square subclassing Rectangle is the classic violation: setting width on a square also changes height, breaking the Rectangle contract.
I — Interface Segregation Principle: Prefer many small, specific interfaces over one large, general one. Do not force a class to implement methods it does not need.
D — Dependency Inversion Principle: High-level modules should not depend on low-level modules; both should depend on abstractions. Your business logic should depend on a Repository interface, not on MySQLDatabase directly. This makes testing and technology changes easy.
# Liskov Substitution Principle violation: Square IS-A Rectangle?
class Rectangle:
def __init__(self, w, h):
self.width = w
self.height = h
def area(self): return self.width * self.height
class Square(Rectangle): # PROBLEM: violates LSP
def __init__(self, side):
super().__init__(side, side)
# If caller does: shape.width = 5, height stays 4 for Rectangle
# but for Square that should also change height — breaking Rectangle's contract
def resize_and_check(rect: Rectangle):
rect.width = 5
rect.height = 4
assert rect.area() == 20 # True for Rectangle, may be False for Square!
r = Rectangle(3, 3)
resize_and_check(r) # passes
s = Square(3)
resize_and_check(s) # MAY FAIL — Square is NOT safely substitutable
# Fix: Square and Rectangle should be siblings, not parent-child
11Design Patterns: Reusable OOP Solutions
Design patterns (Gang of Four: Gamma, Helm, Johnson, Vlissides, 1994) are named, reusable solutions to recurring OOP design problems. They are not code you copy-paste; they are vocabularies for design conversations and blueprints for common scenarios.
Three canonical categories:
Creational patterns abstract the process of object creation:
- Factory Method: define an interface for creating an object, but let subclasses decide which class to instantiate.
- Singleton: ensure a class has only one instance (use sparingly — often a design smell).
Structural patterns compose objects and classes into larger structures:
- Decorator: wrap an object to add behavior without subclassing. Python's
@decoratorsyntax is inspired by this. - Adapter: convert one interface to another (the "plug adapter" of software).
Behavioral patterns define communication between objects:
- Observer: objects subscribe to events from a subject and are notified of changes. Every UI event system, every reactive framework, uses Observer.
- Strategy: encapsulate an algorithm inside an object, making it swappable at runtime (this is exactly the composition-over-inheritance example above).
- Iterator: provide a uniform way to traverse a collection without exposing its representation. Python's
forloop protocol is built on this pattern.
Patterns reveal a meta-insight: most complexity in OOP code arises from the tension between reuse and change. Patterns are the established ways to resolve that tension.
# Observer pattern: classic GUI/event use case
from typing import List
class EventSource:
"""Subject: holds state, notifies observers on change."""
def __init__(self):
self._observers: List['Observer'] = []
self._value = None
def subscribe(self, obs): self._observers.append(obs)
def set_value(self, v):
self._value = v
for obs in self._observers: # notify all
obs.update(v)
class Observer:
def update(self, value): ...
class Logger(Observer):
def update(self, v): print(f"LOG: value changed to {v}")
class Display(Observer):
def update(self, v): print(f"DISPLAY: showing {v}")
source = EventSource()
source.subscribe(Logger())
source.subscribe(Display())
source.set_value(42)
# LOG: value changed to 42
# DISPLAY: showing 42
Key insights
- ✦Objects bundle state and behavior together; the object model (what counts as an object, how dispatch works) is the most foundational concept in OOP, and it varies significantly between languages.
- ✦Encapsulation is not just syntax privacy — it is the design principle that a class owns its invariants and is the sole authority over its internal state. This is what makes large codebases maintainable.
- ✦Dynamic dispatch is what makes OOP's extensibility possible: the Open/Closed Principle — add behavior without modifying existing code — is only achievable because method calls are resolved at runtime based on actual object type.
- ✦The vtable mechanism achieves polymorphism with O(1) overhead: one pointer dereference per virtual call, regardless of hierarchy depth.
- ✦Inheritance expresses IS-A; composition expresses HAS-A. Most inheritance in real codebases should be composition. The test: would replacing the subclass with the superclass in all caller code still be correct (LSP)?
- ✦Alan Kay's original OOP vision was about message passing and autonomous agents — an idea that resurfaces in distributed systems, actors, and microservices. Classes and inheritance were implementation details, not the core idea.
- ✦The SOLID principles are heuristics for designing classes that are independently testable, independently deployable, and resilient to change — they encode decades of hard-won software engineering experience.
- ✦Design patterns are the vocabulary of OOP design: named solutions to recurring tensions between reuse and change. Knowing them lets you recognize and communicate about structure in code instantly.
Common pitfalls
- ⚠Treating inheritance as a code-reuse mechanism rather than a type-relationship mechanism. If the only reason for `class B(A)` is to avoid repeating methods, use composition instead — inheritance creates permanent coupling to the superclass.
- ⚠Violating the Liskov Substitution Principle by making a subclass that cannot safely substitute its superclass (the Square/Rectangle problem). This causes subtle bugs that only manifest when code uses the base type polymorphically.
- ⚠Forgetting to mark destructors `virtual` in C++ base classes, causing undefined behavior (partial destruction) when deleting a derived object through a base pointer.
- ⚠Deep inheritance hierarchies (more than 2-3 levels) — these become nearly impossible to reason about, because understanding any class requires reading the entire ancestor chain.
- ⚠Type-switching with `isinstance()` or `if type(x) == Dog` in code that handles multiple subtypes. This defeats polymorphism and must be updated every time a new subclass is added — the exact problem OOP is designed to solve.
- ⚠Confusing class variables (shared by all instances) with instance variables (per-object), especially in Python where a mutable class variable like a list is shared and mutated by all instances.
- ⚠Making everything public to avoid designing an interface, then depending on implementation details from outside the class — losing all the benefits of encapsulation and making future refactoring painful.
- ⚠Overusing the Singleton pattern. While it seems convenient, Singletons introduce hidden global state and make code extremely difficult to test in isolation.
Practice
- 01easy
Implement a `Shape` hierarchy with `Circle`, `Rectangle`, and `Triangle` as concrete subclasses of an abstract `Shape` base class. Each shape must implement `area()` and `perimeter()`. Write a function `total_area(shapes: list[Shape]) -> float` that sums areas without any `isinstance` checks. Then add a `RegularPolygon` subclass — verify that `total_area` works without any modification.
- 02medium
Diagnose the Liskov Substitution Principle violation: implement `Rectangle` with `set_width` and `set_height` methods, then make `Square` a subclass. Write a test function `assert_area_after_resize(r: Rectangle)` that sets width=5, height=4 and asserts area==20. Show that it passes for Rectangle but fails for Square. Then redesign using composition or a sibling hierarchy so that the test concept is no longer violated.
- 03medium
Implement a simple expression tree interpreter using OOP. Define an abstract `Expr` class with an `eval(env: dict) -> int` method. Implement: `Num(value)`, `Var(name)`, `Add(left, right)`, `Mul(left, right)`, `Let(var, val_expr, body_expr)`. The `Let` node binds a variable in its body scope. Show that `Let('x', Num(5), Add(Var('x'), Num(3))).eval({})` returns 8. This is how real interpreters work.
- 04medium
Implement the Observer pattern from scratch. Create a `Stock` class with a price attribute. When the price changes, it should automatically notify registered observers. Implement two observers: `PriceAlertObserver` (prints an alert if price rises above a threshold) and `HistoryObserver` (records all prices in a list). Write a simulation that changes the stock price several times and verify both observers respond correctly.
- 05hard
Explain and demonstrate the Fragile Base Class Problem. Create a base class `Counter` with `increment(n)` that calls `add(1)` n times, and `add(x)` that increments by x and tracks total. Now subclass it as `LoggingCounter` that overrides `add` to also log. Show that `increment(3)` on a `LoggingCounter` logs 3 times — but also show how a seemingly innocent change to the base class (having `increment` call `add` directly with n instead of looping) would silently break the subclass. Propose a fix.
- 06hard
Design and implement a plugin system using the composition and interface patterns. Define a `TextProcessor` that accepts a list of `Transform` objects (each has a `process(text: str) -> str` method). Implement at least: `UppercaseTransform`, `StripWhitespaceTransform`, `CensorTransform(bad_words: list)`. Show that processors can be assembled at runtime from any combination of transforms. Then implement `PipelineTransform` — a transform that is itself composed of multiple transforms — demonstrating the Composite pattern.
Resources
Chapter 3 covers state and OOP-style design. The canonical MIT CS text. Free online from MIT Press.
MIT's intro course covering Python OOP deeply. Problem sets are excellent.
The official Python tutorial chapter on classes — accurate, concise, covers MRO and descriptors.
Stanford's data structures and abstractions course, covering OOP in C++.
Alan Kay's own account of the ideas behind Smalltalk and the original OOP vision. Essential intellectual history.
Clear explanation of vtable internals with memory layout diagrams.
Checkpoints — you can now…
- ✓You can implement a class with private fields, a constructor, and public methods — and explain why the fields should be private.
- ✓You can write a 3-level inheritance hierarchy, override a method in a subclass, and call the parent's version with super().
- ✓You can explain dynamic dispatch and draw the vtable lookup sequence for a virtual method call on an object whose compile-time type differs from its runtime type.
- ✓You can identify a Liskov Substitution Principle violation in a given class hierarchy and propose a correct redesign.
- ✓You can rewrite an inheritance-based design as a composition-based design, and articulate why the composition version is more flexible.
- ✓You can implement the Observer pattern from scratch and identify the roles (Subject, Observer, ConcreteObserver).
- ✓You can name all five SOLID principles, give a one-sentence description of each, and give a concrete code example that violates one of them.