Object-oriented pro­gram­ming (OOP) is used every­where. For example, in writing operating systems, com­mer­cial software and open-source object-oriented tech­nolo­gies are used. The ad­van­tages of OOP only reveal them­selves starting from a certain project com­plex­i­ty. Object-oriented pro­gram­ming style is one of the main pro­gram­ming paradigms.

What is object-oriented pro­gram­ming and what is it needed for?

The term “object-oriented pro­gram­ming” was coined towards the end of the 1960s by pro­gram­ming legend Alan Kay. He was a co-developer of the pi­o­neer­ing object-oriented pro­gram­ming language Smalltalk, which was in­flu­enced by Simula, the first language with OOP features. The fun­da­men­tal ideas of Smalltalk continue to influence the OOP features of modern pro­gram­ming languages today. Languages in­flu­enced by Smalltalk include Ruby, Python, Go, and Swift.

Object-oriented pro­gram­ming is counted as being one of the pre­dom­i­nant pro­gram­ming paradigms next to the popular func­tion­al pro­gram­ming (FP). Pro­gram­ming ap­proach­es can be clas­si­fied into the two large currents “im­per­a­tive” and “de­clar­a­tive”. OOP is a char­ac­ter­is­tic of im­per­a­tive pro­gram­ming style and specif­i­cal­ly a further de­vel­op­ment of the pro­ce­dur­al pro­gram­ming:

  1. Im­per­a­tive pro­gram­ming: Describe step by step how to solve a problem, for example: algorithm
  • Struc­tured pro­gram­ming
    • Pro­ce­dur­al pro­gram­ming
      • Object-oriented pro­gram­ming
  1. De­clar­a­tive pro­gram­ming: Generate results according to certain rules, for example: SQL query
  • Func­tion­al pro­gram­ming
  • Domain-specific pro­gram­ming
Note

The terms “procedure” and “function” are often used in­ter­change­ably. Both are ex­e­cutable blocks of code that can take arguments. The dif­fer­ence is that functions return a value, while pro­ce­dures do not. Not all languages provide explicit support for pro­ce­dures.

In principle, it is possible to solve any pro­gram­ming problem with any of the paradigms, because all paradigms are “Turing-complete”. Therefore, the limiting factor is not the machine, but humans. In­di­vid­ual pro­gram­mers or pro­gram­ming teams can only have an overview of a limited amount of com­plex­i­ty. So, pro­gram­mers use ab­strac­tions in order to master the com­plex­i­ty. Depending on the ap­pli­ca­tion 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 pro­gram­ming in several pro­gram­ming styles. In contrast, there are languages that support only a single pro­gram­ming style; this is es­pe­cial­ly true of strictly func­tion­al languages such as Haskell:

Paradigm Features Suited to Languages
Im­per­a­tive OOP Objects, classes, methods, in­her­i­tance, poly­mor­phism Modeling, System Design Smalltalk, Java, Ruby, Python, Swift
Im­per­a­tive Pro­ce­dur­al Control Flow, Iteration, Pro­ce­dures / Functions Se­quen­tial data pro­cess­ing C, Pascal, Basic
De­clar­a­tive Func­tion­al Im­mutabil­i­ty, Pure Functions, Lambda Calculus, Recursion, Type Systems Parallel data pro­cess­ing, math­e­mat­i­cal and sci­en­tif­ic ap­pli­ca­tions, parsers and compilers Lisp, Haskell, Clojure
De­clar­a­tive Domain Specific Language (DSL) Ex­pres­sive, large language scope Domain specific ap­pli­ca­tions SQL, CSS
Note

Sur­pris­ing­ly, CSS is a Turing-complete language. This means that any com­pu­ta­tions written in other languages could also be solved in CSS.

Object-oriented pro­gram­ming is part of im­per­a­tive pro­gram­ming and evolved from pro­ce­dur­al pro­gram­ming. The latter basically deals with inert data processed by ex­e­cutable code:

  1. Data: Values, data struc­tures, variables
  2. Code: Ex­pres­sions, control struc­tures, functions

