Object-Oriented Concepts
The ideas behind object-oriented programming in any language: objects, encapsulation, inheritance, polymorphism and abstraction.
Objects: state and behavior
Object-oriented programming (OOP) is a programming paradigm that organizes a program around objects. An object combines two things:
- state — the data that describes it right now: a bank account's owner and balance, a lamp's wattage and whether it is on;
- behavior — the operations it can perform on that data: deposit, withdraw, switch on.
Instead of passing data to separate procedures, you ask the object to do something ("send it a message"), and the object decides how. This keeps the data and the code that is allowed to change it in one place. It also maps well onto the real world and onto user interfaces, where a program naturally consists of things: customers, orders, windows, buttons.
Classes and prototypes
In most object-oriented languages — Java, C#, C++, Python, Ruby —
you first write a class: a blueprint that says
which data an object of that kind holds and which methods it has.
Each object made from the class is an instance. The
instances share their methods but each has its own state: two
BankAccount objects have the same
deposit method but different balances.
Prototypes in JavaScript
JavaScript is prototype-based. There, an object can
be created directly, without a class, and it can use another object
as its prototype: when a property is not found on the
object itself, JavaScript looks it up on the prototype. The
class keyword in modern JavaScript is a more familiar
syntax on top of this same mechanism.
const animal = {
describe() {
return `${this.name} says ${this.sound}`;
},
};
const cat = Object.create(animal); // cat's prototype is animal
cat.name = "Mittens";
cat.sound = "meow";
console.log(cat.describe()); // found on the prototype
console.log(Object.getPrototypeOf(cat) === animal);
Mittens says meow true
Properties, attributes and methods
The members of an object have names that differ slightly between languages, but the ideas are the same:
- Attributes (also called fields or instance variables) are the pieces of data an object stores: a lamp's room and wattage.
-
Properties are characteristics you read or set on
an object, such as its color or status. In general speech the word
is used for attributes; in C# and Python, a property is
specifically an attribute whose reading or writing runs a small
method behind the scenes. JavaScript calls every member a
property, and one backed by
get/setan accessor property. - Methods are the functions that belong to an object and describe what it can do: switch on, deposit, send.
class Lamp:
def __init__(self, room, watts):
self.room = room # attributes: data about this lamp
self.watts = watts
self.is_on = False
def switch_on(self): # method: something the lamp can do
self.is_on = True
@property
def status(self): # property: computed, read like an attribute
return "on" if self.is_on else "off"
desk = Lamp("office", 9)
print(desk.room, desk.watts, desk.status)
desk.switch_on()
print(desk.status)
office 9 off on
The Python syntax — __init__, self,
decorators — is explained on
Python's Object-Oriented Programming
page.
Encapsulation
Encapsulation means keeping an object's state inside it and allowing changes only through its methods. The methods can then enforce the rules: a bank account refuses a negative deposit, a thermostat refuses a target of 90 degrees. Other code depends only on the methods, so the internal representation can change later without breaking anything.
Languages differ in how strictly they enforce this. Java, C# and C++
have private members that the compiler protects.
JavaScript has truly private fields whose names start with
#: code outside the class cannot even mention them.
class BankAccount {
#balance = 0; // private: only code inside the class can use it
deposit(amount) {
if (amount <= 0) {
throw new RangeError("Deposit must be positive");
}
this.#balance += amount;
}
get balance() {
return this.#balance; // read-only property
}
}
const account = new BankAccount();
account.deposit(50);
account.deposit(25);
console.log(account.balance);
console.log(Object.keys(account)); // the private field is not visible
75 []
Writing account.#balance outside the class is a
SyntaxError: the program does not even start. Python
takes the opposite approach and relies on a
convention: a leading underscore marks a name as
internal, but nothing stops other code from using it.
class BankAccount:
def __init__(self):
self._balance = 0 # internal by convention only
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit must be positive")
self._balance += amount
@property
def balance(self):
return self._balance
account = BankAccount()
account.deposit(50)
print(account.balance)
account._balance = -1_000 # possible, but breaks the rules
print(account.balance)
50 -1000
Abstraction: interfaces and abstract classes
Abstraction means showing what something does and hiding how. In OOP, the code that uses an object should need to know only which methods it offers. Two tools make that explicit.
An interface is a contract: a list
of methods (names, parameters, return types) that a type promises to
provide, without any implementation. Java, C# and TypeScript have an
interface keyword; a class that implements the
interface must supply every method in it. (Java and C# also allow
default methods: an interface method with a ready-made
implementation that a class may override.)
An abstract class is a class
that cannot be instantiated itself and serves only as a base for
other classes. It may contain ordinary methods that its subclasses
share, and abstract methods that each subclass must
implement. In Python, the abc module provides this. A
class can implement many interfaces but, in Java and C#, extend only
one abstract class.
import math
from abc import ABC, abstractmethod
class Shape(ABC):
def __init__(self, name):
self.name = name
@abstractmethod
def area(self):
"""Every concrete shape must implement this."""
def describe(self): # shared by all shapes
return f"{self.name}: {self.area():.2f}"
class Circle(Shape):
def __init__(self, radius):
super().__init__("circle")
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
class Rectangle(Shape):
def __init__(self, width, height):
super().__init__("rectangle")
self.width = width
self.height = height
def area(self):
return self.width * self.height
print(Circle(1.5).describe())
print(Rectangle(3, 4).describe())
try:
Shape("blob")
except TypeError:
print("Shape is abstract and cannot be instantiated")
circle: 7.07 rectangle: 12.00 Shape is abstract and cannot be instantiated
A Python class with only abstract methods plays the role of
an interface. Python also has typing.Protocol for
interfaces that are checked by type checkers rather than through
inheritance. Why code should depend on small interfaces it owns is
explained by the
interface segregation and dependency inversion principles.
Inheritance
Inheritance lets a class (the subclass or child)
take over the attributes and methods of another class (the
superclass or parent), and then add or change what it needs. It
models an is-a relationship: a circle
is a shape, a savings account is a bank account.
In the example above, Circle and
Rectangle inherit name and
describe() from Shape and each provide
their own area() — they override it.
Inheritance avoids duplicating shared code, but it couples the child
tightly to the parent: a change in the parent affects every
subclass. Keep hierarchies shallow, and use inheritance only when
the is-a sentence is really true. The Python details —
super(), overriding, isinstance() — are on
Python's inheritance section.
Polymorphism
Polymorphism ("many forms")
means that the same call works on objects of different types, and
each object responds in its own way. The calling code does not need
an if for every type: it calls the method, and the
object picks the right behavior.
In JavaScript, the classes below override area(), so
the same loop gives different results for each shape:
class Shape {
constructor(name) {
this.name = name;
}
area() {
throw new Error("A subclass must implement area()");
}
describe() {
return `${this.name}: ${this.area().toFixed(2)}`;
}
}
class Circle extends Shape {
constructor(radius) {
super("circle");
this.radius = radius;
}
area() {
return Math.PI * this.radius ** 2;
}
}
class Rectangle extends Shape {
constructor(width, height) {
super("rectangle");
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
}
const shapes = [new Circle(1.5), new Rectangle(3, 4), new Circle(0.5)];
for (const shape of shapes) {
console.log(shape.describe()); // one call, three behaviors
}
circle: 7.07 rectangle: 12.00 circle: 0.79
Polymorphism does not require inheritance. In dynamically typed languages such as Python, any object with the right method will do — this is called duck typing ("if it walks like a duck and quacks like a duck, it is a duck"). The three alert classes below share no parent class:
class EmailAlert:
def __init__(self, address):
self.address = address
def send(self, message):
return f"Email to {self.address}: {message}"
class SmsAlert:
def __init__(self, number):
self.number = number
def send(self, message):
return f"SMS to {self.number}: {message}"
class LogAlert:
def send(self, message):
return f"Logged: {message}"
alerts = [EmailAlert("ops@example.com"), SmsAlert("+31 6 1234 5678"), LogAlert()]
for alert in alerts:
print(alert.send("Disk 90% full on web-01"))
Email to ops@example.com: Disk 90% full on web-01 SMS to +31 6 1234 5678: Disk 90% full on web-01 Logged: Disk 90% full on web-01
Adding a fourth kind of alert means writing one new class; the loop
does not change. Built-in functions are polymorphic too:
len() works on strings, lists and dictionaries alike.
Composition over inheritance
Composition builds an object out of other objects that it holds as attributes and delegates work to. It models a has-a relationship: a car has an engine, a report has a formatter. The well-known design advice "favor composition over inheritance" says: when an object only needs to use another's behavior, hold one instead of inheriting from it.
import json
class PlainFormatter:
def format(self, title, value):
return f"{title}: {value}"
class JsonFormatter:
def format(self, title, value):
return json.dumps({title: value})
class Report:
def __init__(self, formatter):
self.formatter = formatter # has-a: the report holds a formatter
def line(self, title, value):
return self.formatter.format(title, value) # delegates the work
report = Report(PlainFormatter())
print(report.line("visitors", 1250))
report.formatter = JsonFormatter() # swap behavior while running
print(report.line("visitors", 1250))
visitors: 1250
{"visitors": 1250}
With inheritance you would need a PlainReport and a
JsonReport class, and a new subclass for every
combination of features. With composition, the parts are small, can
be tested separately, and can be swapped even while the program
runs.
The design principles behind this advice, such as coupling, the SOLID principles and when an is-a relationship is false, are on Design Principles; holding a swappable formatter like this is the Strategy pattern.