Understanding Relationships in OOP: Aggregation vs Composition

Relationships between classes are very important in the object-oriented programming paradigm. Classes and OOP are built to model real-world entities, and the relationships between classes are introduced to model the connections between real-world entities.

Classes without any relations between them are impractical in OOP, because usually in the real world, almost every entity is somehow connected to another entity.

Common Relationship Types

  • One to many — 1 entity is connected with more than 1 instance of another entity.
    Ex: Company(1) has employees(N)
  • Many to many — more than 1 instance of an entity are connected with more than 1 instance of another entity.
    Ex: Many courses(N) can be taught by many Lecturers(M)
  • Many to one — more than 1 instance of an entity are connected with 1 instance of another entity.
    Ex: Multiple Lecturers(N) can be assigned to work in 1 department(1)
  • One to one — 1 entity is connected with only 1 entity.
    Ex: 1 Person(1) has only 1 Mother(1)

Forms of Relationships

There are two main forms of relationships in OOP:

  1. IS-A (Inheritance)
  2. HAS-A (Association)

IS-A Relationship (Inheritance)

This is exactly Inheritance in OOP. That is, parent–child communication. Examples:

  • Car IS A Vehicle
  • Cat IS AN Animal
  • Manager IS AN Employee

All these examples denote that the subtype is in the form of the supertype, creating an is-a relationship.


HAS-A Relationship (Association)

This means the interconnection between two entities/objects in the real world, simply called Association. Examples:

  • Hotel HAS Rooms
  • School HAS Departments
  • Employee HAS Address

This represents the ownership of an entity in another. One entity belongs to another.


Types of Association in Java

There are two forms of Association that are possible in Java:

  1. Aggregation — loose coupling, weak
  2. Composition — tight coupling, strong

Aggregation

In aggregation, entities are loosely coupled together. Each entity can survive on its own, independently. There is only a dependency on the other. If the container is destroyed, the component should be able to survive.

UML diagram for aggregation (Add UML diagram here if desired)

Example:

Imagine we have an Employee class having id, name, and address. Here, address has street and city. Basically, we have two classes: Employee and Address. According to the aggregation definition, the Address should be independent, and the Employee is composed of an address. Address should be able to survive on its own.

Look at the below snippet. Employee is accepting an address object via the constructor, which means we need an address to create an Employee. Employee HAS an Address! 😎

public class AggregateEmployee {

    private final int id;
    private final String name;
    private final Address address;

    public AggregateEmployee(int id, String name, Address address) {
        this.id = id;
        this.name = name;
        this.address = address;
    }

    @Override
    public String toString() {
        return "AggregateEmployee{" +
            "id=" + id +
            ", name='" + name + '\'' +
            ", address=" + address +
        '}';
    }
}

Client code:

public class AggregationTest {
    public static void main(String[] args) {
        Address address = new Address("street 1", "city 1");
        AggregateEmployee e = new AggregateEmployee(1, "Tim", address);
        System.out.println(e);
    }
}

The address object can live in the code without any help of Employee! The outer world can create Address objects without any interference. It’s totally independent. So, we have implemented Aggregation using Java!


Composition

In contrast to aggregation, in composition, entities are tightly coupled together. The dependent entity cannot survive on its own. If the container is destroyed, the component is also destroyed and no longer exists.

UML diagram for composition (Add UML diagram here if desired)

Let’s take the same example: Employee and Address.

Now, Employee is accepting both parameters — city and street — which are needed to create an address. Instead of injecting the Address object via the constructor, now Employee is creating an Address object at runtime inside the constructor. And this Employee has a private inner class of Address!

public class CompositeEmployee {

    private final int id;
    private final String name;
    private final Address address;

    public CompositeEmployee(int id, String name, String street, String city) {
        this.id = id;
        this.name = name;
        this.address = new Address(street, city);
    }

    @Override
    public String toString() {
        return "CompositeEmployee{" +
            "id=" + id +
            ", name='" + name + '\'' +
            ", address=" + address +
        '}';
    }

    private static class Address {
        
        private final String street;
        private final String city;
        
        public Address(String street, String city) {
            this.street = street;
            this.city = city;
        }

        @Override
        public String toString() {
            return "Address{" +
                "street='" + street + '\'' +
                ", city='" + city + '\'' +
            '}';
        }
    }

}

Client code:

public static void main(String[] args) {
    CompositeEmployee e = new CompositeEmployee(1, "John", "street 1", "city 1");
    System.out.println(e);
}

Can the Address survive if the Employee is destroyed? If the Employee class is deleted? No! Since it’s a private inner class, it’s not accessible to the outer world. Clients cannot create independent objects of Address. It implies that Address is tightly coupled with the Employee. So, we have implemented Composition using Java!


Summary

  • Aggregation: Loose coupling, component can exist independently.
  • Composition: Strong coupling, component cannot exist independently.

These are common interview questions! Try to understand with real-world scenarios to make your life easier.