Post

Inheritance vs Composition vs Aggregation

Inheritance vs Composition vs Aggregation

Inheritance

Inheritance is a mechanism where a new class is based on an existing class. The new class inherits the data and behaviors of the existing class, allowing it to reuse the code and extend its functionality. The new class is called the derived or child class, while the existing class is called the base or parent class.

Inheritance promotes code reuse and allows you to create a hierarchy of related classes. However, it can also lead to tight coupling between classes, making the system more rigid and harder to maintain.

Composition

Composition is a design pattern where you build complex objects from simpler ones. In composition, an object contains an instance of another object as a member variable, and the containing object delegates certain responsibilities to the contained object. In composition the parent object is responsible for the lifecycle of the objects that contains.

Composition promotes flexibility and loose coupling, as the containing object can use different implementations of the contained object without affecting its own behavior. It also allows for better code organization and testability.

Aggregation

Aggregation is a special case of composition, where the part has a has-a relationship with the whole. In aggregation, the part can exist independently of the whole, and the whole can exist without the part.

For example, a university has-a department, and a department has-a student. The department can exist without the university, and the student can exist without the department.

Aggregation is useful when you want to model a part-whole relationship where the part can have a separate lifecycle from the whole.

Key Differences:

  1. Inheritance: A is-a relationship, where the child class inherits from the parent class.
  2. Composition: A owns-a relationship, where the containing object contains the contained object as a member variable.
  3. Aggregation: A special case of composition ( has-a relationship ) , where the part can exist independently of the whole.

The choice between these three approaches depends on the specific requirements of your system and the relationship between the classes involved. Generally, composition and aggregation are preferred over inheritance due to their flexibility and loose coupling.

Let’s compare them with some code examples

Inheritance Example in Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Base class
class Animal {
    private String name;
    private int age;

    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void makeSound() {
        System.out.println("The animal makes a sound.");
    }
}

// Derived classes
class Dog extends Animal {
    private String breed;

    public Dog(String name, int age, String breed) {
        super(name, age);
        this.breed = breed;
    }

    @Override
    public void makeSound() {
        System.out.println("The dog barks.");
    }
}

class Cat extends Animal {
    private String color;

    public Cat(String name, int age, String color) {
        super(name, age);
        this.color = color;
    }

    @Override
    public void makeSound() {
        System.out.println("The cat meows.");
    }
}

Since we are all familiar with inheritance I provided a very simple Java example where the Dog and Cat classes inherit from the Animal class. The derived classes can access the name and age properties and the makeSound() method from the base class, and they also add their own specific attributes (breed for Dog and color for Cat) and override the makeSound() method.

Composition Example in Java

In a true composition relationship, the lifecycle of the child objects is dependent on the parent object. This means that when the parent object is created, it is responsible for creating and initializing the child objects it needs. And when the parent object is destroyed, the child objects are also destroyed.

Let’s look at an example that demonstrates this in Java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// Composed objects
class Engine {
    private int horsepower;

    public Engine(int horsepower) {
        this.horsepower = horsepower;
    }

    public void start() {
        System.out.println("The engine starts.");
    }

    public void shutdown() {
        System.out.println("The engine shuts down.");
    }
}

class Transmission {
    private int gears;

    public Transmission(int gears) {
        this.gears = gears;
    }

    public void shift(int gear) {
        System.out.println("The transmission shifts to gear " + gear + ".");
    }

    public void disconnect() {
        System.out.println("The transmission is disconnected.");
    }
}

// Composite object
class Car {
    private String make;
    private String model;
    private Engine engine;
    private Transmission transmission;

    public Car(String make, String model, int engineHP, int transmissionGears) {
        this.make = make;
        this.model = model;
        this.engine = new Engine(engineHP);
        this.transmission = new Transmission(transmissionGears);
    }

    public void start() {
        engine.start();
    }

    public void shiftGear(int gear) {
        transmission.shift(gear);
    }

    public void shutdown() {
        engine.shutdown();
        transmission.disconnect();
        System.out.println("The car is shut down.");
    }
}

In this example, the Car class is responsible for creating and initializing the Engine and Transmission objects that it needs. The Car class also has a shutdown() method that ensures the proper cleanup of the composed objects when the Car object is destroyed.

This demonstrates that in a composition relationship, the child objects’ lifecycle is tightly coupled to the parent object. The parent object controls the creation, initialization, and destruction of the child objects.

Inheritance vs Composition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// INITIAL REQUIREMENTS:
// - Support basic audio and video playback
// - Include file loading and decryption

// With inheritance - Initial implementation
class MediaLibrary {
    protected void loadFile(String filename) {
        System.out.println("Loading file: " + filename);
    }
    
    protected void decrypt(String data) {
        System.out.println("Decrypting data");
    }
}

