Composition
About
Composition is an object-oriented design principle where one object contains another object instead of inheriting from it. It is based on the "has-a" relationship rather than an "is-a" relationship (which is used in inheritance).
Key Idea: Instead of extending a class (inheritance), we create an instance of another class as a field inside our class and use its functionality.
Composition is not considered as one of the four fundamental OOP principles (Encapsulation, Abstraction, Inheritance, and Polymorphism). However, it is an important design concept within OOP.
Types of Composition
There are two main types of composition:
1. Strong Composition (Composition) – "Lifelong Dependency"
The contained object CANNOT exist without the containing object.
Strong ownership: If the main object is destroyed, the contained object is also destroyed.
Represented by a filled diamond in UML.
Example: A Car
has an Engine
(An Engine cannot exist without a Car)
class Engine {
void start() { System.out.println("Engine starts"); }
}
class Car {
private final Engine engine = new Engine(); // Engine belongs to Car
void startCar() {
engine.start();
}
}
2. Weak Composition (Aggregation) – "Independent Relationship"
The contained object CAN exist independently.
Weak ownership: If the main object is destroyed, the contained object can still exist.
Represented by an empty diamond in UML.
Example: A University
has Students
, but students can exist without a university.
class Student {
String name;
Student(String name) { this.name = name; }
}
class University {
private List<Student> students = new ArrayList<>();
void addStudent(Student student) {
students.add(student);
}
}
Better Reusability: Students can exist outside of a university.
Composition vs. Inheritance
Comparison
Feature
Composition (Preferred)
Inheritance (Use with Caution)
Relationship
"Has-a" (Car has-a Engine)
"Is-a" (Car is-a Vehicle)
Coupling
Loosely Coupled
Tightly Coupled
Code Reusability
High
Moderate
Flexibility
More Flexible (can change components easily)
Less Flexible (inherited behavior is fixed)
Encapsulation
Strong (hides implementation details)
Weak (exposes superclass methods)
Code Maintainability
Easier
Harder (deep inheritance chains lead to complexity)
Multiple Behaviour
Can change behaviour at runtime
Fixed at compile time
Example
A Car
contains an Engine
A Car
extends Vehicle
When to Use Composition?
When we need code reuse without tight coupling.
When we need flexibility in changing object behaviour at runtime.
When inheritance doesn’t make sense (e.g., a
Car
is not anEngine
, but it has anEngine
).
Why Composition is Preferred?
More flexible than inheritance (allows dynamic behavior changes).
Encapsulates implementation details (avoids exposing unnecessary behavior).
Avoids tight coupling (can replace contained objects without breaking existing code).
Enhances code reuse (reusable independent components).
When to Use Inheritance?
When objects have a clear hierarchical relationship (
is-a
).When you need default behaviour sharing across multiple classes.
Code Example: Composition vs. Inheritance
Bad Example (Using Inheritance Incorrectly)
class Engine {
void start() { System.out.println("Engine starts"); }
}
// Incorrect: Car is not an Engine!
class Car extends Engine { }
public class Main {
public static void main(String[] args) {
Car car = new Car();
car.start(); // Illogical: Car starts like an Engine?
}
}
Problem: The Car
class is not really an Engine
, but it extends it, which breaks the "is-a" principle.
Good Example (Using Composition Correctly)
class Engine {
void start() { System.out.println("Engine starts"); }
}
// Correct: Car has an Engine
class Car {
private Engine engine; // Composition
Car() { this.engine = new Engine(); } // Injecting dependency
void startCar() {
System.out.println("Car is starting...");
engine.start(); // Delegating functionality
}
}
public class Main {
public static void main(String[] args) {
Car car = new Car();
car.startCar(); // Outputs: Car is starting... Engine starts
}
}
Better Design: Car
contains an Engine
but does not inherit from it. This keeps the code flexible and maintainable.
Last updated