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 the Department instances. These departments are not shared with other objects or hospitals.
  • Lifetime Dependency:

    • When the CloseHospital() method is called, all departments are cleared from the Departments list, symbolizing their destruction when the hospital is closed.
  • Encapsulation:

    • The Departments list is private to the Hospital class, ensuring that only the Hospital can manage its departments.

Other Practical Scenarios for Composition

  1. Car and Engine: A Car is composed of an Engine. If the Car is destroyed, the Engine is destroyed too.

  2. Library and Books: A Library is composed of multiple Books. If the Library is closed, all its Books are no longer accessible.

  3. House and Rooms: A House is composed of Rooms. If the House is destroyed, the Rooms 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 in Hospital) 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 the Car class.

  • The lifecycle of the Engine depends on the Car object.

  • The Car controls how the Engine 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 the Library class.

  • The Library provides clear methods to manage its Books.

  • The Books are destroyed if the Library 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 with Student, Professor, and Course, even though these objects might not have dependent lifecycles.

  • If a Student object is updated, the University 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 PracticeBad 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?

  1. 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.

  2. Avoid Composition When:

    • The relationship is loosely coupled or optional (use association or aggregation instead).

    • Objects have independent lifecycles.