---
title: 'Introduction to Object-Oriented Programming'
source: 'https://academia.sh/en/courses/programming-fundamentals/introduction-to-object-oriented-programming'
course: 'Programming Fundamentals'
language: en
updated: '2026-08-17T18:08:26+00:00'
license: 'CC BY-SA 4.0'
---

# Introduction to Object-Oriented Programming

Combining data and behavior, class and object, encapsulation, preserving invariants, and the idea of an interface.

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.

```python
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.

```python
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.
