What is object-oriented programming (OOP)?

Object-oriented programming (OOP) is used everywhere. For example, in writing operating systems, commercial software and open-source object-oriented technologies are used. The advantages of OOP only reveal themselves starting from a certain project complexity. Object-oriented programming style is one of the main programming paradigms.

What is object-oriented programming and what is it needed for?

The term ‘object-oriented programming’ was coined towards the end of the 1960s by programming legend Alan Kay. He was a co-developer of the pioneering object-oriented programming language Smalltalk, which was influenced by Simula, the first language with OOP features. The fundamental ideas of Smalltalk continue to influence the OOP features of modern programming languages today. Languages influenced by Smalltalk include Ruby, Python, Go, and Swift.

Object-oriented programming is counted as being one of the predominant programming paradigms next to the popular functional programming (FP). Programming approaches can be classified into the two large currents ‘imperative’ and ‘declarative’. OOP is a characteristic of imperative programming style and specifically a further development of the procedural programming:

  1. Imperative programming: Describe step by step how to solve a problem, for example: algorithm
  • Structured programming
    • Procedural programming
      • Object-oriented programming
  1. Declarative programming: Generate results according to certain rules, for example: SQL query
  • Functional programming
  • Domain-specific programming
Note

The terms ‘procedure’ and ‘function’ are often used interchangeably. Both are executable blocks of code that can take arguments. The difference is that functions return a value, while procedures do not. Not all languages provide explicit support for procedures.

In principle, it is possible to solve any programming problem with any of the paradigms, because all paradigms are ‘Turing-complete’. Therefore, the limiting factor is not the machine, but humans. Individual programmers or programming teams can only have an overview of a limited amount of complexity. So, programmers use abstractions in order to master the complexity. Depending on the application and the problem at hand, there might be one program that is more suitable than another.

Most modern languages are multi-paradigm languages, which allow programming in several programming styles. In contrast, there are languages that support only a single programming style; this is especially true of strictly functional languages such as Haskell:

Paradigm Features Suited to Languages
Imperative OOP Objects, classes, methods, inheritance, polymorphism Modeling, System Design Smalltalk, Java, Ruby, Python, Swift
Imperative Procedural Control Flow, Iteration, Procedures / Functions Sequential data processing C, Pascal, Basic
Declarative Functional Immutability, Pure Functions, Lambda Calculus, Recursion, Type Systems Parallel data processing, mathematical and scientific applications, parsers and compilers Lisp, Haskell, Clojure
Declarative Domain Specific Language (DSL) Expressive, large language scope Domain specific applications SQL, CSS
Note

Surprisingly, CSS is a Turing-complete language. This means that any computations written in other languages could also be solved in CSS.

Object-oriented programming is part of imperative programming and evolved from procedural programming. The latter basically deals with inert data processed by executable code:

  1. Data: Values, data structures, variables
  2. Code: Expressions, control structures, functions

This is precisely the difference between object-oriented and procedural programming: OOP combines data and functions into objects. An object is quasi a living data structure; because objects are not inert but have a behaviour. Objects are therefore comparable with machines or unicellular organisms. While data is merely operated on, you interact with objects or objects interact with each other.

Let’s illustrate the difference with an example. An integer variable in Java or C++ contains only one value. It is not a data structure, but a ‘primitive’:

int number = 42;
Java

Operations on primitives are performed using operators or functions defined outside. This is an example of the successor function, which returns the number following an integer:

int successor(int number) {
    return number + 1;
}
// returns `43`
successor(42)
Java

In contrast, in languages like Python and Ruby, ‘everything is an object’. Even a simple number includes the actual value as well as a set of methods that define operations on the value. Here is the example of the built-in succ function in Ruby:

42.succ
Ruby

First, this is convenient because the functionality for a data type is bundled. It is not possible to call up a method that does not match the type. Methods can do even more, though. In Ruby, even the For loop is used as a method of a number. We’ll output the numbers from 51 to 42 as an example:

51.downto(42) { |n| print n,".. " }
Ruby

So, where do the methods come from? Objects are defined by classes in most languages. It is said that objects are instantiated from classes, and therefore objects are also called instances. A class is a template for creating similar objects that have the same methods. Classes in pure OOP languages function as types. This becomes clear in object-oriented programming in Python because the type function returns a class as the type of a value:

type(42) # <class 'int'>
type('Walter White') # <class 'str'>
Python

How does object-oriented programming work?

If you ask someone with programming experience what OOP is all about, the answer will sound like ‘something about classes’. In fact, classes are not the core of the matter. The basic ideas of Alan Kay’s object-oriented programming are simpler and can be summarised as follows:

  1. Objects encapsulate their internal state.
  2. Objects receive messages via their methods.
  3. Methods are assigned dynamically at runtime.

We take a closer look at these three critical points below.

Objects encapsulate their internal state

To understand what is meant by encapsulation, we use the example of a car. A car has a certain state, e.g. the battery level, the tank level, whether the engine is running or not. If we represent such a car as an object, the internal properties should be able to be changed exclusively via defined interfaces.

