Message Passing in Object Oriented Programming (OOP)

We use the idea of message passing in both object-oriented programming as well as concurrent programming. In OOPS, message passing is a way for objects to communicate within a program. Similarly, in concurrent programming, we use it to communicate messages between processes or threads. 

In this blog, we will discuss how message passing works in OOPS. For concurrent programming, we will write a separate blog later.

Let's understand message passing with an analogy

Message passing is similar to the idea of sending and receiving messages in real life. Suppose we are at a hotel and we want to order room service. For this, we write a message (place an order) to the room service department and put it in the internal messaging system of the hotel. Here message contains details about what we want and our room number.

Similarly in OOP, one object can send a message to another object (room service department) with specific data (order details). Then other object performs the requested action based on the information provided in the message.

Concept of message passing in OOPS

In OOPS, objects are self-sufficient units that possess their own data and behaviour. Suppose one object (Sender) wants to communicate with another object (Receiver) to access data or carry out an action by calling its methods. For this, Sender will send a message to Receiver and then Receiver will perform the desired action or returns the requested data.

  • Message passing provides an interface between objects and helps them to communicate in a loosely coupled way while keeping their inner complexities hidden.
  • In Java, we implement message passing using method calls. For example, when Object A invokes a public method of Object B, it is sending a message to Object B. Note that Object A can only invoke Object B's public methods, not its private methods.

What is message passing among objects?

Here is a simple java code example of message passing:

class ObjectA {
    public int sendMessageToB() {
        ObjectB objectB = new ObjectB(5);
        int result = objectB.message(); // ObjectA is sending a message to ObjectB
        return result;
    }
}

class ObjectB {
    private int variable;

    public ObjectB(int value){
        variable = value;
    }
    public int message() {
        // doing some calculations with variable
        return variable + 1; // returning response to the message
    }
}

public class Main {
    public static void main(String[] args) {
        ObjectA objectA = new ObjectA();
        int result = objectA.sendMessageToB();
        System.out.println("ObjectB's response: " + result);
    }
}

In the above example, ObjectA creates an instance of ObjectB and sends a message by invoking ObjectB's public method message(). ObjectB's response is the return value of message(), which is variable + 1.

The above code is not loosely coupled. So the critical question is: How to make it loosely coupled? Here are some key steps:

  • Using interface: We add a new interface MessageSender, which declares the message() method. ObjectB implements this interface and defines its own implementation of the message().
  • Dependency injection: We remove the direct instantiation of ObjectB in ObjectA and pass an instance of ObjectB as a parameter to the sendMessageToB(). This will help ObjectA to interact with any object that implements the required interface, instead of being tightly coupled with ObjectB.
interface MessageSender {
    int message();
}

class ObjectB implements MessageSender {
    private int variable;

    public ObjectB(int value){
        variable = value;
    }

    @Override
    public int message() {
        // doing some calculations with variable
        return variable + 1; // returning response to the message
    }
}

class ObjectA {
    public int sendMessageTo(MessageSender sender) {
        int result = sender.message(); // ObjectA is sending a message to a generic sender
        return result;
    }
}

public class Main {
    public static void main(String[] args) {
        ObjectB objectB = new ObjectB(5);
        ObjectA objectA = new ObjectA();
        int result = objectA.sendMessageTo(objectB);
        System.out.println("ObjectB's response: " + result);
    }
}

Let’s take another example

In the following java code example, transferMoney method of BankAccount class transfers money from one bank account to another by taking the recipient BankAccount object as an argument. After this, the transferMoney method decreases the balance of the sender and calls the receiveMoney method on the recipient BankAccount object to increase its balance.

class BankAccount {
    private double balance;
  
    public BankAccount(double balance) {
        this.balance = balance;
    }
  
    public void transferMoney(BankAccount recipient, double amount) {
        this.balance = this.balance - amount;
        // Message passing
        recipient.receiveMoney(amount);
    }
  
    public void receiveMoney(double amount) {
        this.balance = this.balance + amount;
    }
  
    public double getBalance() {
        return this.balance;
    }
}

