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/set an 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.

Class diagram: the abstract class Shape, with attribute name and methods area and describe, is the parent of Circle, with attribute radius, and Rectangle, with attributes width and height; both override area. Shape {abstract} name area() describe() Circle radius area() Rectangle width height area() is a
A UML class diagram. Each box shows the class name, its attributes and its methods; {abstract} marks an abstract class. The hollow arrowhead means "inherits from".

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.