Class Relationships
Composition
Has-a relationship (strong)
Composition is the strongest form of "has-a" relationship in Object-Oriented Design. It represents a strict whole-part ownership structure where the parts cannot survive independently of the whole. When the container (the "whole") is destroyed, all of its contained elements (the "parts") are automatically destroyed alongside it.
1. Learning Objectives
- Define Composition and identify its UML filled diamond representation.
- Distinguish Composition from Association and Aggregation based on ownership and lifecycles.
- Implement strict lifecycle binding by instantiating parts internally within the owner class.
- Avoid memory leaks in manual memory systems (destructors) and GC systems (reference leaks).
- Prevent external reference leakage using deep-copying techniques.
2. Problem Statement
In many domain-driven designs, certain entities are entirely dependent on their parent context. For instance, an OrderItem only makes sense within the context of an Order, and a Room has no meaning outside of a House.
If we allow the outer client code to create these child parts and pass them into the container (Aggregation), we expose their references to the outside world. This can lead to anomalies:
- External actors can modify the internal state of a room bypassing the house's controls.
- If the house is demolished in the database, the rooms might remain as "orphaned" records, causing data inconsistency.
We need a design pattern that enforces exclusive ownership, restricts direct child instantiation from the outside, and guarantees unified deletion.
3. Real-world Analogy
Think of a House and its Rooms:
- You cannot purchase a kitchen in a store, take it home, and then attach it to a house. The kitchen is built directly inside the walls of the house during construction.
- The kitchen belongs exclusively to that house. It cannot be shared simultaneously with your neighbor's house.
- If the house is demolished, the kitchen is destroyed as well. They share a coincident lifecycle.
This strong, exclusive containment represents Composition.
4. Theory
Composition represents a strong "has-a" relationship. In UML diagrams, it is represented by a filled diamond on the container's side.
Key Characteristics:
- Exclusive Ownership: A part can belong to at most one whole at a time. It cannot be shared.
- Coincident Lifecycle: The whole class is responsible for creating and destroying its parts. The parts cannot exist before or outlive the whole.
- Encapsulated Creation: Parts are instantiated directly inside the constructor or initialization methods of the whole class.
- No Cascading Leakage: If references to composed parts are exposed, they must be returned as read-only copies or unmodifiable views.
5. Visual Diagrams (UML & Memory structures)
Class Diagram
The filled black diamond indicates Composition. It is placed on the side of the container (House).
Object Diagram
Instances at runtime showing total ownership containment:
Memory Diagram (Verifying Cascaded Sweep)
When myHouse is set to null, the garbage collector sweeps the container. Since there are no other references to the Room instances on the stack, all composed room objects are swept in the same pass.
Stack Frame
Heap space (Swept together)
address: "123 Main St"
rooms: [@0xDE12, @0xDE89]
name: "Living Room"
Object Lifecycle
6. Syntax Explanation
Unlike Aggregation, child objects are instantiated inside the container.
- Java: Constructors of part classes are often package-private or inner classes. The parent instantiates them inside:
rooms.add(new Room(name, area));. - Python: Instantiation occurs inside the initializer or helper methods:
self._rooms.append(Room(name, area)). - C++: Can be implemented by nesting values directly inside vectors:
std::vector<Room> rooms;or using exclusive smart pointer scopes:std::vector<std::unique_ptr<Room>> rooms;. When the parent object is destroyed, the vector's elements are automatically cleaned up.
7. Step-by-Step Implementation
- Step 1: Create the
Roomclass. Make its constructor package-private or restricted so that external code cannot instantiate a Room independently. - Step 2: Create the
Houseclass. Declare a private collection to holdRoomobjects. - Step 3: Inside the
Houseconstructor, callnew Room(...)to instantiate the parts. Implement anaddRoom(String name, double area)method that creates and appends new rooms. - Step 4: Write a verification client to confirm that clearing the
Housereference leaves no accessible reference path to the inner rooms.
8. Complete Code (Mini Project)
9. Code Walkthrough
Let's look at how lifecycle binding is enforced in the code:
- Internal Creation: The constructor of
Housedirectly instantiates the rooms. We do not pass existingRoomreferences into the constructor. - Access Restriction: In the Java code, the constructor of
Roomis package-private (no access modifier). This prevents classes outside of thecom.revisealgo.lldpackage from instantiatingRoomdirectly. - Exclusive Ownership in C++: Using
std::vector<std::unique_ptr<Room>>ensures that only theHouseowns the pointers to the rooms. Aunique_ptrcannot be copied, preventing other classes from sharing ownership of the same room. When theHousedestructor is called, theunique_ptrs are cleaned up, releasing the rooms from the heap.
10. Execution Flow
- Construction: Client constructs
myHouse. The constructor immediately instantiates defaultRoomobjects inside the parent layout. - Operation: Client interacts with the house, querying metrics like total area. The client does not reference or modify room objects directly.
- Destruction: The
myHousestack reference is cleared or goes out of scope:- In C++, the destructor deallocates the rooms from the heap.
- In Java/Python, the garbage collector sweeps the rooms because there are no remaining reference paths from the stack.
11. Internal Working
At runtime, the memory footprint of composed objects is fully contained within the container's hierarchy:
- In C++, if we declare fields directly (e.g.
Room livingRoom;), the room data is allocated inside the parent's memory layout. Demolishing the parent cleans up the entire memory block in a single step. - In Java/Python, when
myHouse = nullis executed, the reference pointer pointing to the parent object on the heap is severed. The garbage collector checks for other references to the parent or its children. Because the child objects were created internally and never exposed, they have a reference count of zero and are cleaned up.
12. Complexity Analysis
- Time Complexity:
- Instantiation: $O(C)$ to construct the container and its $C$ composed parts.
- Destruction: $O(C)$ to clean up all parts in manual memory systems.
- Space Complexity: $O(C)$ to store the data for all composed parts on the heap.
13. Best Practices
- Create parts internally: Instantiate all composed objects inside the parent class's constructor or initialization methods.
- Restrict constructor access: Use package-private (Java) or private namespaces (C++) to prevent client code from instantiating parts independently.
- Do not leak references: If a client needs to inspect composed parts, return read-only views, deep copies, or primitive summaries rather than raw references.
- Prefer composition over inheritance: Composition provides a more flexible design than inheritance by letting you swap behaviors at runtime without coupling class hierarchies.
14. Common Mistakes
- Leaking references via getters: Returning raw pointers to composed objects (e.g.
return rooms;). This allows client code to modify internal lists and bypass the parent's business rules. - Allowing external instantiation: Leaving the constructor of the part class public, allowing developers to bypass the container class entirely.
- Memory leaks in C++: Using raw pointers (
Room*) inside the container class and forgetting to write a destructor that callsdelete, leaking the parts when the container goes out of scope. Use smart pointers likeunique_ptrto automate cleanup.
15. Interview Questions
Answer: In Aggregation, the lifecycle of the parts is independent of the whole. Parts are created outside and survive the destruction of the whole. In Composition, the lifecycles are tied. The whole creates the parts and destroys them when it is deleted.
Answer: Use
std::unique_ptr inside a vector or declare the child instances as direct member values of the parent class. Both approaches ensure that the parts are automatically cleaned up when the parent object is destroyed.
Answer: It means you should build complex behaviors by combining small, focused objects (Composition) rather than building deep class inheritance hierarchies. Composition makes the system more modular, easier to test, and flexible to change at runtime.
16. Practice Exercises
- Easy: Implement a
CarandEnginemodel where the engine is created inside the car's constructor and cannot exist independently. - Medium: Design an
OrderandOrderItemrelationship. Ensure that order items can only be added by passing parameters toorder.addItem(itemId, price, quantity), preventing direct instantiation of items in the client code. - Hard: Build a
Documenteditor where a document owns a list ofParagraphobjects, and each paragraph owns a list ofLineobjects. Write a deep copy constructor forDocumentthat duplicates the entire document hierarchy.
17. Challenge Problem
Design a transaction-safe file system simulator where a Folder owns a list of composed Files. Implement a deep copy constructor for Folder that duplicates the entire folder and file tree structures in memory without sharing any file references.
18. Summary
- Composition represents a strong whole-part relationship where the parts cannot survive without the whole.
- Parts are created directly inside the parent container to enforce coincident lifecycles.
- In UML, Composition is represented by a filled black diamond pointing to the owner class.
- Exposing references to composed parts should be avoided; return deep copies or unmodifiable views instead.
19. Cheat Sheet
| Relationship Type | Ownership Strength | UML Notation | Lifecycle Constraint |
|---|---|---|---|
| Association | None (Peers) | Solid line (or arrow) | Fully Independent |
| Aggregation | Weak whole-part | Hollow diamond pointing to whole | Independent survival |
| Composition | Strong whole-part | Filled diamond pointing to whole | Part dies with whole |
20. Quiz
1. What is the defining characteristic of Composition?
A) The parts can belong to multiple container instances
B) The parts are created externally and survive container deletion
C) The parts share a coincident lifecycle with the whole and cannot exist independently (Correct)
2. Which UML symbol represents Composition?
A) Hollow diamond pointing to whole
B) Solid filled diamond pointing to whole (Correct)
C) Dotted arrow pointing to part
3. Where should composed child objects be instantiated?
A) In a static utility class registry
B) Directly inside the constructor or initialization methods of the whole class (Correct)
C) In the client code, then passed via setters
4. What is a key design risk when exposing raw getter lists to composed elements?
A) It decreases thread safety speeds
B) It leaks references, allowing clients to bypass the container class and modify internal state (Correct)
C) It causes class inheritance compilation errors
5. In Java, how can we restrict client code from instantiating composed classes directly?
A) Use package-private or private constructors on the composed classes (Correct)
B) Mark the composed class as final
C) Make the container class abstract
6. What type of smart pointer is ideal to represent Composition in C++?
A) std::shared_ptr
B) std::unique_ptr (Correct)
C) std::weak_ptr
7. Why is "Prefer Composition over Inheritance" recommended in OOP design?
A) It yields flatter class structures that are more modular and flexible to change (Correct)
B) It speeds up compilation execution times
C) It forces all variables to reside on stack frames
8. What happens to composed order items when an order is deleted, if the database schema is configured correctly?
A) They are kept as orphaned records
B) They are deleted automatically via a cascade delete constraint (Correct)
C) They are reassigned to a default parent order
9. What complexity is associated with copying a composite object?
A) Shallow copies are sufficient
B) A deep copy is required to clone all composed parts and prevent reference sharing (Correct)
C) Space complexity is O(1)
10. What is a common mistake when implementing Composition in C++?
A) Failing to delete raw pointers inside the destructor, causing memory leaks (Correct)
B) Using value types inside container arrays
C) Declaring the container as final
21. Next Lesson Preview
In the next lesson, we will explore Dependency to see how classes can utilize other short-lived objects as method parameters without holding them as permanent attributes!