class AudioPlayer extends MediaLibrary {
    private int volume = 50;
    
    public void playAudio(String filename) {
        loadFile(filename);
        decrypt(filename);
        System.out.println("Playing audio at volume: " + volume);
    }
}

class VideoPlayer extends MediaLibrary {
    private int volume = 50;
    private int brightness = 70;
    
    public void playVideo(String filename) {
        loadFile(filename);
        decrypt(filename);
        System.out.println("Playing video");
    }
}

// NEW REQUIREMENT 1: Some media files don't need decryption anymore
// Problem: We're stuck with decrypt() in MediaLibrary, even when we don't need it
// Solution: Create new classes, breaking inheritance chain

class NonEncryptedAudioPlayer extends MediaLibrary {
    private int volume = 50;
    
    public void playAudio(String filename) {
        loadFile(filename);
        // Can't skip decrypt() without changing base class
        decrypt(filename); // Unnecessary operation!
        System.out.println("Playing audio");
    }
}

// NEW REQUIREMENT 2: Add streaming support
// Problem: MediaLibrary assumes file-based loading, need major restructuring
// Need to create parallel inheritance hierarchy!

class StreamingMediaLibrary {
    protected void initStream(String url) {
        System.out.println("Initializing stream: " + url);
    }
}

class StreamingAudioPlayer extends StreamingMediaLibrary {
    private int volume = 50;
    
    public void playAudio(String url) {
        initStream(url);
        System.out.println("Streaming audio");
    }
}
1
2
3
4
5
MediaLibrary

