Chapter 01
Object-oriented programming properly
Classes, objects and the four ideas behind them, used to protect rules and hide change rather than to build deep family trees. Composition over inheritance starts here.
Class and object
Also called instance, constructor, method, field, attribute
- What
- A class is a blueprint that bundles data (fields) with the behaviour that works on it (methods). An object is one instance made from that blueprint, with its own field values.
- Why
- Keeping data next to the only code allowed to change it is what makes a large program understandable: to know how an order can change, you read the Order class.
- Example
- An Order class has fields for items and status and methods addItem, cancel and total. Each customer checkout creates a new Order object.
Watch out
Writing classes that are only bags of public fields, while the real rules live in scattered helper functions.
Encapsulation
Also called information hiding, private, access modifier, invariant
- What
- Hiding an object's internal state behind a small public interface, so outside code can only change it through methods that keep its rules (invariants) true.
- Why
- If only deposit can change a balance, a negative deposit is impossible everywhere. You can also change the inside, such as the storage type, without breaking callers.
class Account:
def __init__(self) -> None:
self._balance_cents = 0 # leading _ means internal
def deposit(self, cents: int) -> None:
if cents <= 0:
raise ValueError("positive only")
self._balance_cents += cents
@property
def balance(self) -> int:
return self._balance_centsWatch out
Generating a getter and setter for every field, which makes the field public in all but name.
Abstraction
Also called interface, contract
- What
- Showing callers what something does and hiding how it does it. A caller asks a PaymentGateway to charge an amount and never sees HTTP calls, retries or card formats.
- Why
- Callers depend on a small, stable idea instead of a large, changing implementation, so the details can change or be swapped without touching them.
Watch out
Inventing an abstraction for something with one implementation and no expected change, which adds a layer and saves nothing.
Inheritance
Also called extends, subclass, superclass, base class, is-a
- What
- A subclass takes on the fields and methods of a parent class and can add to or override them. It models an is-a relationship: a SavingsAccount is an Account.
- Why
- It shares code and lets a subclass be used wherever the parent is expected. Used shallowly, for true is-a cases, it keeps related types consistent.
- Example
- A Shape base class with an abstract area method, and Circle and Square subclasses that each implement it.
Watch out
Inheriting just to reuse code, such as Stack extends ArrayList, which leaks every list method to callers and ties you to the parent forever.
Polymorphism
Also called dynamic dispatch, override, duck typing
- What
- One call working on many types: code asks any Shape for its area, and each type answers in its own way. The right method is chosen at run time.
- Why
- It replaces chains of if type is X, else if type is Y with objects that know their own behaviour, so adding a type means adding a class, not editing every switch.
class Shape(Protocol):
def area(self) -> float: ...
@dataclass
class Circle:
r: float
def area(self) -> float: return math.pi * self.r ** 2
def total(shapes: list[Shape]) -> float:
return sum(s.area() for s in shapes) # no type checksWatch out
Keeping an instanceof or type switch next to a polymorphic method, so new types still need edits in two places.
Interfaces and abstract classes
Also called interface, abstract class, Protocol, ABC, abstract base class
- What
- An interface lists methods a type promises to provide, with no state. An abstract class is a partial class that cannot be created on its own and may hold shared fields and code.
- Why
- Interfaces are the usual seam for swapping implementations and for test doubles. Reach for an abstract class only when subclasses truly share state or steps.
- Example
- Java and C# have both keywords. TypeScript interfaces vanish at run time. Python uses Protocol for structural interfaces and ABC for abstract base classes.
Watch out
Prefixing every class with an interface by habit, even when there will only ever be one implementation.
Composition over inheritance
Also called has-a, delegation, favour composition
- What
- Building a class by holding other objects and delegating to them (has-a) rather than by extending a parent (is-a). The Gang of Four put it as favour object composition over class inheritance.
- Why
- Parts can be swapped at run time, tested alone and combined freely, and the class exposes only what it chooses. Deep hierarchies break when a parent changes.
class Stack:
"""Has a list; does not inherit from list."""
def __init__(self) -> None:
self._items: list = []
def push(self, item) -> None:
self._items.append(item)
def pop(self):
return self._items.pop()Watch out
Treating the rule as never inherit: a shallow, true is-a hierarchy is fine.
Value objects and immutability
Also called immutable, record, dataclass, readonly, value object
- What
- A value object is defined by its values, not an identity: two Money(10, "USD") objects are equal. Making it immutable means it never changes after creation; you create a new one instead.
- Why
- Immutable objects can be shared between threads and passed around without anyone changing them behind your back, which removes a whole class of bugs.
- Example
- Java records, C# records, Python frozen dataclasses and TypeScript readonly fields all make small immutable value types cheap to write.
Watch out
Marking the field final or readonly but exposing a mutable list inside it, so the object can still change.