Index

Composition and Aggregation

Java Object-Oriented Design

7.1 "Has-a" Relationships

One of the foundational principles in Object-Oriented Design (OOD) is understanding the relationships between objects. Two primary types of relationships often discussed are “is-a” and “has-a”. This section focuses on the “has-a” relationship, which plays a crucial role in designing flexible, maintainable systems by favoring composition over inheritance.

What Is a Has-a Relationship?

A “has-a” relationship describes a situation where one object contains or owns another object as part of its state or behavior. It models real-world entities by capturing the fact that some objects are made up of or possess other objects. This kind of relationship is also known as composition or aggregation, depending on the strength of the relationship.

For example:

These relationships reflect part-whole hierarchies where the whole object “has” one or more parts.

Contrasting Has-a and Is-a Relationships

Understanding the difference between “has-a” (composition) and “is-a” (inheritance) relationships is essential for sound design:

In summary:

Why Favor Has-a Relationships?

While inheritance is powerful, relying too heavily on it can lead to rigid, tightly coupled designs. In contrast, composition via “has-a” relationships offers greater flexibility, modularity, and reuse. Here’s why:

Examples of Has-a Relationships

Here are some simple Java examples illustrating “has-a” relationships:

class Engine {
    void start() {
        System.out.println("Engine started.");
    }
}

class Car {
    private Engine engine; // Car "has-a" Engine

    public Car() {
        this.engine = new Engine();
    }

    public void startCar() {
        engine.start(); // Delegates behavior to the engine
        System.out.println("Car is ready to go!");
    }
}

In this example, the Car class has an Engine object. The car delegates engine-related tasks to the engine object rather than inheriting from it.

Another example:

class Room {
    private String name;