This is precisely the dif­fer­ence between object-oriented and pro­ce­dur­al pro­gram­ming: OOP combines data and functions into objects. An object is quasi a living data structure; because objects are not inert but have a behavior. Objects are therefore com­pa­ra­ble with machines or uni­cel­lu­lar organisms. While data is merely operated on, you interact with objects or objects interact with each other.

Let’s il­lus­trate the dif­fer­ence 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

Op­er­a­tions on prim­i­tives 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, “every­thing is an object”. Even a simple number includes the actual value as well as a set of methods that define op­er­a­tions on the value. Here is the example of the built-in succ function in Ruby:

# returns `43`
42.succ
Ruby

First, this is con­ve­nient because the func­tion­al­i­ty 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 in­stan­ti­at­ed 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 pro­gram­ming 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 pro­gram­ming work?

If you ask someone with pro­gram­ming ex­pe­ri­ence 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 pro­gram­ming are simpler and can be sum­ma­rized as follows:

  1. Objects en­cap­su­late their internal state.
  2. Objects receive messages via their methods.
  3. Methods are assigned dy­nam­i­cal­ly at runtime.

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

Objects en­cap­su­late their internal state

To un­der­stand what is meant by en­cap­su­la­tion, 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 prop­er­ties should be able to be changed ex­clu­sive­ly via defined in­ter­faces.

Let’s look at a few examples. We have an object car that rep­re­sents 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 cor­re­spond­ing message is issued. If there is not enough battery charge or the tank is empty, the motor remains off. If all con­di­tions 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 schemat­i­cal­ly 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 con­di­tions 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; tech­ni­cal­ly, 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 pro­gram­ming a smart­phone. Different objects represent func­tion­al­i­ties, e.g. the phone functions the flash­light, a call, a text message, etc. Usually, the in­di­vid­ual sub­com­po­nents are modeled 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 as­sign­ment of the methods

The third essential criterion in Alan Kay’s original de­f­i­n­i­tion of OOP is the dynamic al­lo­ca­tion 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 con­se­quence, the behavior of an object can be modified at runtime.

The dynamic as­sign­ment of methods has important im­pli­ca­tions for the technical im­ple­men­ta­tion of OOP func­tion­al­i­ty in pro­gram­ming languages. In practice, it doesn’t come up all too often. Nev­er­the­less, let’s look at an example. We’ll model the flash­light of the smart­phone as object flash­light. 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 flash­light breaks and we decide to issue an ap­pro­pri­ate 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 flash­light af­ter­wards, 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? In­stan­ti­a­tion and ini­tial­iza­tion

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 in­stan­ti­a­tion. In­stan­ti­a­tion is the process by which an object is brought into existence. In different OOP languages there are different mech­a­nisms of in­stan­ti­a­tion. Mostly one or more of the following mech­a­nisms are used:

  1. De­f­i­n­i­tion per object literal
  2. In­stan­ti­a­tion with con­struc­tor function
  3. In­stan­ti­a­tion from a class

JavaScript excels here because objects like numbers or strings can be defined directly as literals. A simple example is if we in­stan­ti­ate an empty object person and then assign the property name and a method greet. Sub­se­quent­ly, 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 in­stan­ti­at­ed a unique object. However, we often want to repeat the in­stan­ti­a­tion to create a set of similar objects. This case can also be easily covered in JavaScript. We create what’s called a con­struc­tor function that assembles an object when called. Our con­struc­tor 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 place­hold­er for an in­stan­ti­at­ed object. When a method is called, this ref­er­ences 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.

Fur­ther­more, in JavaScript we need the new keyword to correctly create the object instance. This is found es­pe­cial­ly in Java and C++, which dis­tin­guish 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 un­nec­es­sary. Python demon­strates 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 con­struc­tor function in JavaScript: Both serve as a blueprint by which similar objects can be in­stan­ti­at­ed as needed. At the same time, in languages such as Python and Ruby, a class acts as a re­place­ment 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 pro­gram­ming has come under in­creas­ing criticism since the beginning of the 21st century. Modern, func­tion­al languages with im­mutabil­i­ty and strong type systems are con­sid­ered more stable, more reliable and better in their per­for­mance. Nev­er­the­less, OOP is widely used and has distinct ad­van­tages. It is important to choose the right tool for each problem instead of relying on just one method­ol­o­gy.

