Skip to Content
Course content

306: Composition Over Inheritance in Practice

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I want to show you a snippet of code I inherited from a junior dev a few years ago. They were building a simple RPG combat system. On the surface, it looks logical: a Character base class, and then specific roles like Warrior and Mage inheriting from it. But look at what happened as the game grew.

public class GameCharacter {
    public String name;
    
    public void attack() {
        System.out.println(name + " attacks!");
    }

    // Added later because Mages needed it
    public void castSpell() {
        throw new UnsupportedOperationException("This character cannot cast spells");
    }
}

public class Warrior extends GameCharacter {
    @Override
    public void attack() {
        System.out.println(name + " swings a massive sword!");
    }
}

public class Mage extends GameCharacter {
    @Override
    public void castSpell() {
        System.out.println(name + " casts Fireball!");
    }
}

public class Paladin extends Warrior {
    // Paladins are Warriors, but they also need to cast spells.
    // But we can't inherit from Mage too!
    @Override
    public void castSpell() {
        System.out.println(name + " casts Holy Light!");
    }
}

The "Junk-Drawer" Base Class

Do you see the problem here? The developer hit a wall with Java's single inheritance. When the Paladin arrived—a character that is both a fighter and a spellcaster—they realized they couldn't inherit from both Warrior and Mage. To "fix" this, they started pushing methods like castSpell() up into the GameCharacter base class.

This is a classic trap. Now, every single character in the game—even a simple peasant or a dog—inherits a castSpell() method they can't actually use. We've created a "junk-drawer" base class. The moment you call castSpell() on a Warrior, the program crashes with an UnsupportedOperationException. We've violated the Liskov Substitution Principle; a Warrior is no longer a reliable substitute for a GameCharacter because it breaks the contract of the base class.

Decoupling Abilities via Composition

The fix isn't to find a way to inherit from more classes; it's to stop asking "what is this object?" and start asking "what can this object do?". Instead of saying a Paladin is a Warrior, we say a Character has a combat style and has a magic ability.

I prefer to extract these behaviors into interfaces and separate classes. This is composition. Here is how I would refactor that mess:

public interface AttackBehavior {
    void executeAttack(String name);
}

public interface MagicBehavior {
    void executeSpell(String name);
}

// Specific implementations
public class SwordAttack implements AttackBehavior {
    public void executeAttack(String name) { System.out.println(name + " swings a sword!"); }
}

public class FireballSpell implements MagicBehavior {
    public void executeSpell(String name) { System.out.println(name + " casts Fireball!"); }
}

public class HolySpell implements MagicBehavior {
    public void executeSpell(String name) { System.out.println(name + " casts Holy Light!"); }
}

public class GameCharacter {
    public String name;
    private AttackBehavior attackBehavior;
    private MagicBehavior magicBehavior; // Can be null if they can't cast

    public GameCharacter(String name, AttackBehavior attack, MagicBehavior magic) {
        this.name = name;
        this.attackBehavior = attack;
        this.magicBehavior = magic;
    }

    public void attack() {
        attackBehavior.executeAttack(name);
    }

    public void cast() {
        if (magicBehavior != null) {
            magicBehavior.executeSpell(name);
        } else {
            System.out.println(name + " can't cast spells!");
        }
    }
}

Why this actually works in the long run

Notice how much more flexible this is. If we want to create a Paladin, we don't need a new class. We just instantiate a GameCharacter and pass in a SwordAttack and a HolySpell. If we want a Mage, we pass in a StaffAttack and a FireballSpell.

But the real superpower here is runtime flexibility. With inheritance, a Warrior is a Warrior until the program ends. With composition, I can change the AttackBehavior on the fly. If your character picks up a bow, you just call setAttackBehavior(new BowAttack()). You can't "change your parent class" at runtime, but you can absolutely change your components.




📋 Practical Task

Build a Dynamic Weapon Switching System

You are tasked with implementing a weapon system for a character that can switch between different attack modes during a game. Instead of creating separate classes for Archer, Swordsman, and Brawler, you will use composition.

Requirements:

  • Create an interface Weapon with a method String useWeapon().
  • Implement three versions of Weapon: Sword (returns "Slashes with a sword!"), Bow (returns "Shoots an arrow!"), and Fists (returns "Punches with fists!").
  • Create a Player class that:
    • Holds a reference to a Weapon object.
    • Has a method setWeapon(Weapon newWeapon) to change the equipped weapon.
    • Has a method performAttack() that calls the current weapon's useWeapon() method and prints the result.
  • In your main method, instantiate a Player, make them attack with a Sword, switch to a Bow, and then attack again.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.