Topic Note Part 4: Composition¶
Course: Java Programming Masterclass - Tim Buchalka (Udemy)
Section: 08. Advanced OOP Techniques
Status: Complete
Learning Objectives¶
- Understand the difference between IS-A and HAS-A relationships
- Master Composition as an alternative to Inheritance
- Learn when to prefer Composition over Inheritance
- Build complex objects from simpler component parts
- Implement delegation patterns through composite classes
- Apply composition in real-world modeling scenarios
IS-A vs HAS-A Relationships¶
The Two Fundamental Relationships in OOP¶
In Object-Oriented Programming, there are two primary ways to relate classes:
| Relationship | Keyword | Description | Example |
|---|---|---|---|
| IS-A | extends |
Inheritance - subclass is a type of parent | Dog extends Animal → Dog IS-A Animal |
| HAS-A | field reference | Composition - class contains other classes | Car has-a Engine → Car HAS-A Engine |
flowchart LR
subgraph Inheritance
direction TB
A[Animal] --> B[Dog]
end
subgraph Composition
direction TB
C[Car] -.contains.-> D[Engine]
C -.contains.-> E[Wheel]
end
Understanding the Difference¶
Inheritance (IS-A):
- Creates a class hierarchy where subclasses inherit from parents
- Establishes a "type-of" relationship
- Subclass can be used anywhere parent is expected
- Example: A
MonitorIS-AProduct
Composition (HAS-A):
- Creates objects from other objects as building blocks
- Establishes a "contains" or "made-up-of" relationship
- The containing class delegates work to its components
- Example: A
PersonalComputerHAS-AMonitor, HAS-AMotherboard
What is Composition?¶
Definition¶
Composition is a design technique where a class is built by combining references to other objects. The composite class (the container) manages its component parts and delegates behavior to them.
Key Characteristics¶
- Contains references to other objects as instance fields
- Delegates work to component objects
- Manages lifecycle of its parts (creates, uses, destroys)
- Hides complexity from external code
- More flexible than inheritance for many scenarios
Visual Comparison¶
flowchart TB
subgraph inheritance[" INHERITANCE "]
direction TB
P[Product<br>Base Class]
P --> M[Monitor]
P --> MB[Motherboard]
P --> CC[ComputerCase]
note1[Each subclass IS-A Product]
end
flowchart TB
subgraph composition[" COMPOSITION "]
direction TB
PC[PersonalComputer<br>Contains parts]
PC -.HAS-A.-> M2[Monitor]
PC -.HAS-A.-> MB2[Motherboard]
PC -.HAS-A.-> CC2[Case]
note2[PC HAS-A Monitor, Motherboard, Case]
end
Building a Computer with Composition¶
The Product Hierarchy (Inheritance)¶
First, let's establish a base class for computer products:
public class Product {
private String model;
private String manufacturer;
private int width;
private int height;
private int depth;
public Product(String model, String manufacturer) {
this.model = model;
this.manufacturer = manufacturer;
}
}
The Component Parts (Subclasses)¶
Each computer part IS-A Product with its own specific behavior:
class Monitor extends Product {
private int size;
private String resolution;
public Monitor(String model, String manufacturer) {
super(model, manufacturer);
}
public Monitor(String model, String manufacturer,
int size, String resolution) {
super(model, manufacturer);
this.size = size;
this.resolution = resolution;
}
public void drawPixelAt(int x, int y, String color) {
System.out.printf("Drawing pixel at %d,%d in color %s%n",
x, y, color);
}
}
class Motherboard extends Product {
private int ramSlots;
private int cardSlots;
private String bios;
public Motherboard(String model, String manufacturer,
int ramSlots, int cardSlots, String bios) {
super(model, manufacturer);
this.ramSlots = ramSlots;
this.cardSlots = cardSlots;
this.bios = bios;
}
public void loadProgram(String programName) {
System.out.println("Program: " + programName + " is now loading...");
}
}
class ComputerCase extends Product {
private String powerSupply;
public ComputerCase(String model, String manufacturer,
String powerSupply) {
super(model, manufacturer);
this.powerSupply = powerSupply;
}
public void pressPowerButton() {
System.out.println("Power button pressed!");
}
}
The Composite Class (PersonalComputer)¶
Now, the PersonalComputer uses BOTH inheritance AND composition:
public class PersonalComputer extends Product {
// COMPOSITION: These are HAS-A relationships
private ComputerCase computerCase;
private Monitor monitor;
private Motherboard motherboard;
public PersonalComputer(String model, String manufacturer,
ComputerCase computerCase,
Monitor monitor,
Motherboard motherboard) {
super(model, manufacturer); // Inheritance
this.computerCase = computerCase;
this.monitor = monitor;
this.motherboard = motherboard;
}
// Private method - internal behavior
private void drawLogo() {
monitor.drawPixelAt(1200, 50, "yellow");
}
// Public method - delegates to parts
public void powerUp() {
computerCase.pressPowerButton();
drawLogo();
}
}
Combining Inheritance and Composition
PersonalComputer IS-A Product (inherits common attributes) AND HAS-A Monitor, Motherboard, and ComputerCase. This is a powerful combination!
Two Approaches to Using Composition¶
Approach 1: Exposing Components via Getters¶
The calling code directly accesses parts through getter methods:
// With getter methods on PersonalComputer:
public ComputerCase getComputerCase() { return computerCase; }
public Monitor getMonitor() { return monitor; }
public Motherboard getMotherboard() { return motherboard; }
// Calling code
PersonalComputer pc = new PersonalComputer("2208", "Dell",
theCase, theMonitor, theMotherboard);
// Direct access through getters - method chaining
pc.getMonitor().drawPixelAt(10, 10, "red");
pc.getMotherboard().loadProgram("Windows OS");
pc.getComputerCase().pressPowerButton();
Disadvantages:
- Exposes internal structure
- Calling code needs to know about parts
- Changes to parts affect all calling code
Approach 2: Hiding Components via Delegation (Preferred)¶
The composite class provides public methods that delegate to parts:
public class PersonalComputer extends Product {
private ComputerCase computerCase;
private Monitor monitor;
private Motherboard motherboard;
// NO GETTERS - parts are hidden
// Private method - only PC can draw logo
private void drawLogo() {
monitor.drawPixelAt(1200, 50, "yellow");
}
// Public method - high-level interface
public void powerUp() {
computerCase.pressPowerButton();
drawLogo();
}
}
// Calling code - much simpler!
PersonalComputer pc = new PersonalComputer("2208", "Dell",
theCase, theMonitor, theMotherboard);
pc.powerUp(); // One method, handles everything
Advantages:
- Parts are encapsulated
- Calling code doesn't need to know details
- Changes to parts don't affect calling code
- Simpler interface (better encapsulation)
Smart Kitchen Challenge Example¶
The Composite Container¶
public class SmartKitchen {
private CoffeeMaker brewMaster;
private Refrigerator iceBox;
private DishWasher dishWasher;
// No-args constructor creates all appliances
public SmartKitchen() {
brewMaster = new CoffeeMaker();
iceBox = new Refrigerator();
dishWasher = new DishWasher();
}
// Method 1: Set state of all appliances
public void setKitchenState(boolean coffeeFlag,
boolean fridgeFlag,
boolean dishWasherFlag) {
brewMaster.setHasWorkToDo(coffeeFlag);
iceBox.setHasWorkToDo(fridgeFlag);
dishWasher.setHasWorkToDo(dishWasherFlag);
}
// Method 2: Delegate work to all appliances
public void doKitchenWork() {
brewMaster.brewCoffee();
iceBox.orderFood();
dishWasher.doDishes();
}
}
The Component Appliances¶
class CoffeeMaker {
private boolean hasWorkToDo;
public void setHasWorkToDo(boolean hasWorkToDo) {
this.hasWorkToDo = hasWorkToDo;
}
public void brewCoffee() {
if (hasWorkToDo) {
System.out.println("Coffee is brewing...");
hasWorkToDo = false;
}
}
}
class Refrigerator {
private boolean hasWorkToDo;
public void setHasWorkToDo(boolean hasWorkToDo) {
this.hasWorkToDo = hasWorkToDo;
}
public void orderFood() {
if (hasWorkToDo) {
System.out.println("Ordering Food...");
hasWorkToDo = false;
}
}
}
class DishWasher {
private boolean hasWorkToDo;
public void setHasWorkToDo(boolean hasWorkToDo) {
this.hasWorkToDo = hasWorkToDo;
}
public void doDishes() {
if (hasWorkToDo) {
System.out.println("Washing dishes...");
hasWorkToDo = false;
}
}
}
Using the Smart Kitchen¶
SmartKitchen kitchen = new SmartKitchen();
// Set which appliances have work to do
kitchen.setKitchenState(true, false, true); // Coffee & dishes only
// Do all the work - kitchen delegates to parts
kitchen.doKitchenWork();
// Output:
// Coffee is brewing...
// Washing dishes...
Composition vs Inheritance¶
Why Prefer Composition Over Inheritance?¶
| Aspect | Inheritance | Composition |
|---|---|---|
| Flexibility | Less flexible - locked into hierarchy | More flexible - can swap parts |
| Coupling | Tight coupling between classes | Loose coupling - parts are independent |
| Changes | Changes cascade to all subclasses | Changes isolated to components |
| Reuse | Limited to hierarchy | Reuse outside class hierarchy |
| Testing | Harder to test in isolation | Easy to test parts independently |
| Encapsulation | May break encapsulation (protected) | Preserves encapsulation |
When to Use Each¶
flowchart TD
A{Does it make sense to say<br>X IS-A Y?}
A -->|Yes| B{Will subclass use ALL<br>parent functionality?}
A -->|No| C[Use Composition]
B -->|Yes| D[Consider Inheritance]
B -->|No| C
D --> E{Is behavior fixed<br>at compile time?}
E -->|Yes| F[Inheritance is OK]
E -->|No| C
The Digital Product Problem¶
Consider adding DigitalProduct to a Product hierarchy:
public class Product {
private String manufacturer;
private String model;
private int width; // What about digital products?
private int height; // They don't have dimensions!
private int depth;
}
class DigitalProduct extends Product {
// PROBLEM: Inherits width, height, depth
// which makes no sense for software!
}
Solution with Composition:
// Make Product more generic
public class Product {
private String manufacturer;
private String model;
}
// Create Dimensions as a component
class Dimensions {
private int width;
private int height;
private int depth;
}
// Physical products USE Dimensions
class Motherboard extends Product {
private Dimensions dimensions; // HAS-A dimensions
}
// Digital products don't need Dimensions
class SoftwareProduct extends Product {
private String licenseKey;
// No Dimensions needed!
}
Key Takeaways¶
Composition Best Practices¶
- "Favor composition over inheritance" - Common OOP design principle
- Use inheritance for IS-A relationships where subclass truly is a type of parent
- Use composition for HAS-A relationships where class contains other objects
- Combine both when appropriate (class inherits AND contains)
- Hide components when possible - delegate through public methods
- Create parts internally when they don't need to be configured externally
Benefits Summary¶
- Flexibility: Add/remove/change parts without affecting hierarchy
- Reuse: Same components can be used in different composites
- Encapsulation: Hide complexity behind simple interfaces
- Maintainability: Changes are localized to components
Related Notes¶
| Part | Topic | Link |
|---|---|---|
| 1 | Classes, Objects & Encapsulation | ← Part 1 |
| 2 | Inheritance & Method Overriding | ← Part 2 |
| 3 | Strings & StringBuilder | ← Part 3 |
| 4 | Composition | You are here |
| 5 | Encapsulation | Part 5 → |
| 6 | Polymorphism | Part 6 → |
Last Updated: 2026-01-26