Let’s look at a few examples. We have an object car that represents a car. Inside the object, the state is stored in variables. The object manages the values of the variables; for example, we can ensure that energy is consumed to start the engine. We’ll start the car’s engine by sending a message start:

car.start()
Python

At this point the object decides what happens next: If the engine is already running, the message is ignored or a corresponding message is issued. If there is not enough battery charge or the tank is empty, the motor remains off. If all conditions are met, the engine is started, and the internal state is adjusted. For example, a Boolean variable motor_running is set to ‘True’ and the battery charge is reduced by the charge needed to start the engine. We show schematically how the code inside the object might look:

# starting car
motor_running = True
battery_charge -= start_charge
Python

It is important that the internal state cannot be changed directly from the outside. Otherwise, we could set motor_running to ‘True’ even if the battery is empty. That would be magic and would not reflect the actual conditions of reality.

Send messages / call a method

As we have seen, objects respond to messages and may change their internal state in response. We call these messages methods; technically, they are functions bound to an object. The message consists of the name of the method and possibly other arguments. The receiving object is called the receiver. We express the general scheme of message reception by objects as follows:

# call a method
receiver.method(args)
Python

Another example is as follows. Let’s imagine we are programming a smartphone. Different objects represent functionalities, e.g. the phone functions the flashlight, a call, a text message, etc. Usually, the individual subcomponents are modelled as objects. For example, the address book is an object, as is each contact it contains, and so is a contact’s phone number. This makes it easy to model processes from reality:

# find a person in our address book
person = contacts.find('Walter White')
# let's call that person's work number
call = phone.call(person.phoneNumber('Work'))
...
# after some time, hang up the phone
call.hangUp()
Python

Dynamic assignment of the methods

The third essential criterion in Alan Kay’s original definition of OOP is the dynamic allocation of methods at runtime. This means that the decision about which code to execute when a method is called is made only when the program is executed. As a consequence, the behaviour of an object can be modified at runtime.

The dynamic assignment of methods has important implications for the technical implementation of OOP functionality in programming languages. In practice, it doesn’t come up all too often. Nevertheless, let’s look at an example. We’ll model the flashlight of the smartphone as object flashlight. This reacts to the messages on, off and intensity:

// turn on flashlight
flashlight.on()
// set flashlight intensity to 50%
flashlight.intensity(50)
// turn off flashlight
flashlight.off()
JavaScript

Let’s say the flashlight breaks and we decide to issue an appropriate warning for any access. One approach is to replace all methods with a new method. In JavaScript, for example, this is quite simple. We define the new function out_of_order and use it to overwrite the existing methods:

function out_of_order() {
    console.log('Flashlight out of order. Please service phone.')
    return false;
}
flashlight.on = out_of_order;
flashlight.off = out_of_order;
flashlight.intensity = out_of_order;
JavaScript

If we try to interact with the flashlight afterwards, out_of_order is always called:

// calls `out_of_order()`
flashlight.on()
// calls `out_of_order()`
flashlight.intensity(50)
// calls `out_of_order()`
flashlight.off()
JavaScript

Where do objects come from? Instantiation and initialisation

So far, we have seen how objects receive messages and react to them. But where do the objects come from? Let’s look at the central concept of instantiation. Instantiation is the process by which an object is brought into existence. In different OOP languages there are different mechanisms of instantiation. Mostly one or more of the following mechanisms are used:

  1. Definition per object literal
  2. Instantiation with constructor function
  3. Instantiation from a class

JavaScript excels here because objects like numbers or strings can be defined directly as literals. A simple example is if we instantiate an empty object person and then assign the property name and a method greet. Subsequently, our object is able to greet another person and call its own name:

// instantiate empty object
let person = {};
// assign object property
person.name = "Jack";
// assign method
person.greet = function(other) {
    return `"Hi ${other}, I'm ${this.name}"`
};
// let's test
person.greet("Jim")
JavaScript

We have instantiated a unique object. However, we often want to repeat the instantiation to create a set of similar objects. This case can also be easily covered in JavaScript. We create what’s called a constructor function that assembles an object when called. Our constructor function named Person takes a name and an age and creates a new object when called up:

function Person(name, age) {
    this.name = name;
    this.age = age;
    
    this.introduce_self = function() {
        return `"I'm ${this.name}, ${this.age} years old."`
    }
}
// instantiate person
person = new Person('Walter White', 42)
// let person introduce themselves
person.introduce_self()
JavaScript

Note the use of the this keyword. This is also found in other languages such as Java, PHP and C++ and often causes confusion for OOP newbies. In short, this is a placeholder for an instantiated object. When a method is called, this references the receiver, pointing to a specific object instance. Other languages such as Python and Ruby use the keyword self instead of this, which serves the same purpose.

Furthermore, in JavaScript we need the new keyword to correctly create the object instance. This is found especially in Java and C++, which distinguish between ‘stack’ and ‘heap’ when storing values in memory. In both languages, new is used to allocate memory on the heap. JavaScript, like Python, stores all values on the heap, so new is actually unnecessary. Python demonstrates that it can be done without.

The third and most common mechanism for creating object instances makes use of classes. A class fulfills a similar role as a constructor function in JavaScript: Both serve as a blueprint by which similar objects can be instantiated as needed. At the same time, in languages such as Python and Ruby, a class acts as a replacement for the types used in other languages. We show you an example of a class below.

