What is a programming paradigm?

Reading time5 min

In brief

Article summary

In this article, we introduce the concept of programming paradigms and how they are used in programming. Then, we focus on procedural programming and its limitations when it comes to develop and maintain programs.

Main takeaways

  • A programming paradigm is a method or style of programming that defines a set of principles, techniques, and patterns for structuring code to solve problems on a computer.

  • There are different types of programming paradigms, including procedural (or imperative) programming, functional programming, logic programming and object-oriented paradigm. A programming language can support one or more programming paradigms. In Python and Java, for example, it’s possible to write programs in the imperative (and procedural), object-oriented and functional paradigms.

  • The object-oriented paradigm was introduced to overcome the concerns of procedural programming: the difficulty of creating and maintaining programs to solve increasingly complex problems.

Article contents

1— What is a programming paradigm?

From their very beginning, computer programs have combined algorithms (i.e., the instructions used to solve a problem) and data (i.e., the information manipulated by the algorithms). It’s the way these two aspects are structured and organized that has evolved over time. This structuring and organization of computer code is what we call a programming paradigm.

Definition

A programming paradigm is a method or style of programming that defines a set of principles, techniques, and patterns for structuring code to solve problems on a computer.

2—Types of programming paradigms, the case of procedural programming

Programming paradigms can be classified into different categories including imperative, functional or logic programming paradigm. So far in this course, your programs followed the imperative paradigm: the program execution is given by the order of control instructions present in the code. More precisely, you’ve used procedural programming, in which a program is broken down into a series of functions/procedures whose purpose is to carry out a particular task.

As seen in the session dedicated to functions, this paradigm has advantages in terms of program modularity and ease of code reuse and maintenance. However, it also has a number of disadvantages related to the need to use global data:

  • It makes it more difficult to understand a program, debug it and modify it later.

  • When debugging a program, if a global variable contains erroneous information, it is more difficult to find the source of the error, as the variable may have been modified in any part of the program.

  • When modifying a program, it’s complex to change the way a global variable is handled, because you have to understand the whole program, since the variable can be modified from any function.

As an example, consider the code bellow. It corresponds to a program (using the procedural programming paradigm) that manage the transactions history in a banking system. The program includes functions to deposit, withdraw, and retrieve the transactions history and current balance. All these functions manipulate the same data stored in the transactions variable: the amount of each deposit or withdrawal operation. In the given implementation, each element in the transactions list is a deposit or withdrawal amount:

  • As the transactions variable is global, its value can be modified anywhere in the program, which can be dangerous and lead to errors. For example, it would be possible to modify all withdrawal operations and convert them into deposit operations!

  • Let us suppose that each element of the transactions variable should contain the amount of a deposit or withdrawal operation and a timestamp corresponding to when the operation was carried out. In this case, you need to understand how the variable is used in the different functions and modify the code accordingly which again can be tedious and lead to errors.

# Global array to store transactions
# Declared here to be accessible by all functions
# It is a list of tuples, each tuple contains the user and the amount of the transaction
transactions = []

def deposit(user: str, amount: float) -> None:
    """
    Deposit money into an account.
    This function adds a new transaction to the list of transactions.
    It also checks if the amount to deposit is valid.
    user: the account to deposit money into
    amount: the amount to deposit
    """
    global transactions
    if amount > 0:
        transactions.append((user,amount))  # Add deposit amount
        print(f"Deposited {amount}, new balance: {calculate_balance(user)}")
    else:
        print("Invalid deposit amount")

def withdraw(user: str, amount: float) -> None:
    """
    Withdraw money from an account.
    This function adds a new transaction to the list of transactions.
    It also checks if the amount to withdraw is valid.
    user: the account to withdraw money from
    amount: the amount to withdraw
    """
    global transactions
    balance = calculate_balance(user)
    if 0 < amount <= balance:
        transactions.append((user,-amount))  # Add withdrawal amount
        print(f"Withdrew {amount}, new balance: {calculate_balance(user)}")
    else:
        print("Invalid withdraw amount")

def calculate_balance(user: str) -> float:
    """
    Calculate the balance of an account.
    This function sums all the transactions of a user to calculate the balance.
    It returns the balance of the account.
    user: the account to calculate the balance of
    """
    return sum(t[1] for t in transactions if t[0] == user)

def get_transaction_history(user: str) -> List[tuple]:
    """
    Get the transaction history of an account.
    This function filters the transactions of a user and returns them.
    It returns the transaction history of the account.
    user: the account to get the transaction history of
    """
    return [t for t in transactions if t[0] == user]

