Evolution of Programming Languages

Object-Oriented Programming


Learning Objectives

  • You know the core principles of object-oriented programming (OOP) and know how object-oriented programming differs from structured programming.
  • You know of the “everything as an object” approach and its influence on modern programming languages.

Structured programming was a key step in improving code clarity and maintainability, but it had its limitations as there was no clear way to explicitly separate data and behavior. Object-oriented programming (OOP) filled this gap by introducing objects that encapsulate data and behavior, promoting modular, reusable, and flexible code.

Encapsulation, Inheritance, and Polymorphism

Object-oriented programming (OOP) is built on three core principles: encapsulation, inheritance, and polymorphism. These principles work together to promote modular, reusable, and flexible code.

Encapsulation

Encapsulation involves bundling data (attributes) and the methods (functions) that operate on that data into a single unit, typically called an object. This practice restricts direct access to the contents of an object, guarding the internal state from unintended use. By exposing a controlled interface, a programmer can ensure that that interactions with the object are predictable and safe.

For example, consider the following Java class that represents a bank account:

public class BankAccount {
  private double balance;

  public BankAccount(double initialBalance) {
    this.balance = initialBalance;
  }

  public double getBalance() {
    return balance;
  }

  public void deposit(double amount) {
    if(amount > 0) {
      balance += amount;
    }
  }

  public void withdraw(double amount) {
    if(amount > 0 && balance >= amount) {
      balance -= amount;
    }
  }
}

In this example, the balance field is marked as private so that it can only be modified through the public methods deposit and withdraw. This encapsulation ensures that the internal state of a bank account remains valid and consistent.

If you wish to learn more about Java, see e.g. the Java Programming MOOC from the University of Helsinki.

Inheritance

Inheritance allows creating a new class (a subclass) from an existing class (a superclass), where the subclass inherit attributes and methods from the superclass while also adding its own features or modifying existing ones. Inheritance promotes reusability and helps establishing a hierarchical relationship among classes.

As a classic example, consider the following Java code, where we define a base class Animal and extend it with a subclass Dog:

// Superclass
public class Animal {
  public void speak() {
    System.out.println("The animal makes a sound.");
  }
}

// Subclass that inherits from Animal
public class Dog extends Animal {
  @Override
  public void speak() {
    System.out.println("The dog barks.");
  }
}

public class Main {
  public static void main(String[] args) {
    Dog myDog = new Dog();
    myDog.speak();  // Output: "The dog barks."
  }
}

Above, Dog inherits the structure of Animal but overrides the method speak to provide dog-specific behavior. This inheritance relationship reduces redundancy and allows changes in the superclass to propagate to its subclasses.

Inheritance is powerful when used with care, but it can also lead to complex class hierarchies that are difficult to maintain. The “is-a” relationship should guide the use of inheritance: a subclass should be a more specific version of its superclass.

Polymorphism

Polymorphism allows objects of different classes to be treated as objects of a common superclass. In practice, this means that a single function or method can operate on objects of different types, and the correct method implementation is chosen at runtime based on the actual type.

This increases flexibility and makes it easier to extend or modify behavior without changing existing code. Let’s continue with the above example, adding a Cat, demonstrating polymorphism:

// Superclass
public class Animal {
  public void speak() {
    System.out.println("The animal makes a sound.");
  }
}

// Subclass that inherits from Animal
public class Dog extends Animal {
  @Override
  public void speak() {
    System.out.println("The dog barks.");
  }
}

// Subclass that inherits from Animal
public class Cat extends Animal {
  @Override
  public void speak() {
    System.out.println("The cat meows.");
  }
}

public class Main {
  public static void main(String[] args) {
    // Polymorphic behavior: Animal reference holding a Dog object
    Animal myAnimal = new Dog();
    myAnimal.speak();  // Output: "The dog barks."

    // Changing the reference to a Cat object
    myAnimal = new Cat();
    myAnimal.speak();  // Output: "The cat meows."
  }
}

This example illustrates how the same method call (speak()) results in different behavior depending on the type of object (Dog or Cat) that the Animal reference points to.

Generics and so on

In languages like Java, C#, and TypeScript, polymorphism can be further enhanced with features like generics, interfaces, and abstract classes. These features allow for more flexible and type-safe code, enabling developers to write reusable components that work with a variety of data types.


Loading Exercise...

Everything as an Object

The evolution of object-oriented programming began with early concepts introduced in languages such as Simula, widely regarded as the first object-oriented language. Simula laid the groundwork by modeling real-world entities using objects, a concept that later found full expression in Smalltalk.

In terms of contemporary languages, Smalltalk was revolutionary. It was the first language to treat everything as an object and provided an interactive environment, greatly influencing later languages. Smalltalk’s object-oriented principles, such as encapsulation, inheritance, and polymorphism, have become fundamental concepts in modern programming.

Many programming languages since Smalltalk such as Ruby, Python, Scala, and Dart treat everything as an object.

By treating everything as an object, these languages provide a consistent and unified model for representing data and behavior. This approach simplifies code organization and promotes a more intuitive understanding of complex systems.

Loading Exercise...

Object-Oriented vs Structured Programming

While object-oriented programming (OOP) and structured programming share common goals — improving code clarity, maintainability, and reliability — they approach code organization in fundamentally different ways.

Structured Programming Example

Structured programming relies on a top-down approach, using clear control structures like loops, conditionals, and functions to break down a program. Here’s a simple procedural (structured) Python example for calculating the area of a rectangle:

def calculate_area(width, height):
  return width * height

width = 5
height = 3
area = calculate_area(width, height)
print("Area:", area)  # Output: Area: 15

This example separates the logic into a function that performs a single task, adhering to the principles of structured programming.

Object-Oriented Programming Example

In contrast, object-oriented programming organizes code around objects that encapsulate both data and behavior. The following Python example encapsulates the rectangle concept within a class:

class Rectangle:
  def __init__(self, width, height):
    self.width = width
    self.height = height

  def area(self):
    return self.width * self.height

# Creating an object (instance of Rectangle)
rect = Rectangle(5, 3)
print("Area:", rect.area())  # Output: Area: 15

In the OOP example, the rectangle’s properties and the method to compute its area are bundled together. This encapsulation not only organizes the code logically but also makes it easier to extend — for example, by adding methods to compute the perimeter or to resize the rectangle.

Paradigm Differences

While in structured programming, we use functions to break down tasks, in object-oriented programming, we model real-world entities and their interactions. In terms of the rectangle example:

In OOP, we create a rectangle and ask it about its area. In contrast, in structured programming, we call a function to calculate the area of a rectangle, passing the width and height as arguments.

The difference between the two paradigms becomes more evident as systems grow in complexity. Structured programming excels in smaller or less complex tasks, where a linear, procedural flow is sufficient. Object-oriented programming, on the other hand, helps in scenarios where modeling real-world entities and their interactions is beneficial.

Object-oriented programming also helps in understanding and defining the problem domain more clearly, as it encourages thinking in terms of objects and their interactions. This modeling approach can lead to more maintainable and scalable systems.

Understanding the strengths and limitations of both paradigms helps in selecting the right approach, or blending both approaches, for a given problem. A balanced approach would be to use structured techniques for clarity within individual components, while employing object-oriented design to manage relationships and interactions in larger systems.

Loading Exercise...