Pro: En­cap­su­la­tion

An immediate advantage of OOP is the grouping of func­tion­al­i­ty. Instead of grouping multiple variables and functions into a loose col­lec­tion, they can be combined into con­sis­tent units. We will show the dif­fer­ence with an example: We model a bus and use two variables and one function for it. Pas­sen­gers 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 prob­lematc. The take_bus function accesses the bus_pas­sen­gers 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. Fur­ther­more, it is possible to “cheat”. We can continue to add pas­sen­gers 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 in­creas­ing the capacity of the bus. However, this violates as­sump­tions about physical reality, because an existing bus has a limited capacity that cannot be changed ar­bi­trar­i­ly af­ter­wards:

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

En­cap­su­lat­ing the internal state of objects protects against non­sen­si­cal or unwanted changes. Here is the same func­tion­al­i­ty in object-oriented code. We define a bus class and in­stan­ti­ate a bus with limited capacity. Adding pas­sen­gers is possible only through the cor­re­spond­ing 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 modeling

Object-oriented pro­gram­ming is par­tic­u­lar­ly well suited for modeling system. Thereby OOP is human intuitive, because we also think in objects, which can be clas­si­fied into cat­e­gories. Objects can be physical things as well as abstract concepts.

The in­her­i­tance via class hi­er­ar­chies found in many OOP languages also reflects human thought patterns. Let’s il­lus­trate the last point with an example. An animal is an abstract concept. Animals that actually occur are always concrete ex­pres­sions of a species. Depending on the species, animals have different char­ac­ter­is­tics. A dog cannot climb or fly, so it is limited to movements in two-di­men­sion­al 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 pro­gram­ming

A clear dis­ad­van­tage of OOP is the jargon, which is difficult to un­der­stand at the beginning. You’re forced to learn com­plete­ly new concepts, the meaning and purpose of which is often not clear from simple examples. This makes it easy to make mistakes; es­pe­cial­ly the modeling of in­her­i­tance hi­er­ar­chies requires a lot of skill and ex­pe­ri­ence.

One of the most frequent crit­i­cisms of OOP is the en­cap­su­la­tion of the internal state, which is actually intended as an advantage. This leads to dif­fi­cul­ties when par­al­leliz­ing 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 in­for­ma­tion en­cap­su­lat­ed elsewhere within a program.

The dynamic nature of object-oriented pro­gram­ming usually results in per­for­mance penalties. This is because fewer static op­ti­miza­tions can be made. The ten­den­tial­ly less pro­nounced type systems of pure OOP languages also make some static checks im­pos­si­ble. This means that errors only become visible at runtime. Newer de­vel­op­ments such as the JavaScript language Type­Script counter this.

Which pro­gram­ming languages support or are suited to OOP?

Almost all multi-paradigm languages are suitable for object-oriented pro­gram­ming. These include the well-known internet pro­gram­ming languages PHP, Ruby, Python and JavaScript. In contrast, OOP prin­ci­ples are largely in­com­pat­i­ble with the re­la­tion­al algebra un­der­ly­ing SQL. Special trans­la­tion layers known as “object re­la­tion­al mappers” (ORM) are used to bridge the “impedance mismatch”.

Even purely func­tion­al languages like Haskell usually do not provide native support for OOP. Im­ple­ment­ing OOP in C requires effort. In­ter­est­ing­ly, Rust is a modern language that does not need classes. Instead struct and enum are used as data struc­tures, behavior of which is defined by an impl keyword. With Traits, behavior can be grouped so in­her­i­tance and poly­mor­phism are rep­re­sent­ed. The design of the language reflects the OOP best practice “Com­po­si­tion over In­her­i­tance”.

Go to Main Menu