Composition in OOP: Strong Has-A Relationship
TL;DR
Composition is a strong “owns-a” relationship where one object contains other objects as parts, and those parts cannot exist independently without the whole. When the container is destroyed, its parts are destroyed too. This is one of the most fundamental ways to build complex objects from simpler ones.
Core Concept
What is Composition?
Composition represents a “whole-part” relationship where the part cannot exist without the whole. Think of a car and its engine: the engine is part of the car, and if you destroy the car, the engine (as part of that specific car) is destroyed too.
In code, composition means one class contains instances of other classes as member variables, and those instances are created when the container is created. The container owns its parts and is responsible for their entire lifecycle.
Key Characteristics
Strong Ownership: The container object owns its parts. The parts are created by the container and exist only as long as the container exists.
Lifecycle Dependency: When the container is destroyed, all its parts are automatically destroyed. There’s no independent existence for the parts.
Exclusive Relationship: Parts typically belong to exactly one container. A heart belongs to one body, not shared between multiple bodies.
Why Composition Matters
Composition is a cornerstone of good object-oriented design because it:
- Promotes code reuse without the tight coupling of inheritance
- Models real-world relationships naturally (a house has rooms, a book has pages)
- Enables encapsulation by hiding complex internal structures
- Supports the principle “favor composition over inheritance” — a fundamental OOP guideline
Composition vs. Other Relationships
Composition vs. Aggregation: Aggregation is a weaker “has-a” relationship where parts can exist independently. A university has students, but students can exist without that specific university.
Composition vs. Inheritance: Inheritance is an “is-a” relationship. A dog is an animal. Composition is “has-a” or “owns-a”. A dog has a heart.
Composition is generally preferred over inheritance because it’s more flexible and doesn’t create tight coupling between classes.
Visual Guide
Composition Relationship Diagram
classDiagram
class Car {
-engine: Engine
-wheels: List~Wheel~
+start()
+drive()
}
class Engine {
-horsepower: int
+ignite()
}
class Wheel {
-size: int
+rotate()
}
Car *-- Engine : owns
Car *-- Wheel : owns
note for Car "Car owns Engine and Wheels.\nThey cannot exist without Car."
Composition is shown with a filled diamond. The Car owns its Engine and Wheels — they are created with the Car and destroyed with it.
Lifecycle Dependency
sequenceDiagram
participant Client
participant Car
participant Engine
participant Wheel
Client->>Car: create Car()
activate Car
Car->>Engine: create Engine()
activate Engine
Car->>Wheel: create Wheel()
activate Wheel
Car->>Wheel: create Wheel()
activate Wheel
Note over Car,Wheel: Car owns these parts
Client->>Car: destroy Car
Car->>Engine: destroy
deactivate Engine
Car->>Wheel: destroy
deactivate Wheel
Car->>Wheel: destroy
deactivate Wheel
deactivate Car
Note over Client,Wheel: Parts destroyed with container
When a Car is created, it creates its parts. When the Car is destroyed, all parts are destroyed automatically.
Examples
Example 1: Basic Composition - House and Rooms
class Room:
def __init__(self, name, area):
self.name = name
self.area = area
def __str__(self):
return f"{self.name} ({self.area} sq ft)"
class House:
def __init__(self, address):
self.address = address
# House OWNS these rooms - they're created here
self.rooms = [
Room("Living Room", 300),
Room("Bedroom", 200),
Room("Kitchen", 150)
]
def get_total_area(self):
return sum(room.area for room in self.rooms)
def __str__(self):
room_list = ", ".join(str(room) for room in self.rooms)
return f"House at {self.address}\nRooms: {room_list}\nTotal: {self.get_total_area()} sq ft"
# Usage
my_house = House("123 Main St")
print(my_house)
# Output:
# House at 123 Main St
# Rooms: Living Room (300 sq ft), Bedroom (200 sq ft), Kitchen (150 sq ft)
# Total: 650 sq ft
# When my_house is deleted, all rooms are deleted too
del my_house # Rooms are destroyed automatically
Key Points:
- The
HousecreatesRoomobjects in its constructor - Rooms don’t exist before the house is created
- Rooms can’t be shared between houses
- When the house is destroyed, rooms are destroyed
Try it yourself: Add a Bathroom room to the house and a method to count rooms by type.
Example 2: Composition with Multiple Levels - Computer System
class CPU:
def __init__(self, cores, speed_ghz):
self.cores = cores
self.speed_ghz = speed_ghz
def process(self):
return f"Processing on {self.cores} cores at {self.speed_ghz}GHz"
class Memory:
def __init__(self, size_gb):
self.size_gb = size_gb
def store(self, data):
return f"Storing {data} in {self.size_gb}GB memory"
class Motherboard:
def __init__(self, cpu_cores, cpu_speed, memory_size):
# Motherboard OWNS CPU and Memory
self.cpu = CPU(cpu_cores, cpu_speed)
self.memory = Memory(memory_size)
def boot(self):
return f"Booting...\n{self.cpu.process()}\n{self.memory.store('OS')}"
class Computer:
def __init__(self, brand):
self.brand = brand
# Computer OWNS Motherboard (which owns CPU and Memory)
self.motherboard = Motherboard(cpu_cores=8, cpu_speed=3.5, memory_size=16)
def start(self):
return f"{self.brand} Computer Starting:\n{self.motherboard.boot()}"
# Usage
my_computer = Computer("TechBrand")
print(my_computer.start())
# Output:
# TechBrand Computer Starting:
# Booting...
# Processing on 8 cores at 3.5GHz
# Storing OS in 16GB memory
# Nested composition: Computer owns Motherboard, Motherboard owns CPU and Memory
# When computer is destroyed, everything is destroyed
del my_computer # Destroys Motherboard, CPU, and Memory
Key Points:
- Multi-level composition:
Computer→Motherboard→CPU/Memory - Each level owns its parts completely
- Destruction cascades: destroying
Computerdestroys all nested parts - Parts are created in constructors, not passed in
Try it yourself: Add a HardDrive class to the Motherboard and implement a save_file() method.
Example 3: Composition vs. Aggregation Comparison
# COMPOSITION: Engine belongs to Car exclusively
class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower
class Car:
def __init__(self, model):
self.model = model
# Car CREATES and OWNS the engine
self.engine = Engine(250)
def start(self):
return f"{self.model} starting with {self.engine.horsepower}hp engine"
car = Car("Sedan")
print(car.start()) # Output: Sedan starting with 250hp engine
# Engine cannot exist without this car
# AGGREGATION (for contrast): Driver can exist independently
class Driver:
def __init__(self, name):
self.name = name
class Taxi:
def __init__(self, driver):
# Taxi USES driver, but doesn't own them
self.driver = driver # Driver passed in, not created
def drive(self):
return f"{self.driver.name} is driving the taxi"
driver = Driver("John") # Driver exists independently
taxi = Taxi(driver) # Taxi uses existing driver
print(taxi.drive()) # Output: John is driving the taxi
del taxi # Taxi destroyed, but driver still exists
print(driver.name) # Output: John (driver still exists)
Key Differences:
- Composition:
CarcreatesEngineinternally — strong ownership - Aggregation:
TaxireceivesDriverexternally — weak relationship - Lifecycle: Engine dies with Car; Driver survives Taxi
Try it yourself: Create a Playlist class that owns Song objects (composition) vs. a Library class that references existing songs (aggregation).
Language-Specific Notes
Java:
public class House {
private List<Room> rooms; // Composition
public House(String address) {
this.rooms = new ArrayList<>();
rooms.add(new Room("Living Room", 300));
// Rooms are created and owned by House
}
}
C++:
class House {
private:
std::vector<Room> rooms; // Composition by value
// Rooms are destroyed when House is destroyed
public:
House(std::string address) {
rooms.push_back(Room("Living Room", 300));
}
};
Python’s Garbage Collection: Python automatically destroys objects when references are gone. In languages like C++, you must explicitly manage memory for composed objects.
Common Mistakes
1. Confusing Composition with Aggregation
Mistake: Passing objects into the constructor and calling it composition.
# This is AGGREGATION, not composition
class Car:
def __init__(self, engine): # Engine passed in
self.engine = engine # Car doesn't own it
# Correct composition:
class Car:
def __init__(self):
self.engine = Engine() # Car creates and owns engine
Why it matters: In interviews, you need to distinguish these relationships. Composition means the container creates and owns the parts.
2. Creating Parts Outside the Container
Mistake: Creating composed objects externally instead of in the constructor.
# Wrong: Parts created outside
engine = Engine()
car = Car()
car.engine = engine # Not true composition
# Correct: Parts created inside
class Car:
def __init__(self):
self.engine = Engine() # Created internally
Why it matters: True composition means the container controls the entire lifecycle of its parts. External creation breaks this contract.
3. Allowing Parts to Outlive the Container
Mistake: Returning references to internal parts that can be stored elsewhere.
class House:
def __init__(self):
self.rooms = [Room("Bedroom")]
def get_rooms(self):
return self.rooms # Returns reference!
# Problem:
house = House()
rooms = house.get_rooms() # rooms now references internal list
del house # House destroyed, but rooms list still accessible
Better approach: Return copies or use read-only access.
def get_rooms(self):
return self.rooms.copy() # Return a copy
4. Overusing Composition When Inheritance Makes Sense
Mistake: Using composition for “is-a” relationships.
# Wrong: Dog is-a Animal, not has-a Animal
class Dog:
def __init__(self):
self.animal = Animal() # Awkward
# Correct: Use inheritance for is-a
class Dog(Animal):
pass
Rule of thumb: Use composition for “has-a” or “owns-a”, inheritance for “is-a”.
5. Not Considering Deep vs. Shallow Copying
Mistake: Shallow copying composed objects when you need deep copies.
import copy
class House:
def __init__(self):
self.rooms = [Room("Bedroom")]
# Shallow copy problem:
house1 = House()
house2 = copy.copy(house1) # Shallow copy
house2.rooms[0].name = "Kitchen" # Modifies house1's room too!
# Solution: Deep copy when needed
house2 = copy.deepcopy(house1) # Now they're independent
Why it matters: In composition, you often need to ensure complete independence between objects, especially when copying.
Interview Tips
1. Be Ready to Explain the Difference
Common question: “What’s the difference between composition and inheritance?”
Strong answer structure:
- Composition: “has-a” relationship, flexible, loose coupling
- Inheritance: “is-a” relationship, rigid, tight coupling
- Example: “A car has an engine (composition), but a sedan is a car (inheritance)”
- Conclude with: “I prefer composition when possible because it’s more flexible and easier to test”
2. Draw the Diagram
When asked about composition, offer to draw a UML diagram. Interviewers love this.
What to draw:
- Two boxes (classes)
- Filled diamond on the container side
- Line connecting them
- Label: “owns” or “1 to many”
Say: “The filled diamond shows composition — the container owns the parts and they can’t exist independently.”
3. Know the “Favor Composition Over Inheritance” Principle
You might be asked: “When should you use composition instead of inheritance?”
Answer with specifics:
- “When the relationship is ‘has-a’ not ‘is-a’”
- “When you need flexibility to change behavior at runtime”
- “When you want to avoid deep inheritance hierarchies”
- “When you need to combine behaviors from multiple sources”
Example: “Instead of inheriting from multiple classes (which Python allows but gets messy), I’d compose objects with different behaviors.”
4. Discuss Lifecycle Management
Advanced question: “How do you handle memory management with composition?”
Python answer: “Python’s garbage collector handles it automatically. When the container is destroyed and no other references exist, composed objects are cleaned up.”
C++ answer: “In C++, I use RAII — composed objects are destroyed automatically when the container’s destructor runs. For dynamic allocation, I use smart pointers like unique_ptr to ensure proper cleanup.”
Java answer: “Java’s garbage collector handles it, but I’m careful about resource cleanup. For resources like file handles, I implement AutoCloseable and use try-with-resources.”
5. Provide Real-World Design Examples
Be prepared to design a system using composition:
Example prompt: “Design a Library system.”
Strong approach:
- Identify composition relationships: “A Library owns Books, a Book owns Pages”
- Identify aggregation: “A Library has Members, but Members exist independently”
- Explain: “I’d use composition for Books and Pages because they’re tightly coupled. A Page doesn’t make sense without its Book.”
6. Code It Live
If asked to implement composition:
- Start with the simplest case (one container, one part)
- Create parts in the constructor
- Show how destruction works
- Add a method that uses the composed objects
Template:
class Container:
def __init__(self):
self.part = Part() # Create part here
def use_part(self):
return self.part.do_something()
7. Mention Testing Benefits
Advanced tip: “Composition makes testing easier because I can mock the composed objects. With inheritance, I’m stuck with the parent class’s behavior.”
This shows you think about maintainability and testing — highly valued in interviews.
Key Takeaways
-
Composition is a strong “owns-a” relationship where the container creates and owns its parts, and parts cannot exist independently. When the container is destroyed, all parts are destroyed.
-
Create parts in the constructor of the container class. This ensures the container controls the entire lifecycle of its parts, which is the defining characteristic of composition.
-
Composition vs. Aggregation vs. Inheritance: Composition is “owns-a” with strong lifecycle dependency, aggregation is “has-a” with independent parts, and inheritance is “is-a” for type relationships.
-
Favor composition over inheritance for flexibility, loose coupling, and easier testing. Use composition when you need “has-a” relationships or want to combine behaviors from multiple sources.
-
In interviews, be ready to: explain the difference between composition and other relationships, draw UML diagrams with filled diamonds, discuss lifecycle management, and design systems using composition appropriately.