Object-Oriented Programming (OOP)

Object-oriented programming models a system using objects that combine state and behavior.

4.1 Core idea

Object-oriented programming models a system using objects that combine state and behavior. Common OOP features include encapsulation, abstraction, inheritance, and polymorphism. Different languages implement these concepts differently.

4.2 Classes and objects

Output (CPython 3.13.1): A class defines behavior and object construction; account refers to an instance. Python's leading underscore is a convention indicating non-public use, not a strict access-control mechanism.

class BankAccount:
    def __init__(self, owner: str, opening_balance: float = 0.0):
        if opening_balance < 0:
            raise ValueError("Opening balance cannot be negative")
        self.owner = owner
        self._balance = opening_balance

    def deposit(self, amount: float) -> None:
        if amount <= 0:
            raise ValueError("Deposit must be positive")
        self._balance += amount

    def get_balance(self) -> float:
        return self._balance

account = BankAccount("Mina", 100.0)
account.deposit(25.0)
print(account.get_balance())  # 125.0

4.3 Encapsulation

Encapsulation protects an object's invariants by controlling how its state is accessed or changed. In languages with access modifiers, use private, protected, or module-level visibility intentionally. Invariants should be enforced at the object's public boundary.

A bank account should not permit arbitrary callers to set its balance to an invalid value. Encapsulation makes invalid states harder to create.

4.4 Abstraction

Abstraction exposes what a component does while hiding implementation details.

The caller depends on the contract, not a specific payment provider implementation. The interface should define error cases and semantics, not just method names.

interface PaymentProcessor {
  charge(amountInCents: number): Promise<PaymentResult>;
}

4.5 Inheritance

Inheritance represents an “is-a” relationship when the subtype can honor the base type's behavioral contract.

Inheritance can support reuse and polymorphism, but deep hierarchies can become brittle. Prefer composition when behavior is assembled from independent parts rather than true substitutable subtypes.

class Shape:
    def area(self) -> float:
        raise NotImplementedError

class Rectangle(Shape):
    def __init__(self, width: float, height: float):
        self.width = width
        self.height = height

    def area(self) -> float:
        return self.width * self.height

4.6 Polymorphism

Polymorphism allows different types to be used through a common contract.

A rectangle can be passed because it supports the expected behavior. Python uses dynamic dispatch; statically typed languages may use interfaces, traits, virtual methods, or generic constraints.

def print_area(shape: Shape) -> None:
    print(shape.area())

4.7 Composition

Composition builds behavior by combining components.

The service receives a logger rather than inheriting from a concrete logging class. This makes the dependency replaceable and easier to test. In production, log fields should be structured and should avoid unnecessary personal information.

Inheritance versus composition Text equivalent: Rectangle inherits from Shape because a rectangle can honour every promise a shape makes (an "is-a" relationship). EnrollmentService *has* a Logger supplied from outside (composition), so any object with an info method can be substituted — for example, a fake logger in tests.

type Logger = {
  info(message: string): void;
};

class EnrollmentService {
  constructor(private readonly logger: Logger) {}

  enroll(studentId: string, courseId: string): void {
    this.logger.info(`Enrollment requested: ${studentId}/${courseId}`);
    // Real implementation would validate and persist enrollment.
  }
}
classDiagram
  class Shape {
    +area() float
  }
  class Rectangle {
    +width
    +height
    +area() float
  }
  Shape <|-- Rectangle : is-a (inheritance)
  class EnrollmentService {
    -logger Logger
    +enroll(studentId, courseId)
  }
  class Logger {
    <<interface>>
    +info(message)
  }
  EnrollmentService o-- Logger : has-a (composition)

4.8 SOLID principles

S — Single Responsibility: a module should have one coherent reason to change.

O — Open/Closed: extend behavior without repeatedly modifying stable core logic where practical.

L — Liskov Substitution: subtypes should honor the behavioral expectations of their base type.

I — Interface Segregation: prefer small, focused interfaces over one broad interface.

D — Dependency Inversion: high-level policy should depend on abstractions, not fragile implementation details.

These are design heuristics, not mechanical laws. Over-abstracting a small program can make it harder to understand.

4.9 Other OOP concepts

Constructors and object initialization Destructors/finalizers and deterministic resource cleanup Static/class members Abstract classes and interfaces Method overriding and overloading Object identity versus value equality Mixins and traits Multiple inheritance and its ambiguity Prototype-based object systems

Design patterns: Strategy, Factory, Adapter, Observer, Decorator, Command, State, Repository

Object-relational mapping and the object-relational impedance mismatch

4.10 Prototype-based programming

JavaScript objects can delegate property lookup through a prototype chain.

Output (Node.js 24.18): This demonstrates delegation through prototypes. JavaScript class syntax provides a class-oriented interface over prototype-based behavior.

const animal = {
  speak() {
    return "sound";
  }
};

const dog = Object.create(animal);
dog.speak = function () {
  return "bark";
};

console.log(dog.speak()); // bark

4.11 When OOP fits

OOP is often useful when the domain contains entities with lifecycle, identity, and behavior; when implementations need interchangeable interfaces; or when boundaries are naturally represented by components. It is less compelling when a deep object graph obscures simple transformations or when mutable objects make state transitions difficult to track.

Exercise

Model a library system using both inheritance and composition. Explain which design better represents Book, Ebook, AudioBook, Catalog, and BorrowingPolicy, and why.

Quiz

? Which principle says a subtype must honour the behavioural expectations of its base type?
- Single Responsibility :: That is about having one reason to change.
- Open/Closed :: That is about extending without modifying stable code.
+ Liskov Substitution :: Subtypes must be usable wherever the base type is expected.
- Dependency Inversion :: That is about depending on abstractions.

? Why does `EnrollmentService` receive a `Logger` instead of inheriting from a logging class?
+ The dependency becomes replaceable and easier to test (composition). :: A fake logger can be passed in tests.
- Inheritance is not allowed in TypeScript. :: TypeScript supports class inheritance.
- Composition is always faster. :: Speed is not the reason.
- Loggers cannot be subclassed. :: They can; it would just couple the service to one implementation.

? What does `Object.create(animal)` followed by overriding `dog.speak` demonstrate in JavaScript?
- Class inheritance only. :: No class is used.
+ Delegation through the prototype chain; `dog.speak()` returned "bark" in a real Node.js run. :: Own properties shadow those found on the prototype.
- Multiple inheritance. :: There is a single prototype.
- That J
…