public class Main {
    public static void main(String[] args) {
        BankAccount account1 = new BankAccount(1000.0);
        BankAccount account2 = new BankAccount(500.0);
    
        account1.transferMoney(account2, 200.0);
    
        System.out.println("Account1 balance: " + account1.getBalance());
        System.out.println("Account2 balance: " + account2.getBalance());
    }
}

When we use message passing in OOP?

  • When one object needs to request data from another object
  • When one object needs to notify another object (or group of objects) of an event
  • When multiple objects need to collaborate to achieve a common goal
  • When one object needs to delegate responsibility to another object
  • When one object needs to control the behavior of another object
  • When one object needs to communicate with other objects in a concurrent system

For example, let's take an example when a sender object wants to send a message to receiver objects to notify it of an event that has occurred. Here receiver object will process the message and takes appropriate action based on the event.

class Event {
    private String eventName;

    public Event(String eventName) {
        this.eventName = eventName;
    }

    public String getEventName() {
        return eventName;
    }
}

class EventNotifier {
    private List<EventListener> listeners = new ArrayList<>();

    public void addEventListener(EventListener listener) {
        listeners.add(listener);
    }

    public void removeEventListener(EventListener listener) {
        listeners.remove(listener);
    }

    public void notifyEvent(Event event) {
        for (EventListener listener : listeners) {
            listener.handleEvent(event);
        }
    }
}

interface EventListener {
    void handleEvent(Event event);
}

class EventReceiver implements EventListener {
    private String receiverName;

    public EventReceiver(String receiverName) {
        this.receiverName = receiverName;
    }

    public void handleEvent(Event event) {
        System.out.println(receiverName + " Received " + event.getEventName());
        // Perform appropriate action based on the event
    }
}

public class Main {
    public static void main(String[] args) {
        EventNotifier notifier = new EventNotifier();
        
        EventListener receiver1 = new EventReceiver("Receiver 1");
        EventListener receiver2 = new EventReceiver("Receiver 2");
        EventListener receiver3 = new EventReceiver("Receiver 3");
        
        notifier.addEventListener(receiver1);
        notifier.addEventListener(receiver2);
        notifier.addEventListener(receiver3);
        
        notifier.notifyEvent(new Event("Event 1"));
    }
}

This idea is similar to the observer design pattern where objects (observers) register with a subject (observable) to receive notifications. When the state of the subject changes, it sends a message to all the registered observers to notify them of the change. On another side, some other design patterns in object-oriented programming also use the idea of message passing:

  • Command pattern
  • Mediator pattern
  • Proxy pattern
  • Chain of Responsibility pattern
  • Visitor pattern

All of these patterns use message passing to some extent, either to coordinate the actions of different objects or to allow objects to interact with each other in a loosely coupled manner. Note: We highly recommend exploring various code examples for each of the above scenarios.

Advantages and disadvantages of message passing

Message passing helps us to create modular, loosely coupled and self-contained objects that can easily interact with each other. In addition to this:

  • Message passing enables us to design scalable systems. The idea is simple: New objects can be added and existing ones can be modified without affecting the system functionality.
  • Message passing enables objects to communicate asynchronously. This will make it easier to build concurrent systems that can handle multiple tasks simultaneously.

There are several drawbacks to this approach if we do not implement and utilize message passing correctly. For example:

  • Complex interactions between multiple objects can result in a complicated software design. Debugging can also be difficult when object interactions are not clear.
  • When a large number of messages are involved, the overhead of sending and receiving messages can impact system performance.
  • Overusing message passing can create tightly coupled objects.

Real-life application of message passing

  • We use message passing to handle user interactions in graphical user interfaces (GUIs). For example, when we click a button, a message is sent to the button object to trigger an action.
  • Objects can send messages to each other to synchronize their activities in multithreaded applications. For example, one thread can send a message to another thread to signal that a task has been completed.
  • In a distributed system, objects on different devices communicate with each other by sending messages over the network.
  • In event-driven programming, objects send messages to each other to respond to events like system notifications. For example, a button object might send a message to a handler object to process a click event.
  • In simulation and modelling, objects represent real-world entities and send messages to each other to simulate their interactions. For example, a vehicle object might send a message to a road object to request permission to enter.

Please share your thoughts in the message below. Enjoy learning, Enjoy OOPS!

More from EnjoyAlgorithms

Self-paced Courses and Blogs