← Back to Home

Module 3: Design with Composition

Module Overview

Understand composition as an alternative to inheritance and learn how to design flexible and maintainable object relationships.

Learning Objectives

Code Examples

Here are some examples demonstrating key composition patterns:

Exposing a Subset of Functionality (Delegation)

import java.util.ArrayList;
import java.util.Collection;

// This class implements a read-only list that only exposes
// a subset of ArrayList functionality
public class ReadOnlyList<E> {
    // Composition: has-a relationship with ArrayList
    private final ArrayList<E> list;
    
    public ReadOnlyList(Collection<E> items) {
        this.list = new ArrayList<>(items);
    }
    
    // We only expose get and size methods
    public E get(int index) {
        return list.get(index);
    }
    
    public int size() {
        return list.size();
    }
    
    public boolean contains(E element) {
        return list.contains(element);
    }
    
    // Notice we don't expose add, remove, or other mutating methods
    
    public static void main(String[] args) {
        ArrayList<String> source = new ArrayList<>();
        source.add("Apple");
        source.add("Banana");
        source.add("Cherry");
        
        ReadOnlyList<String> readOnly = new ReadOnlyList<>(source);
        System.out.println("Element at index 1: " + readOnly.get(1));
        System.out.println("Size: " + readOnly.size());
        System.out.println("Contains Banana? " + readOnly.contains("Banana"));
        
        // This would cause a compilation error:
        // readOnly.add("Date");
    }
}

Exposing a Superset of Functionality

import java.util.ArrayList;
import java.util.List;

// This class extends ArrayList functionality with additional features
public class EnhancedList<E> {
    // Composition: has-a relationship with ArrayList
    private final List<E> list;
    
    public EnhancedList() {
        this.list = new ArrayList<>();
    }
    
    // Original ArrayList functionality
    public boolean add(E element) {
        return list.add(element);
    }
    
    public E get(int index) {
        return list.get(index);
    }
    
    public int size() {
        return list.size();
    }
    
    // Enhanced functionality: get first element
    public E getFirst() {
        if (list.isEmpty()) {
            throw new IllegalStateException("List is empty");
        }
        return list.get(0);
    }
    
    // Enhanced functionality: get last element
    public E getLast() {
        if (list.isEmpty()) {
            throw new IllegalStateException("List is empty");
        }
        return list.get(list.size() - 1);
    }
    
    // Enhanced functionality: add multiple elements at once
    public boolean addAll(E... elements) {
        boolean changed = false;
        for (E element : elements) {
            changed |= list.add(element);
        }
        return changed;
    }
    
    public static void main(String[] args) {
        EnhancedList<String> enhanced = new EnhancedList<>();
        
        // Using original ArrayList functionality
        enhanced.add("Apple");
        enhanced.add("Banana");
        
        // Using enhanced functionality
        enhanced.addAll("Cherry", "Date", "Elderberry");
        
        System.out.println("First element: " + enhanced.getFirst());
        System.out.println("Last element: " + enhanced.getLast());
        System.out.println("Total size: " + enhanced.size());
    }
}

Creating New Functionality with Composition

import java.util.HashMap;
import java.util.Map;

// An example of using composition to create a cache system
public class Cache<K, V> {
    // Composition: uses a HashMap internally
    private final Map<K, CacheEntry<V>> cacheMap;
    private final long expiryTimeMs;
    
    public Cache(long expiryTimeMs) {
        this.cacheMap = new HashMap<>();
        this.expiryTimeMs = expiryTimeMs;
    }
    
    public void put(K key, V value) {
        cacheMap.put(key, new CacheEntry<>(value, System.currentTimeMillis()));
    }
    
    public V get(K key) {
        CacheEntry<V> entry = cacheMap.get(key);
        
        if (entry == null) {
            return null;
        }
        
        // Check if entry has expired
        if (System.currentTimeMillis() - entry.timestamp > expiryTimeMs) {
            cacheMap.remove(key);
            return null;
        }
        
        return entry.value;
    }
    
    public int size() {
        // Clean expired entries before returning size
        cacheMap.entrySet().removeIf(entry -> 
            System.currentTimeMillis() - entry.getValue().timestamp > expiryTimeMs);
        return cacheMap.size();
    }
    
    // Helper class for storing cache entries with timestamps
    private static class CacheEntry<V> {
        private final V value;
        private final long timestamp;
        
        public CacheEntry(V value, long timestamp) {
            this.value = value;
            this.timestamp = timestamp;
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        Cache<String, String> cache = new Cache<>(2000); // 2 second expiry
        
        cache.put("key1", "value1");
        System.out.println("After adding key1: " + cache.get("key1"));
        
        // Sleep for 1 second (not expired yet)
        Thread.sleep(1000);
        System.out.println("After 1 second: " + cache.get("key1"));
        
        // Sleep for 1.5 more seconds (should be expired now)
        Thread.sleep(1500);
        System.out.println("After 2.5 seconds: " + cache.get("key1"));
    }
}

Resources