What are the pros and cons of OOP?

Object-oriented programming has come under increasing criticism since the beginning of the 21st century. Modern, functional languages with immutability and strong type systems are considered more stable, more reliable and better in their performance. Nevertheless, OOP is widely used and has distinct advantages. It is important to choose the right tool for each problem instead of relying on just one methodology.

Pro: Encapsulation

An immediate advantage of OOP is the grouping of functionality. Instead of grouping multiple variables and functions into a loose collection, they can be combined into consistent units. We will show the difference with an example: We model a bus and use two variables and one function for it. Passengers can board the bus until it is full:

# list to hold the passengers
bus_passengers = []
# maximum number of passengers
bus_capacity = 12
# add another passenger
def take_bus(passenger)
    if len(bus_passengers) < bus_capacity:
        bus_passengers.append(passenger)
    else:
        raise Exception("Bus is full")
Python

The code works but is problematc. The take_bus function accesses the bus_passengers and bus_capacity variables without passing them as arguments. This leads to problems with extensive code, since the variables must either be provided globally or passed with each call. Furthermore, it is possible to ‘cheat’. We can continue to add passengers to the bus even though it is actually full:

# bus is full
assert len(bus_passengers) == bus_capacity
# will raise exception, won't add passenger
take_bus(passenger)
# we cheat, adding an additional passenger directly
bus_passengers.append(passenger)
# now bus is over capacity
assert len(bus_passengers) > bus_capacity
Python

Moreover, nothing prevents us from increasing the capacity of the bus. However, this violates assumptions about physical reality, because an existing bus has a limited capacity that cannot be changed arbitrarily afterwards:

# can't do this in reality
bus_capacity += 1
Python

Encapsulating the internal state of objects protects against nonsensical or unwanted changes. Here is the same functionality in object-oriented code. We define a bus class and instantiate a bus with limited capacity. Adding passengers is possible only through the corresponding method:

class Bus():
    def __init__(self, capacity):
        self._passengers = []
        self._capacity = capacity
    
    def enter(self, passenger):
        if len(self._passengers) < self._capacity:
            self._passengers.append(passenger)
            print(f"{passenger} has entered the bus")
        else:
            raise Exception("Bus is full")
# instantiate bus with given capacity
bus = Bus(2)
bus.enter("Jack")
bus.enter("Jim")
# will fail, bus is full
bus.enter("John")
Python

Pro: System modelling

Object-oriented programming is particularly well suited for modeling system. Thereby OOP is human intuitive, because we also think in objects, which can be classified into categories. Objects can be physical things as well as abstract concepts.

The inheritance via class hierarchies found in many OOP languages also reflects human thought patterns. Let’s illustrate the last point with an example. An animal is an abstract concept. Animals that actually occur are always concrete expressions of a species. Depending on the species, animals have different characteristics. A dog cannot climb or fly, so it is limited to movements in two-dimensional space:

# abstract base class
class Animal():
    def move_to(self, coords):
        pass
# derived class
class Dog(Animal):
    def move_to(self, coords):
        match coords:
            # dogs can't fly nor climb
            case (x, y):
                self._walk_to(coords)
# derived class
class Bird(Animal):
    def move_to(self, coords):
        match coords:
            # birds can walk
            case (x, y):
                self._walk_to(coords)
            # birds can fly
            case (x, z, y):
                self._fly_to(coords)
Python

Cons of object-oriented programming

A clear disadvantage of OOP is the jargon, which is difficult to understand at the beginning. You’re forced to learn completely new concepts, the meaning and purpose of which is often not clear from simple examples. This makes it easy to make mistakes; especially the modelling of inheritance hierarchies requires a lot of skill and experience.

One of the most frequent criticisms of OOP is the encapsulation of the internal state, which is actually intended as an advantage. This leads to difficulties when parallelising OOP code. This is because if an object is passed to multiple parallel functions, the internal state could change between function calls. Sometimes it is necessary to access information encapsulated elsewhere within a program.

The dynamic nature of object-oriented programming usually results in performance penalties. This is because fewer static optimisations can be made. The tendentially less pronounced type systems of pure OOP languages also make some static checks impossible. This means that errors only become visible at runtime. Newer developments such as the JavaScript language TypeScript counter this.

Which programming languages support or are suited to OOP?

Almost all multi-paradigm languages are suitable for object-oriented programming. These include the well-known internet programming languages PHP, Ruby, Python and JavaScript. In contrast, OOP principles are largely incompatible with the relational algebra underlying SQL. Special translation layers known as ‘object relational mappers’ (ORM) are used to bridge the “impedance mismatch”.

Even purely functional languages like Haskell usually do not provide native support for OOP. Implementing OOP in C requires effort. Interestingly, Rust is a modern language that does not need classes. Instead struct and enum are used as data structures, behaviour of which is defined by an impl keyword. With Traits, behavior can be grouped so inheritance and polymorphism are represented. The design of the language reflects the OOP best practice ‘Composition over Inheritance’.

Was this article helpful?
Page top