def main():
    """
    Main function to test the transactions history program.
    """
    # Perform a series of deposits and withdrawals
    deposit("Alice", 500)
    withdraw("Alice", 200)
    deposit("Bob", -50)  # Invalid deposit
    withdraw("Bob", 2000)  # Invalid withdraw

    print("Transaction history:", get_transaction_history("Alice"))
    print("Final balance:", calculate_balance("Alice"))

if __name__ == "__main__":
    """
    This block of code is executed when the script is run.
    It calls the main function to test the transactions history program.
    """
    main()
public class TransactionHistory {

    // Inner class to represent a transaction
    // The keyword 'record' makes this class meant to store data
    // It is a shorthand for a class with private final fields, a constructor, and getters
    // Each transaction has a user and an amount
    record Transaction(String user, double amount) {
    }

    // Global list to store transactions
    // The keyword 'final' makes this list immutable
    // The keyword 'static' makes this list shared among all instances of the class
    private final static List<Transaction> transactions = new ArrayList<>();

    /**
     * Deposit money into an account.
     * This method adds a new transaction to the list of transactions.
     * It also checks if the amount to deposit is valid.
     *
     * @param user   the account to deposit money into
     * @param amount the amount to deposit
     * @implSpec making this method static allows it to be called without creating an instance of the class
     */
    public static void deposit(String user, double amount) {
        if (amount > 0) {
            transactions.add(new Transaction(user, amount));
            System.out.println("Deposited " + amount + ", new balance: " + calculateBalance(user));
        } else {
            System.out.println("Invalid deposit amount");
        }
    }

    /**
     * Withdraw money from an account.
     * This method adds a new transaction to the list of transactions.
     * It also checks if the amount to withdraw is valid.
     *
     * @param user   the account to withdraw money from
     * @param amount the amount to withdraw
     * @implSpec making this method static allows it to be called without creating an instance of the class
     */
    public static void withdraw(String user, double amount) {
        double balance = calculateBalance(user);
        if (0 < amount && amount <= balance) {
            transactions.add(new Transaction(user, -amount));
            System.out.println("Withdrew " + amount + ", new balance: " + calculateBalance(user));
        } else {
            System.out.println("Invalid withdraw amount");
        }
    }

    /**
     * Calculate the balance of an account.
     * This method sums all the transactions of a user to calculate the balance.
     * It uses the stream API to filter the transactions of a user and sum them.
     * The method returns the balance of the account.
     *
     * @param user the account to calculate the balance of
     * @return the balance of the account
     * @implSpec making this method static allows it to be called without creating an instance of the class
     */
    public static double calculateBalance(String user) {
        return transactions.stream()
                .filter(t -> t.user().equals(user))
                .mapToDouble(Transaction::amount)
                .sum();
    }

    /**
     * Get the transaction history of an account.
     * This method filters the transactions of a user and returns them.
     * It uses the stream API to filter the transactions of a user and collect them into a list.
     * The method returns the transaction history of the account.
     *
     * @param user the account to get the transaction history of
     * @return the transaction history of the account
     * @implSpec making this method static allows it to be called without creating an instance of the class
     */
    public static List<Transaction> getTransactionHistory(String user) {
        return transactions.stream()
                .filter(t -> t.user().equals(user))
                .collect(Collectors.toList());
    }

    /**
     * Main method to test the TransactionHistory class.
     * This block of code is executed when the script is run.
     *
     * @param args the command-line arguments -- not used
     */
    public static void main(String[] args) {
        // Perform a series of deposits and withdrawals
        deposit("Alice", 500);
        withdraw("Alice", 200);
        deposit("Bob", -50);  // Invalid deposit
        withdraw("Bob", 2000);  // Invalid withdraw

        System.out.println("Transaction history: " + getTransactionHistory("Alice"));
        System.out.println("Final balance: " + calculateBalance("Alice"));
    }
}

Over time, the problems to be solved by computer programs grew in complexity - accounting, automatic games, understanding and translating natural languages, decision support, graphical programs, and so on. The architecture and functionning of a procedural program to solve a given problem became difficult to produce, understand and maintain. It therefore became vital to invent simple-to-implement computer mechanisms that would reduce this complexity and bring program writing closer to human ways of posing and solving problems. This is what the object-oriented programming (OOP) paradigm aims to propose.

To go further

Important

The content of this section is optional. It contains additional material for you to consolidate your understanding of the current topic.

To go beyond

Important

The content of this section is very optional. We suggest you directions to explore if you wish to go deeper in the current topic.