    public Room(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

class House {
    private Room[] rooms; // House "has-a" collection of Rooms

    public House(Room[] rooms) {
        this.rooms = rooms;
    }

    public void listRooms() {
        for (Room room : rooms) {
            System.out.println("Room: " + room.getName());
        }
    }
}

The House object contains an array of Room objects, showing a “has-a” relationship with multiple components.

Click to view full runnable Code

class Engine {
    void start() {
        System.out.println("Engine started.");
    }
}

class Car {
    private Engine engine; // Car "has-a" Engine

    public Car() {
        this.engine = new Engine();
    }

    public void startCar() {
        engine.start(); // Delegates behavior to the engine
        System.out.println("Car is ready to go!");
    }
}

class Room {
    private String name;

    public Room(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

class House {
    private Room[] rooms; // House "has-a" collection of Rooms

    public House(Room[] rooms) {
        this.rooms = rooms;
    }

    public void listRooms() {
        for (Room room : rooms) {
            System.out.println("Room: " + room.getName());
        }
    }
}

public class HasARelationshipDemo {
    public static void main(String[] args) {
        Car car = new Car();
        car.startCar();

        Room[] rooms = { new Room("Living Room"), new Room("Bedroom"), new Room("Kitchen") };
        House house = new House(rooms);
        house.listRooms();
    }
}

Reflecting on Has-a Relationships

When designing software, try to think in terms of how objects are composed of other objects rather than forcing everything into an inheritance hierarchy. Ask yourself:

By focusing on “has-a” relationships, your designs will tend to be more adaptable to change, easier to test, and clearer to understand.

Summary

Mastering these concepts will help you design better, more maintainable object-oriented systems as you progress through this book.

Index

7.2 Composition vs Inheritance

In object-oriented design, two fundamental ways to model relationships between classes are inheritance and composition. Both techniques allow one class to reuse or extend the behavior of another, but they do so in very different ways, each with distinct advantages and trade-offs. Understanding when to use each is crucial to designing flexible, maintainable, and reusable Java software.

What Is Inheritance?

Inheritance is a mechanism where one class (called the subclass or child class) inherits fields and methods from another class (the superclass or parent class). It represents an “is-a” relationship: the subclass is a specialized type of the superclass.

For example, consider a class hierarchy where Car extends Vehicle:

class Vehicle {
    void start() {
        System.out.println("Vehicle starting");
    }
}

class Car extends Vehicle {
    void openTrunk() {
        System.out.println("Opening trunk");
    }
}

Here, Car inherits the start() method from Vehicle and adds its own behavior.

What Is Composition?

Composition models a “has-a” relationship where one class contains an instance of another as a component. Instead of inheriting behavior, the containing class delegates tasks to the component objects it holds.

Using the same example:

class Engine {
    void start() {
        System.out.println("Engine starting");
    }
}

class Car {
    private Engine engine;

    public Car() {
        engine = new Engine();
    }

    void start() {
        engine.start();
        System.out.println("Car is ready to go");
    }
}

Here, Car has an Engine, and it uses that engine to perform the start operation.

Click to view full runnable Code

public class Main {
    public static void main(String[] args) {
        System.out.println("Inheritance example:");
        CarInheritance car1 = new CarInheritance();
        car1.start();         // Inherited from Vehicle
        car1.openTrunk();     // Defined in CarInheritance

        System.out.println("\nComposition example:");
        CarComposition car2 = new CarComposition();
        car2.start();         // Delegates to Engine
    }
}

// Inheritance example
class Vehicle {
    void start() {
        System.out.println("Vehicle starting");
    }
}

class CarInheritance extends Vehicle {
    void openTrunk() {
        System.out.println("Opening trunk");
    }
}

// Composition example
class Engine {
    void start() {
        System.out.println("Engine starting");
    }
}

class CarComposition {
    private Engine engine;

    public CarComposition() {
        engine = new Engine();
    }

    void start() {
        engine.start();
        System.out.println("Car is ready to go");
    }
}

Comparing Composition and Inheritance

Aspect Inheritance Composition
Relationship “Is-a” — subclass is a specialized type of superclass “Has-a” — class contains component objects
Coupling Tight coupling to superclass implementation Loose coupling; components are replaceable
Flexibility Less flexible; changes in superclass affect subclasses More flexible; components can be changed or replaced independently
Reuse Reuse by inheriting behavior and overriding methods Reuse by delegating behavior to composed objects
Maintenance Can lead to fragile base classes if superclass changes Easier to maintain as components are independent
Multiple inheritance Java does not support multiple class inheritance Allows mixing different components freely

Pros and Cons of Inheritance

Pros:

Cons:

Pros and Cons of Composition

Pros:

Cons:

Modeling the Same Scenario: Inheritance vs Composition

Suppose you want to model different types of payment processing.

Inheritance example:

class PaymentProcessor {
    void processPayment(double amount) {
        System.out.println("Processing payment: " + amount);
    }
}

class CreditCardProcessor extends PaymentProcessor {
    @Override
    void processPayment(double amount) {
        System.out.println("Processing credit card payment: " + amount);
    }
}

Composition example:

interface PaymentMethod {
    void pay(double amount);
}

class CreditCard implements PaymentMethod {
    public void pay(double amount) {
        System.out.println("Paying with credit card: " + amount);
    }
}

class PaymentProcessor {
    private PaymentMethod paymentMethod;

    public PaymentProcessor(PaymentMethod paymentMethod) {
        this.paymentMethod = paymentMethod;
    }

    void processPayment(double amount) {
        paymentMethod.pay(amount);
    }
}

In the composition example, PaymentProcessor can work with any PaymentMethod implementation, making it flexible and extensible without changing the processor class.

Favor Composition Over Inheritance: The Guiding Principle

Modern object-oriented design strongly advocates “favor composition over inheritance”. The rationale is:

This does not mean inheritance is bad—it's still valuable for clear “is-a” relationships and polymorphism. However, composition should be the default choice when designing relationships between classes.

Summary

Understanding these concepts deeply equips you to make better design choices as you build complex, scalable object-oriented systems.

Index

7.3 Designing for Reuse

In software development, code reuse is a key goal—it saves time, reduces errors, and promotes consistency across projects. Designing classes and components for reuse requires thoughtful planning, especially when using composition, which is the preferred way to build flexible and maintainable systems.

Why Composition Enhances Reuse

Unlike inheritance, which tightly couples subclasses to their parent classes, composition enables reuse by assembling independent, well-defined components. This modular approach makes it easier to reuse and extend parts of a system without affecting unrelated areas.

For example, a logging component designed as a standalone class can be reused across many different parts of an application or even across different projects simply by including and configuring it, rather than forcing all clients to inherit from a base class that provides logging.

Strategies for Designing Reusable Components

Single Responsibility Principle (SRP)

Design classes to have one clear responsibility. When a class does one thing well, it becomes easier to understand, test, and reuse in different contexts.

class Logger {
    void log(String message) {
        System.out.println("LOG: " + message);
    }
}

This Logger class is simple and focused—perfect for reuse anywhere logging is needed.

Programming to Interfaces

Define components using interfaces rather than concrete classes. Interfaces specify what a component does without prescribing how it does it. This abstraction enables multiple implementations that can be swapped easily.

interface PaymentMethod {
    void pay(double amount);
}

class CreditCardPayment implements PaymentMethod {
    public void pay(double amount) {
        System.out.println("Paying $" + amount + " by credit card");
    }
}

By depending on PaymentMethod rather than CreditCardPayment directly, your system becomes more flexible and reusable.

Favor Composition and Delegation

Build complex behavior by composing small, reusable components. Use delegation to forward requests to component objects, avoiding the pitfalls of inheritance.

Example: A Car class composed of an Engine and Transmission:

class Engine {
    void start() { System.out.println("Engine started"); }
}

class Transmission {
    void shiftGear(int gear) { System.out.println("Shifted to gear " + gear); }
}

class Car {
    private Engine engine = new Engine();
    private Transmission transmission = new Transmission();

    void drive() {
        engine.start();
        transmission.shiftGear(1);
        System.out.println("Car is driving");
    }
}

Each component is reusable on its own, and Car simply composes these parts.

Use Immutability Where Possible

Immutable objects (objects whose state cannot change after construction) are naturally reusable because they are thread-safe and side-effect free.

Examples of Reusable Components

By designing these components with clear interfaces and minimal dependencies, you enable reuse across many parts of your application or even across projects.

Design Patterns That Emphasize Composition for Reuse

Several well-known design patterns explicitly promote composition to maximize reuse:

interface Coffee {
    double cost();
}

class SimpleCoffee implements Coffee {
    public double cost() { return 2.0; }
}

class MilkDecorator implements Coffee {
    private Coffee coffee;
    public MilkDecorator(Coffee coffee) { this.coffee = coffee; }
    public double cost() { return coffee.cost() + 0.5; }
}
interface SortingStrategy {
    void sort(int[] array);
}

class BubbleSort implements SortingStrategy {
    public void sort(int[] array) { /* bubble sort implementation */ }
}

class QuickSort implements SortingStrategy {
    public void sort(int[] array) { /* quick sort implementation */ }
}

class Sorter {
    private SortingStrategy strategy;
    public Sorter(SortingStrategy strategy) { this.strategy = strategy; }
    public void sortArray(int[] array) { strategy.sort(array); }
}
Click to view full runnable Code

interface SortingStrategy {
    void sort(int[] array);
}

class BubbleSort implements SortingStrategy {
    public void sort(int[] array) {
        // Simple bubble sort implementation
        int n = array.length;
        for(int i = 0; i < n-1; i++) {
            for(int j = 0; j < n-i-1; j++) {
                if(array[j] > array[j+1]) {
                    int temp = array[j];
                    array[j] = array[j+1];
                    array[j+1] = temp;
                }
            }
        }
    }
}

class QuickSort implements SortingStrategy {
    public void sort(int[] array) {
        quickSort(array, 0, array.length - 1);
    }

    private void quickSort(int[] arr, int low, int high) {
        if(low < high) {
            int pi = partition(arr, low, high);
            quickSort(arr, low, pi -1);
            quickSort(arr, pi + 1, high);
        }
    }

    private int partition(int[] arr, int low, int high) {
        int pivot = arr[high];
        int i = (low - 1);
        for(int j = low; j < high; j++) {
            if(arr[j] < pivot) {
                i++;
                int temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
        }
        int temp = arr[i+1];
        arr[i+1] = arr[high];
        arr[high] = temp;
        return i + 1;
    }
}

class Sorter {
    private SortingStrategy strategy;

    public Sorter(SortingStrategy strategy) {
        this.strategy = strategy;
    }

    public void sortArray(int[] array) {
        strategy.sort(array);
    }
}

public class MainSort {
    public static void main(String[] args) {
        int[] data = {5, 3, 8, 1, 2};

        Sorter bubbleSorter = new Sorter(new BubbleSort());
        bubbleSorter.sortArray(data);
        System.out.print("BubbleSorted array: ");
        for(int num : data) System.out.print(num + " ");
        System.out.println();

        data = new int[]{5, 3, 8, 1, 2}; // reset array

        Sorter quickSorter = new Sorter(new QuickSort());
        quickSorter.sortArray(data);
        System.out.print("QuickSorted array: ");
        for(int num : data) System.out.print(num + " ");
        System.out.println();
    }
}

Summary

Designing for reuse with composition involves:

By following these strategies, you build software that is easier to maintain, extend, and adapt—key qualities for modern, scalable Java applications.

Index

7.4 Example: Modeling a Car with Components

In this section, we will explore a practical, real-world example of composition by modeling a Car composed of several smaller components such as an Engine, Wheel, and Transmission. This example will demonstrate how objects can be assembled from other objects to build flexible and maintainable designs.

Why Use Composition to Model a Car?

A car is not just one big object; it is made up of many parts working together. Instead of trying to cram all functionality into one monolithic class, composition allows us to break down the problem into manageable, reusable pieces.

By modeling a car as a has-a relationship—i.e., a car has an engine, has wheels, has a transmission—we reflect the real-world relationships clearly and create components that can be developed and tested independently.

Defining Component Classes

First, let’s define each component class with simple functionality.

// Engine.java
public class Engine {
    private boolean running;

    public void start() {
        running = true;
        System.out.println("Engine started.");
    }

    public void stop() {
        running = false;
        System.out.println("Engine stopped.");
    }

    public boolean isRunning() {
        return running;
    }
}

Wheel.java

public class Wheel {
    private String position;

    public Wheel(String position) {
        this.position = position;
    }

    public void rotate() {
        System.out.println(position + " wheel is rotating.");
    }
}

Transmission.java

public class Transmission {
    private int currentGear = 0;

    public void shiftGear(int gear) {
        currentGear = gear;
        System.out.println("Shifted to gear " + currentGear);
    }

    public int getCurrentGear() {
        return currentGear;
    }
}

Defining the Car Class with Composition

The Car class has an Engine, multiple Wheel objects, and a Transmission. We compose these inside Car as fields and use their behavior to implement the car's functionality.

Car.java

public class Car {
    private Engine engine;
    private Wheel[] wheels;
    private Transmission transmission;

    public Car() {
        // Initialize components
        engine = new Engine();
        wheels = new Wheel[] {
            new Wheel("Front Left"),
            new Wheel("Front Right"),
            new Wheel("Rear Left"),
            new Wheel("Rear Right")
        };
        transmission = new Transmission();
    }

    public void startCar() {
        engine.start();
        transmission.shiftGear(1);
        for (Wheel wheel : wheels) {
            wheel.rotate();
        }
        System.out.println("Car is started and ready to drive.");
    }

    public void stopCar() {
        transmission.shiftGear(0);
        engine.stop();
        System.out.println("Car has stopped.");
    }

    // Additional behaviors can be added here...
}

Running the Example

Let’s create a Main class to run this car simulation.

// Main.java
public class Main {
    public static void main(String[] args) {
        Car myCar = new Car();

        myCar.startCar();
        // Simulate driving...

        myCar.stopCar();
    }
}

Expected Console Output:

Engine started.
Shifted to gear 1
Front Left wheel is rotating.
Front Right wheel is rotating.
Rear Left wheel is rotating.
Rear Right wheel is rotating.
Car is started and ready to drive.
Shifted to gear 0
Engine stopped.
Car has stopped.
Click to view full runnable Code

// Engine.java
class Engine {
    private boolean running;

    public void start() {
        running = true;
        System.out.println("Engine started.");
    }

    public void stop() {
        running = false;
        System.out.println("Engine stopped.");
    }

    public boolean isRunning() {
        return running;
    }
}

// Wheel.java
class Wheel {
    private String position;

    public Wheel(String position) {
        this.position = position;
    }

    public void rotate() {
        System.out.println(position + " wheel is rotating.");
    }
}

// Transmission.java
class Transmission {
    private int currentGear = 0;

    public void shiftGear(int gear) {
        currentGear = gear;
        System.out.println("Shifted to gear " + currentGear);
    }

    public int getCurrentGear() {
        return currentGear;
    }
}

// Car.java
class Car {
    private Engine engine;
    private Wheel[] wheels;
    private Transmission transmission;

    public Car() {
        engine = new Engine();
        wheels = new Wheel[] {
            new Wheel("Front Left"),
            new Wheel("Front Right"),
            new Wheel("Rear Left"),
            new Wheel("Rear Right")
        };
        transmission = new Transmission();
    }

    public void startCar() {
        engine.start();
        transmission.shiftGear(1);
        for (Wheel wheel : wheels) {
            wheel.rotate();
        }
        System.out.println("Car is started and ready to drive.");
    }

    public void stopCar() {
        transmission.shiftGear(0);
        engine.stop();
        System.out.println("Car has stopped.");
    }
}

// Main.java
public class Main {
    public static void main(String[] args) {
        Car myCar = new Car();

        myCar.startCar();
        // Simulate driving...

        myCar.stopCar();
    }
}

Design Reflection and Advantages of Composition

  1. Modularity: Each component (Engine, Wheel, Transmission) encapsulates its own behavior and state. This modularity makes it easier to maintain and enhance individual parts without affecting the whole car.

  2. Reusability: The same Engine or Transmission classes could be reused in other vehicle types like trucks or motorcycles with little or no modification.

  3. Flexibility: If we want to model different types of wheels or engines (e.g., electric vs combustion), we can subclass or swap components without changing the Car class’s overall structure.

  4. Clear Responsibilities: Each class has a clear responsibility—Engine starts and stops, Wheel rotates, Transmission shifts gears—promoting better separation of concerns.

  5. Testability: You can unit test each component independently before integrating them into the Car. This reduces bugs and improves reliability.

Summary

This example showcases how composition models real-world “has-a” relationships effectively in Java. Instead of one large class trying to do everything, breaking down a complex object like a Car into components leads to a cleaner, more manageable design.

Composition encourages building systems by assembling smaller parts with defined roles—making code easier to understand, extend, and maintain. This principle is central to modern object-oriented design and is preferred over inheritance when modeling “part-of” relationships.

Index