├── AudioPlayer (has volume, needs decrypt)
├── VideoPlayer (has volume and brightness, needs decrypt)
└── NonEncryptedAudioPlayer (has volume, doesn't need decrypt but forced to have it)

New Streaming Hierarchy:

1
2
3
StreamingMediaLibrary

└── StreamingAudioPlayer (has volume, completely different loading mechanism)

Problems of this design:

  • Duplicate volume control in both hierarchies
  • Can’t share code between file-based and streaming players
  • Must maintain two separate class hierarchies
  • Adding new features requires updating both hierarchies

Better Solution with Composition:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Components that can be mixed and matched
interface MediaLoader {
    void load(String source);
}

class FileLoader implements MediaLoader { ... }
class StreamLoader implements MediaLoader { ... }
class Decryptor { ... }
class VolumeControl { ... }

// Single player class that can handle both cases
class ModernAudioPlayer {
    private final MediaLoader loader;      // Can be FileLoader or StreamLoader
    private final Decryptor decryptor;     // Optional
    private final VolumeControl volume;    // Shared functionality
}

Real-World Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
// With parallel hierarchies (problematic):
AudioPlayer filePlayer = new AudioPlayer();
filePlayer.playAudio("song.mp3");

StreamingAudioPlayer streamPlayer = new StreamingAudioPlayer();
streamPlayer.playAudio("http://stream.music.com/song");

// With composition (better):
ModernAudioPlayer player1 = new ModernAudioPlayer(new FileLoader(), new Decryptor());
ModernAudioPlayer player2 = new ModernAudioPlayer(new StreamLoader()); // No decryptor needed

// Both players use the same class but with different components!

This composition approach:

  • Eliminates Duplication: Volume control is a single reusable component
  • Flexible Loading: Can switch between file and streaming without inheritance
  • Optional Features: Decryption can be included or excluded easily
  • Easy to Extend: New features (like caching) just become new components

Think of it like building with LEGO blocks:

  • Inheritance approach: You have two separate, incompatible sets of blocks
  • Composition approach: All blocks work together, and you can pick the ones you need

This example perfectly demonstrates why parallel hierarchies are a code smell and why composition often provides a more flexible solution. Instead of maintaining multiple inheritance trees, you create reusable components that can be combined as needed. In object-oriented design, choosing between inheritance and composition is a crucial decision. Here’s why composition often proves to be the more flexible choice.

On the other hand there are cases where inheritance proves to be a better choice :

  • The relationships are natural and stable
  • The base behavior is unlikely to change
  • Code reuse is significant and meaningful
  • Polymorphism provides real benefits

Remember, the key is not to avoid inheritance completely, but to use it when it truly models the problem domain and relationships correctly.

Aggregation Example in Java

In an aggregation relationship, the “part” object can exist independently of the “whole” object. This means that the lifecycle of the “part” object is not dependent on the lifecycle of the “whole” object.

Let’s revisit the University-Department-Student example from before, and demonstrate this independence:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// Professor class represents a faculty member who can teach multiple courses
public class Professor {
  private String name;
  private String department;
  private String employeeId;

  public Professor(String name, String department, String employeeId) {
    this.name = name;
    this.department = department;
    this.employeeId = employeeId;
  }

  public String getName() {
    return name;
  }

  // Other getters and setters
}

// Course class demonstrates aggregation with Professor
public class Course {
  private String courseCode;
  private String courseName;
  private Professor instructor;  // Aggregation: Course has-a Professor
  private int maxStudents;

  public Course(String courseCode, String courseName, int maxStudents) {
    this.courseCode = courseCode;
    this.courseName = courseName;
    this.maxStudents = maxStudents;
  }

  // Professor can be assigned or changed later
  public void assignInstructor(Professor professor) {
    this.instructor = professor;
  }

  // Professor can exist without the course
  public void removeInstructor() {
    this.instructor = null;
  }

  public String getCourseInfo() {
    return String.format("Course: %s - %s, Instructor: %s",
      courseCode,
      courseName,
      instructor != null ? instructor.getName() : "TBA");
  }
}

// Department class to demonstrate usage
public class Department {
  private String name;
  private List<Course> courses;
  private List<Professor> faculty;

  public Department(String name) {
    this.name = name;
    this.courses = new ArrayList<>();
    this.faculty = new ArrayList<>();
  }

  public void addProfessor(Professor professor) {
    faculty.add(professor);
  }

  public void addCourse(Course course) {
    courses.add(course);
  }

  // Method to demonstrate the flexibility of aggregation
  public void reassignCourse(Course course, Professor newInstructor) {
    if (courses.contains(course) && faculty.contains(newInstructor)) {
      course.assignInstructor(newInstructor);
      System.out.println("Course reassigned successfully.");
    }
  }
}

// Main class to demonstrate why aggregation is better here
public class UniversitySystem {
  public static void main(String[] args) {
    // Create a department
    Department computerScience = new Department("Computer Science");

    // Create professors (they can exist independently)
    Professor prof1 = new Professor("Dr. Smith", "Computer Science", "CS001");
    Professor prof2 = new Professor("Dr. Johnson", "Computer Science", "CS002");

    // Add professors to department
    computerScience.addProfessor(prof1);
    computerScience.addProfessor(prof2);

    // Create courses
    Course dataStructures = new Course("CS201", "Data Structures", 30);
    Course algorithms = new Course("CS301", "Algorithms", 25);

    // Initially assign professors
    dataStructures.assignInstructor(prof1);
    algorithms.assignInstructor(prof2);

    // Add courses to department
    computerScience.addCourse(dataStructures);
    computerScience.addCourse(algorithms);

    // Demonstrate flexibility of aggregation
    System.out.println("Initial assignment:");
    System.out.println(dataStructures.getCourseInfo());
    System.out.println(algorithms.getCourseInfo());

    // Prof1 goes on sabbatical - reassign their course to prof2
    System.out.println("\nAfter reassignment (Prof1 goes on sabbatical):");
    dataStructures.assignInstructor(prof2);
    System.out.println(dataStructures.getCourseInfo());

    // Prof2 now teaches both courses
    // Note: Prof1 still exists and remains in the faculty!
  }
}

Comparing Aggregation vs Composition in Java: University System Example

Aggregation Benefits

This example demonstrates why aggregation is better than composition in this scenario:

1. Independent Lifecycles

  • Professors exist independently of courses (they can teach 0, 1, or many courses)
  • If a course is deleted, the professor shouldn’t be deleted
  • If a professor goes on sabbatical, their courses can be reassigned without affecting the professor’s existence

2. Flexibility in Relationships

  • Professors can be reassigned to different courses
  • Courses can change instructors without creating new professor objects
  • One professor can teach multiple courses
  • Courses can exist temporarily without an assigned professor

3. Resource Efficiency

  • Multiple courses can reference the same professor object
  • No need to duplicate professor information across courses

Composition Limitations

If this were implemented using composition instead:

  • Each course would need to “own” its professor
  • Reassigning professors would require creating new professor objects
  • A professor teaching multiple courses would exist as multiple copies
  • Deleting a course would delete its professor object

Implementation Issues with Composition

1. Data Duplication

Notice how we need separate professor instances for the same professor teaching multiple courses. This leads to data inconsistency and maintenance problems.

2. Rigid Structure

The composition version forces us to:

  • Have a professor at course creation time
  • Create entirely new course objects just to change professors
  • Maintain duplicate professor data

3. Resource Issues

  • More memory usage from duplicate professor objects
  • No single source of truth for professor information
  • Garbage collection of professor data when courses are deleted

4. Identity Problems

  • Two instances of the same professor aren’t actually the same object
  • Can’t easily track which courses a professor is teaching
  • Equality checks fail even for the same professor

Conclusion

This comparison clearly shows why aggregation is superior for this scenario - it maintains proper relationships while avoiding data duplication and allowing for flexible assignment changes.

This post is licensed under CC BY 4.0 by the author.

Trending Tags