Understanding Composition in Object-Oriented Programming
Composition is a "has-a" relationship in object-oriented programming. It describes a relationship where one class contains an instance (or instances) of another class. Importantly, the lifetime of the contained objects depends on the lifetime of the container object. When the container object is destroyed, the contained objects should also be destroyed.
Key Characteristics of Composition:
Ownership: The container (parent) class owns the components (child objects).
Lifetime Dependency: If the parent object is deleted, the child objects are also deleted.
Encapsulation: The contained objects are often tightly encapsulated within the parent.
Example: Composition in a Practical Scenario
Let’s create a Hospital Management System where a Hospital
is composed of multiple Departments
. If the hospital is closed, its departments are also destroyed.
Classes in Our Example
Department
: Represents a department in the hospital (e.g., Cardiology, Neurology).Hospital
: Composed of multiple departments.
Code Implementation
Department Class
public class Department
{
public string Name { get; private set; }
public int NumberOfStaff { get; private set; }
public Department(string name, int numberOfStaff)
{
Name = name;
NumberOfStaff = numberOfStaff;
}
public void DisplayDetails()
{
Console.WriteLine($"Department: {Name}, Staff: {NumberOfStaff}");
}
}
using System;
using System.Collections.Generic;
public class Hospital
{
public string Name { get; private set; }
private List<Department> Departments;
public Hospital(string name)
{
Name = name;
Departments = new List<Department>();
}
// Add a department to the hospital
public void AddDepartment(string name, int numberOfStaff)
{
var department = new Department(name, numberOfStaff);
Departments.Add(department);
Console.WriteLine($"Department '{name}' added to {Name} Hospital.");
}
// Display details of all departments
public void DisplayHospitalDetails()
{
Console.WriteLine($"Hospital: {Name}");
Console.WriteLine("Departments:");
foreach (var department in Departments)
{
department.DisplayDetails();
}
}
// Close the hospital and remove all departments
public void CloseHospital()
{
Console.WriteLine($"Closing {Name} Hospital...");
Departments.Clear(); // All departments are destroyed when the hospital closes.
Console.WriteLine("All departments have been removed.");
}
}
Usage Example
Usage Example
class Program
{
static void Main(string[] args)
{
// Create a hospital
var hospital = new Hospital("City General");
// Add departments
hospital.AddDepartment("Cardiology", 25);
hospital.AddDepartment("Neurology", 20);
hospital.AddDepartment("Emergency", 50);
// Display hospital details
hospital.DisplayHospitalDetails();
// Close the hospital
hospital.CloseHospital();
// Try to display hospital details again
hospital.DisplayHospitalDetails();
}
}
Output
Explanation of Composition in This Example
Ownership:
- The
Hospital
class owns theDepartment
instances. These departments are not shared with other objects or hospitals.
- The
Lifetime Dependency:
- When the
CloseHospital()
method is called, all departments are cleared from theDepartments
list, symbolizing their destruction when the hospital is closed.
- When the
Encapsulation:
- The
Departments
list is private to theHospital
class, ensuring that only theHospital
can manage its departments.
- The
Other Practical Scenarios for Composition
Car and Engine: A
Car
is composed of anEngine
. If theCar
is destroyed, theEngine
is destroyed too.Library and Books: A
Library
is composed of multipleBooks
. If theLibrary
is closed, all itsBooks
are no longer accessible.House and Rooms: A
House
is composed ofRooms
. If theHouse
is destroyed, theRooms
cease to exist.
Best Practices
Use composition to model relationships where the parent object strongly owns its child objects.
Avoid exposing the contained objects (e.g.,
Departments
inHospital
) directly to external classes. Instead, provide methods in the parent class to manage them (AddDepartment
,DisplayHospitalDetails
).
Here are some real-world examples of good and bad practices involving composition in object-oriented programming (OOP). These examples will help illustrate when and how composition is used effectively and where it can go wrong.
Good Practices in Composition
Example 1: Car and Engine
Scenario: A Car
is composed of an Engine
. The engine cannot exist without the car, and its lifecycle depends on the car.
Code Example (Good Practice):
public class Engine
{
public int HorsePower { get; private set; }
public Engine(int horsePower)
{
HorsePower = horsePower;
}
public void Start()
{
Console.WriteLine($"Engine with {HorsePower} HP started.");
}
}
public class Car
{
public string Model { get; private set; }
private Engine Engine;
public Car(string model, int horsePower)
{
Model = model;
Engine = new Engine(horsePower); // Composition: Car owns the Engine
}
public void StartCar()
{
Console.WriteLine($"Starting car: {Model}");
Engine.Start();
}
}
Why It's a Good Practice:
The
Engine
object is encapsulated within theCar
class.The lifecycle of the
Engine
depends on theCar
object.The
Car
controls how theEngine
is used.
Usage:
csharpCopyEditCar car = new Car("Toyota Corolla", 150);
car.StartCar();
Example 2: Library and Books
Scenario: A Library
is composed of multiple Books
. If the library is closed, its books are also destroyed.
Code Example (Good Practice):
csharpCopyEditpublic class Book
{
public string Title { get; private set; }
public Book(string title)
{
Title = title;
}
}
public class Library
{
private List<Book> Books;
public Library()
{
Books = new List<Book>();
}
public void AddBook(string title)
{
Books.Add(new Book(title));
}
public void DisplayBooks()
{
foreach (var book in Books)
{
Console.WriteLine($"Book: {book.Title}");
}
}
}
Why It's a Good Practice:
The
Book
objects are tightly encapsulated within theLibrary
class.The
Library
provides clear methods to manage itsBooks
.The
Books
are destroyed if theLibrary
object is destroyed.
Bad Practices in Composition
Example 1: Exposing Internal Objects
Scenario: A Car
exposes its Engine
directly to external classes, breaking encapsulation.
Code Example (Bad Practice):
csharpCopyEditpublic class Engine
{
public int HorsePower { get; set; }
}
public class Car
{
public Engine Engine; // Exposed directly (Bad Practice)
public Car(int horsePower)
{
Engine = new Engine { HorsePower = horsePower };
}
}
What's Wrong Here:
The
Engine
object is exposed publicly, allowing external classes to modify it directly.This breaks encapsulation, making the
Car
class less secure and harder to maintain.
Example of Misuse:
csharpCopyEditCar car = new Car(150);
car.Engine.HorsePower = 300; // External modification of the internal Engine object
Example 2: Overusing Composition
Scenario: A University
is composed of Student
, Professor
, and Course
. Instead of grouping them logically, all relationships are modeled through composition, creating unnecessary complexity.
Code Example (Bad Practice):
csharpCopyEditpublic class University
{
public Student Student;
public Professor Professor;
public Course Course;
public University(Student student, Professor professor, Course course)
{
Student = student;
Professor = professor;
Course = course;
}
}
public class Student
{
public string Name { get; set; }
}
public class Professor
{
public string Name { get; set; }
}
public class Course
{
public string CourseName { get; set; }
}
What's Wrong Here:
This unnecessarily couples the
University
withStudent
,Professor
, andCourse
, even though these objects might not have dependent lifecycles.If a
Student
object is updated, theUniversity
object might also need to be updated, increasing maintenance overhead.
Better Approach: Instead of composition, use an association or aggregation for loosely coupled relationships:
csharpCopyEditpublic class University
{
public List<Student> Students { get; set; }
public List<Professor> Professors { get; set; }
public List<Course> Courses { get; set; }
public University()
{
Students = new List<Student>();
Professors = new List<Professor>();
Courses = new List<Course>();
}
}
Example 3: Cyclical Composition
Scenario: A Department
is composed of Employee
, and Employee
is composed of Department
. This creates a circular dependency.
Code Example (Bad Practice):
csharpCopyEditpublic class Department
{
public Employee Employee { get; set; }
}
public class Employee
{
public Department Department { get; set; }
}
What's Wrong Here:
Circular dependencies make the system harder to manage and test.
This design increases the risk of stack overflow or memory leaks.
Better Approach: Avoid composition and use association:
csharpCopyEditpublic class Department
{
public List<Employee> Employees { get; set; }
}
public class Employee
{
public Department Department { get; set; }
}
Key Takeaways: Good vs. Bad Practices
Good Practice | Bad Practice |
Encapsulate contained objects within the parent. | Exposing internal objects publicly. |
Use composition only for tightly coupled objects. | Overusing composition where association is enough. |
Ensure the parent fully owns the lifecycle of the child. | Introducing circular dependencies. |
Provide clear methods in the parent to manage children. | Allowing external classes to modify internal objects directly. |
How to Decide When to Use Composition?
Use Composition When:
Objects have a strong "has-a" relationship.
Child objects cannot exist independently of the parent.
The parent fully owns the lifecycle of the child.
Avoid Composition When:
The relationship is loosely coupled or optional (use association or aggregation instead).
Objects have independent lifecycles.