Lesson 16 / 18
Introduction to Object-Oriented Programming
Combining data and behavior, class and object, encapsulation, preserving invariants, and the idea of an interface.
Contents
The previous lesson ended with two difficulties: which procedures a data structure belongs to is written nowhere in the code, and the rules of a data structure have to be preserved separately in every procedure that uses it.
Object-oriented programming answers both difficulties with the same move: it gathers data and the behavior that operates on it into a single unit. This unit is called an object.
Class and Object
A class is the definition of an object type: it states which data it will carry and which operations it will support. An object is a concrete instance produced from that definition.
The distinction mirrors the one between a type and a value: a class is a template, an
object an individual produced from it. A single MeasurementSeries class can produce
hundreds of independent objects, each with its own data and shared behavior.
class MeasurementSeries: """Carries a sequence of measurements together with the operations on it.""" def __init__(self, measurements: list[int]) -> None: if not measurements: raise ValueError("measurement list cannot be empty") self._measurements = list(measurements) # defensive copy def add(self, measurement: int) -> None: self._measurements.append(measurement) def average(self) -> float: return sum(self._measurements) / len(self._measurements) def largest(self) -> int: largest = self._measurements[0] for measurement in self._measurements: if measurement > largest: largest = measurement return largest def count(self) -> int: return len(self._measurements) series = MeasurementSeries([12, 18, 7, 25, 14]) print(series.average(), series.largest(), series.count()) # 15.2 25 5 series.add(30) print(series.average(), series.largest()) # 17.666666666666668 30
In the structural solution, add_up, find_largest, and the data stood apart; here
they are gathered into a single unit. The calling style also changes: instead of
passing data to a procedure, a message is sent to an object.
Encapsulation and Invariants
An object’s data is closed to direct access from the outside; access happens only through the operations the object offers. This is called encapsulation.
The reason for encapsulation is not secrecy but preserving invariants. The
invariant of the MeasurementSeries class is this: the measurement list is never
empty. This rule is preserved in two places — it is checked at creation, and no
operation is offered that would empty the list.
In the structural solution, preserving the same rule required a separate check in every procedure that used the list. Here the rule sits in a single place: the owner of the data.
This continues the defensive-copy discussion from the previous topic: without the copy in the constructor, changing the outside list could violate the invariant. An operation returning data outward would need to return a copy for the same reason.
Interface and Implementation
The set of operations an object exposes to the outside is called its interface; how those operations are carried out is its implementation. The caller knows only the interface.
The value of this distinction is that the implementation can be changed freely. Instead
of recomputing the largest value from scratch on every call, MeasurementSeries could
keep it updated as elements are added; because the interface stays the same, not a
single line of the calling code changes.
This is a stronger form of the information-hiding idea from structural decomposition: there, the inside of a procedure was hidden; here, the representation of the data is hidden as well.
Relationships Between Objects
A single object is rarely enough; a program is built as a network of objects sending each other messages. Relationships take two basic forms.
Composition is one object carrying another as its part. A report object that stores a measurement series holds the series inside itself and manages its lifetime.
Collaboration is one object knowing another and sending it messages; the two live independently.
class Report: """Produces a readable summary from a measurement series.""" def __init__(self, series: MeasurementSeries) -> None: self._series = series # collaboration: series was created outside def text(self) -> str: return (f"{self._series.count()} measurements, " f"average {self._series.average():.1f}, " f"largest {self._series.largest()}") series = MeasurementSeries([12, 18, 7, 25, 14]) print(Report(series).text()) # 5 measurements, average 15.2, largest 25
The Report class does not know the inside of the series; it uses only its interface.
This is the object-to-object counterpart of the interface–implementation distinction
from the previous section, and it gives the deciding criterion for design: the less
one object depends on the internal structure of another, the less a change propagates.
The direction of the dependency is also a decision. Here the report depends on the series, and the series knows nothing of the report; adding a new report format therefore never changes the series. Had the dependency run the other way, every new report type would have required changing the series as well.
Inheritance and Polymorphism
Two more concepts belong to this approach and are only introduced here.
Inheritance is one class extending the definition of another. Shared behavior is written once in the parent class; subclasses add the differences.
Polymorphism is the same message behaving differently across different object types. A caller can invoke the same operation without knowing the exact type of the object it is dealing with; which implementation runs is decided by the object.
The practical consequence of polymorphism is that conditional chains disappear: logic
that says “behave differently by type” is distributed into separate classes instead of
an if chain. This is the object-oriented counterpart of the lookup-table approach from
the conditional-branching lesson.
Using the two correctly — in particular, knowing when to prefer composition over inheritance — is the subject of the Software Design and Architectural Principles curriculum.
Identity and Equality
Because objects carry state, the word “same” carries two meanings. The identity–equality distinction from the variables lesson turns into a design decision here.
Should two measurement series that carry the same values be considered equal, or only when they are the same object? The answer depends on what the object represents.
Value objects — a date, a monetary amount, a coordinate — are defined by their content; two instances with the same content are equal, and they are usually made immutable.
Entity objects — a user account, an order — are defined by their identity. Two accounts are different accounts even if every field matches; what distinguishes them is identity.
Without this distinction, searching and comparing in collections produces unexpected results. Languages generally let a class define its own equality behavior; without that definition, the default is identity comparison.
The distinction between value and entity objects is one of the core concepts of domain-driven design and is expanded in the Software Design and Architectural Principles curriculum.
When It Fits, When It Does Not
Object-oriented organization is strong where a problem naturally contains entities with state: an account, a connection, a document, a game character. These entities have rules, and the rules must stay with the data.
By contrast, building an object for a stateless pure transformation adds an unnecessary layer. An operation that adds two numbers does not need a class.
The second criticism concerns scale: because objects carry state, a program splits into many pieces of mutable state. As objects hold references to one another, tracing how far a change spreads becomes harder. This observation is the starting point of the next lesson.
Summary
- The object-oriented approach gathers data and the behavior that operates on it into a single unit.
- A class is a definition; an object is a concrete instance produced from it.
- Encapsulation limits access to data to an object’s own operations; its purpose is to preserve invariants in a single place.
- Separating interface from implementation allows the internal structure to be changed without affecting the caller.
- Inheritance shares common behavior; polymorphism selects behavior by type without a conditional chain.
- The approach is strong for entities with state; it adds an unnecessary layer for pure transformations, and mutable state that spreads at scale is a cost.
Next Step
If tracing mutable state becomes harder, the counter-idea that comes to mind is obvious: never change state at all. The next lesson introduces the functional approach, which treats computation as value transformation, and solves the same problem a third time, this time without changing a single value.
To keep your progress and take notes, Log in
My notes
Log in to take notes.