A shop calculates shipping for standard, express and pickup orders. Written procedurally, that is one function with an if for each option. It works until someone adds a courier option, and every function that checks the shipping type has to be found and edited. Written in an object-oriented style, each option is an object that knows its own cost, and adding a courier means adding one class. Nothing that already works has to change. That difference, in where a change lands, is most of what object-oriented programming (OOP) is for.
This guide is the overview of the object-oriented programming series. It explains what OOP is, how it differs from procedural programming, and what its core concepts mean, with one example in C++, Java and Python that uses all of them. It then covers the techniques that make object-oriented code easy to change and the cases where OOP is the wrong tool. Every program was run for this article on Ubuntu 24.04 with GCC 13.3 and Clang 18.1.3 (-std=c++17 -Wall -Wextra -pedantic -Werror), OpenJDK 21 (javac -Xlint:all -Werror) and Python 3.11, 3.12 and 3.13 (-W error), and every output below is captured verbatim. The code is in a GitHub repository whose build repeats these checks on each commit.
The Short Answer
What is object-oriented programming? A way of structuring programs around objects, each combining data with the operations that act on it. Objects are created from classes and work together by calling each other’s methods, instead of passing shared data through a sequence of functions.
What are the four pillars of OOP? Encapsulation (an object controls changes to its own data), abstraction (code depends on what an object does, not how), inheritance (a class builds on an existing one) and polymorphism (one call runs different code for different types of object).
Are C++, Java and Python object-oriented? All three support OOP fully, in different ways. Java builds every program from classes. Python makes every value an object but doesn’t require classes. C++ supports OOP alongside procedural and generic programming, and uses it only where you choose to.
What Is Object-Oriented Programming?
Object-oriented programming is a programming paradigm that organizes software as a collection of objects. Each object bundles state (its data) with behavior (the methods that read and change that data), and is created from a class that defines both. Programs work by objects calling each other’s methods, and each object is responsible for keeping its own data valid.
The idea is older than any language in this guide. Ole-Johan Dahl and Kristen Nygaard introduced classes, objects, subclasses and virtual procedures in Simula 67, a language for simulation. Alan Kay’s group at Xerox PARC developed Smalltalk through the 1970s, and Kay is generally credited with coining the term “object-oriented”. Bjarne Stroustrup brought classes to C as “C with Classes” from 1979, released as C++ in 1985. Python followed in 1991 and Java in 1995, and OOP became the default way to structure large programs.
Two basic building blocks come first:
- A class is a definition: the data each object will hold and the methods that operate on it.
- An object is an instance of a class, created at run time, with its own state and identity.
Procedural vs Object-Oriented Programming
Procedural programming, the dominant style of C, Pascal and Fortran, organizes a program as a sequence of steps grouped into functions. The functions are the main unit, and the data they work on is passed between them or shared. This works well for small programs. As a program grows, more functions come to depend on the same data and the same decisions, and a change to either has to be made in many places.
Here is the shipping calculation written procedurally:
"""shipping_procedural.py - the procedural version: one function decides
the cost by checking which kind of shipping was chosen."""
def shipping_cost_cents(kind: str, kg: int) -> int:
if kind == "standard":
return 400 + 50 * kg
if kind == "express":
return 1000 + 100 * kg
if kind == "pickup":
return 0
raise ValueError(f"unknown shipping kind: {kind}")
def money(cents: int) -> str:
return f"{cents // 100}.{cents % 100:02d}"
for kind in ("standard", "express", "pickup"):
print(f"{kind:<8} 2 kg: {money(shipping_cost_cents(kind, 2))}")
Output:
standard 2 kg: 5.00
express 2 kg: 12.00
pickup 2 kg: 0.00
The function is short and correct. The problem is what happens next. The decision “which kind of shipping is this?” is made by string comparison inside the function, and any other function that depends on the kind (an estimated delivery date, a label format, a tracking link) has to make the same decision again. Adding a courier option means finding and editing every one of them.
| Aspect | Procedural | Object-oriented |
|---|---|---|
| Main unit | Functions | Objects (data plus methods) |
| Data | Passed between functions, or shared | Owned by the object, changed through its methods |
| Choosing behavior by type | if or switch on a type code | The object’s own method runs |
| Adding a new type | Edit every function that checks the type | Add a class |
| Adding a new operation | Add one function | Add a method to every class |
The last two rows are the real trade-off, sometimes called the expression problem (Philip Wadler’s name for it, from 1998). Object-oriented design makes new types cheap and new operations expensive; procedural design does the opposite. OOP fits a program whose set of types grows more often than its set of operations.
One Example, Four Pillars
Here is the same calculation written with objects. Each shipping method is an object with a name() and a cost_cents() method; quote() works with any of them; and a courier option has been added as a new class:
C++:
// shipping.cpp - the object-oriented version: each shipping method is an
// object that knows its own cost, and quote() works with any of them.
#include <iomanip>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
class ShippingMethod { // abstraction: what every shipping method offers
public:
virtual ~ShippingMethod() = default;
virtual std::string name() const = 0;
virtual long cost_cents(int kg) const = 0;
};
class RateShipping : public ShippingMethod { // inheritance: one implementation
public:
RateShipping(std::string name, long base_cents, long per_kg_cents)
: name_(std::move(name)), base_(base_cents), per_kg_(per_kg_cents) {
if (base_ < 0 || per_kg_ < 0) throw std::invalid_argument("rates must not be negative");
}
std::string name() const override { return name_; }
long cost_cents(int kg) const override { return base_ + per_kg_ * kg; }
private: // encapsulation: the rates are checked once and can't change afterwards
std::string name_;
long base_;
long per_kg_;
};
class Pickup : public ShippingMethod {
public:
std::string name() const override { return "pickup"; }
long cost_cents(int) const override { return 0; }
};
class Courier : public ShippingMethod { // added later: quote() did not change
public:
std::string name() const override { return "courier"; }
long cost_cents(int) const override { return 750; }
};
std::string money(long cents) {
std::string fraction = std::to_string(cents % 100);
return std::to_string(cents / 100) + "." + (fraction.size() < 2 ? "0" : "") + fraction;
}
void quote(const ShippingMethod& method, int kg) { // polymorphism: any method works
std::cout << std::left << std::setw(8) << method.name() << ' ' << kg
<< " kg: " << money(method.cost_cents(kg)) << '\n';
}
int main() {
std::vector<std::unique_ptr<ShippingMethod>> methods;
methods.push_back(std::make_unique<RateShipping>("standard", 400, 50));
methods.push_back(std::make_unique<RateShipping>("express", 1000, 100));
methods.push_back(std::make_unique<Pickup>());
methods.push_back(std::make_unique<Courier>());
for (const auto& method : methods) quote(*method, 2);
}
Java:
// Shipping.java - the object-oriented version: each shipping method is an
// object that knows its own cost, and quote() works with any of them.
import java.util.List;
public final class Shipping {
interface ShippingMethod { // abstraction: what every shipping method offers
String name();
long costCents(int kg);
}
static final class RateShipping implements ShippingMethod { // inheritance: one implementation
private final String name; // encapsulation: the rates are checked once and can't change afterwards
private final long base;
private final long perKg;
RateShipping(String name, long baseCents, long perKgCents) {
if (baseCents < 0 || perKgCents < 0) throw new IllegalArgumentException("rates must not be negative");
this.name = name;
this.base = baseCents;
this.perKg = perKgCents;
}
@Override public String name() { return name; }
@Override public long costCents(int kg) { return base + perKg * kg; }
}
static final class Pickup implements ShippingMethod {
@Override public String name() { return "pickup"; }
@Override public long costCents(int kg) { return 0; }
}
static final class Courier implements ShippingMethod { // added later: quote() did not change
@Override public String name() { return "courier"; }
@Override public long costCents(int kg) { return 750; }
}
static String money(long cents) {
return String.format("%d.%02d", cents / 100, cents % 100);
}
static void quote(ShippingMethod method, int kg) { // polymorphism: any method works
System.out.printf("%-8s %d kg: %s%n", method.name(), kg, money(method.costCents(kg)));
}
public static void main(String[] args) {
List<ShippingMethod> methods = List.of(
new RateShipping("standard", 400, 50),
new RateShipping("express", 1000, 100),
new Pickup(),
new Courier());
for (ShippingMethod method : methods) quote(method, 2);
}
}
Python:
"""shipping.py - the object-oriented version: each shipping method is an
object that knows its own cost, and quote() works with any of them."""
from abc import ABC, abstractmethod
class ShippingMethod(ABC): # abstraction: what every shipping method offers
@abstractmethod
def name(self) -> str: ...
@abstractmethod
def cost_cents(self, kg: int) -> int: ...
class RateShipping(ShippingMethod): # inheritance: one implementation
def __init__(self, name: str, base_cents: int, per_kg_cents: int) -> None:
if base_cents < 0 or per_kg_cents < 0:
raise ValueError("rates must not be negative")
self._name = name # encapsulation: internal by convention, set once here
self._base = base_cents
self._per_kg = per_kg_cents
def name(self) -> str:
return self._name
def cost_cents(self, kg: int) -> int:
return self._base + self._per_kg * kg
class Pickup(ShippingMethod):
def name(self) -> str:
return "pickup"
def cost_cents(self, kg: int) -> int:
return 0
class Courier(ShippingMethod): # added later: quote() did not change
def name(self) -> str:
return "courier"
def cost_cents(self, kg: int) -> int:
return 750
def money(cents: int) -> str:
return f"{cents // 100}.{cents % 100:02d}"
def quote(method: ShippingMethod, kg: int) -> None: # polymorphism: any method works
print(f"{method.name():<8} {kg} kg: {money(method.cost_cents(kg))}")
methods: list[ShippingMethod] = [
RateShipping("standard", 400, 50),
RateShipping("express", 1000, 100),
Pickup(),
Courier(),
]
for method in methods:
quote(method, 2)
Output (identical for all three):
standard 2 kg: 5.00
express 2 kg: 12.00
pickup 2 kg: 0.00
courier 2 kg: 7.50
The first three lines match the procedural version exactly. The fourth comes from Courier, which was added without changing quote() or any existing class. Costs are kept in integer cents, so no floating-point rounding can creep into a price.
Every core concept of OOP appears in this one program:
| Concept | What it means | In the shipping example | Full guide |
|---|---|---|---|
| Class | A definition of data and methods | RateShipping defines a name, two rates and how to compute a cost | Classes |
| Object | An instance of a class with its own state | standard and express are two objects of one class, with different rates | Objects |
| Encapsulation | An object controls changes to its own data | The rates are checked in the constructor and can’t be changed from outside (in Python, by convention only) | Encapsulation |
| Abstraction | Code depends on what an object does, not how | ShippingMethod declares name() and cost_cents(), with no implementation | Abstraction |
| Inheritance | A class builds on an existing class or interface | RateShipping, Pickup and Courier all implement ShippingMethod (an interface in Java, an abstract base class in C++ and Python) | Inheritance |
| Polymorphism | One call runs different code for different types | quote() calls cost_cents(), and each class runs its own version | Polymorphism |
The guides go in the order of the table, and each builds on the one before it, using C++, Java and Python side by side.
Object-Oriented Programming Techniques
Knowing the four pillars is not the same as writing object-oriented code that is easy to change. These techniques are what experienced developers mean by good OOP, and each one is visible in the shipping example.
- Program to an interface, not an implementation.
quote()takes aShippingMethod, not aRateShipping, so it works with classes that did not exist when it was written. The phrase comes from the “Gang of Four” book Design Patterns (Gamma, Helm, Johnson and Vlissides, 1994). - Replace type checks with polymorphism. The procedural version’s chain of
if kind == ...became one method call. Martin Fowler’s Refactoring lists this as “Replace Conditional with Polymorphism”. A longif/switchon a type code is often a sign that a class hierarchy is missing. - Protect invariants in the class that owns the data.
RateShippingrejects negative rates once, in its constructor, so no method has to check them again. That is the practical meaning of encapsulation. - Favor composition over inheritance. Design Patterns states this one directly: “Favor object composition over class inheritance.” Standard and express shipping are two objects of one class with different rates, not two subclasses. Inheritance is reserved for real “is-a” relationships, where the code that uses the base type must work with every subclass.
- Keep classes small and focused. Each shipping class does one thing. A class that computes costs, prints receipts and talks to a database has three reasons to change, and changes to one put the others at risk.
- Pass dependencies in.
quote()receives the shipping method instead of creating it, so a caller (or a test) can supply any implementation. The abstraction guide shows the same technique with a clock that a test can fix at 8:00.
SOLID Principles
Robert C. Martin collected five design principles for object-oriented code, known by the acronym SOLID. Each is a more precise statement of one of the techniques above:
| Principle | In one sentence | Technique above |
|---|---|---|
| Single responsibility | A class should have one reason to change | 5 |
| Open/closed | Extend behavior by adding code, not by editing working code | 1, 2 (adding Courier) |
| Liskov substitution | A subclass must work anywhere its base class is expected | 4 |
| Interface segregation | Clients shouldn’t depend on methods they don’t use | 1 |
| Dependency inversion | Depend on abstractions, and pass concrete objects in | 1, 6 |
The principles are guidelines, not laws. Applied mechanically, they produce many small interfaces with a single implementation each, which is harder to read than the code they replaced.
Design Patterns
A design pattern is a named, reusable solution to a common design problem. Design Patterns catalogued 23; a handful cover most everyday object-oriented code:
| Pattern | Problem it solves | Where it appears in this series |
|---|---|---|
| Strategy | Choose one of several interchangeable algorithms at run time | The shipping methods on this page |
| Template method | Fix the steps of an operation, let subclasses change one step | send() and format() in the inheritance guide |
| Factory | Create objects without the caller naming their concrete class | A function that returns a ShippingMethod for a name read from a config file |
| Observer | Notify many objects when one object changes | Event listeners and GUI callbacks |
| Adapter | Make an existing class fit an interface it wasn’t written for | Wrapping a third-party shipping API as a ShippingMethod |
Singleton, a class with exactly one global instance, is also in the catalog, but it behaves like a global variable: it hides dependencies and makes code hard to test. Passing the one instance in (technique 6) usually works better.
OOP in C++, Java and Python Compared
All three languages implement the same concepts, with different defaults:
| Aspect | C++ | Java | Python |
|---|---|---|---|
| Is everything an object? | No. int and free functions are not | Almost: primitives such as int are not, and all code lives in classes | Yes. Integers, functions and classes are all objects |
Assignment b = a | Copies the object | Copies a reference | Binds a second name to the object |
| Runtime polymorphism | Opt-in with virtual | Default for instance methods | Every method call |
| Interfaces | Abstract classes with pure virtual functions | interface | Abstract base classes, or typing.Protocol |
| Multiple inheritance | Supported for classes | Classes no; a class can implement many interfaces | Supported, with a defined method resolution order |
| Access control | public, protected, private, checked at compile time | Adds package-private, checked at compile time | Conventions (_name); not enforced |
| Object cleanup | Deterministic: destructors run at end of scope | Garbage collected; try-with-resources for resources | Reference counting plus a cycle collector (CPython); with for resources |
For C++ in depth, see object-oriented programming in C++, inheritance in C++, multiple inheritance in C++ and virtual functions in C++.
When Not to Use OOP
OOP is one way to organize a program, and it isn’t always the right one:
- Small scripts and data pipelines. A fifty-line script that reads a file and prints a summary gains nothing from classes. Functions are clearer.
- New operations more often than new types. A compiler adds passes over a fixed set of syntax-tree node types far more often than it adds node types. That is the procedural side of the trade-off in the table above. In C++,
std::variantwithstd::visit, or a visitor, often fits better than virtual functions, as shown in object-oriented programming in C++. - Performance-critical data processing. Large arrays of small objects, each reached through a pointer and a virtual call, use the CPU cache poorly. Game engines and simulations often switch to data-oriented design, storing each field in its own contiguous array.
- Deep inheritance hierarchies. If understanding a class means reading five base classes, composition would probably serve better.
Most real programs mix styles. The standard libraries of all three languages use objects for containers and resources, plain functions for algorithms, and generics or templates for code that works across types.
Key Takeaways
- OOP organizes a program around objects that combine data with the methods that change it.
- Its core concepts are classes and objects, plus four pillars: encapsulation, abstraction, inheritance and polymorphism. One short program, about 60 lines in each language, used all six.
- The main trade-off: object-oriented design makes new types cheap (the courier was one class) and new operations expensive. Procedural design does the opposite.
- Good OOP is about techniques, not features: program to interfaces, replace type checks with polymorphism, protect invariants, and prefer composition to inheritance.
- C++, Java and Python share the concepts but not the defaults: copying versus sharing on assignment, opt-in versus automatic virtual dispatch, and enforced versus conventional access control.
Conclusion
Object-oriented programming is a way of deciding where code lives. Data goes with the methods that keep it valid, decisions about type go into the types themselves, and new behavior arrives as new classes instead of edits to working code. It doesn’t suit every program, and its trade-off is real, but for systems whose set of types keeps growing it keeps changes local.
The rest of this series takes each concept in turn, in C++, Java and Python. Start with classes and objects, then encapsulation, abstraction, inheritance and polymorphism. Everything in the series is collected in the OOP section.
Source Code and Tests
All programs on this page are in the oop/oop-overview directory of the MYCPLUS C++ examples repository.
Build and test:
bash tests/run_tests.sh g++
What the build checks. On Linux, with GCC and with Clang, it compiles the C++ program with -Wall -Wextra -pedantic -Werror, compiles the Java program with javac -Xlint:all -Werror on Java 21, runs the Python programs with -W error on Python 3.12, and checks that every program prints exactly the output shown on this page. The object-oriented version prints the same in all three languages. Python 3.11 and 3.13 were checked locally and produced the same output.




