Skip to Content
Course content

284: Practice Exercise: Implementing a Simple Event Bus

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

I've always found that the hardest part of growing a Java project isn't writing the initial features—it's managing how those features talk to each other. If every class has a direct reference to every other class, you end up with a "big ball of mud" where changing one line in the User class breaks the EmailService, the AnalyticsEngine, and the DatabaseLogger. This is why we use an Event Bus.

The Coupling Problem

Let's imagine we're building a simple RPG. When an enemy dies, a few things need to happen: the player gets XP, the quest tracker checks if we've killed enough goblins, and the UI needs to show a "Victory!" toast. My first instinct might be to just put all that logic inside the Enemy class:

public class Enemy {
    private QuestTracker questTracker;
    private ExperienceManager xpManager;
    private UIManager uiManager;

    public void die() {
        xpManager.addXp(100);
        questTracker.notifyKill("Goblin");
        uiManager.showToast("Enemy Defeated!");
    }
}

This looks fine for a weekend project, but it's a nightmare for a real system. The Enemy class now needs to know about the UI, the XP system, and the quest system. If I decide to add a SoundManager to play a death sound, I have to modify the Enemy class again. I don't like that. The enemy should just know it died; it shouldn't care who is listening.

Moving to a Central Hub

I want a middleman. I'll create an EventBus where the Enemy can just shout, "I died!" into the void, and whoever cares can listen for that specific shout. I'll start simple. I'll create a basic event class and a bus that holds a list of listeners.

public class EnemyDeathEvent {
    public final String enemyType;
    public EnemyDeathEvent(String type) { this.enemyType = type; }
}

public class EventBus {
    private final List<Consumer<Object>> listeners = new ArrayList<>();

    public void subscribe(Consumer<Object> listener) {
        listeners.add(listener);
    }

    public void publish(Object event) {
        listeners.forEach(l -> l.accept(event));
    }
}

I tried running this, and while it "works," I hit a wall immediately. Since the listeners are Consumer<Object>, every single listener has to manually check if (event instanceof EnemyDeathEvent) and cast it. That's tedious and feels very "Java 1.4." We can do better with generics.

Fighting the Type System

I want to be able to subscribe to a specific type of event. I'll try using a Map where the key is the event class and the value is a list of listeners for that specific class. This is where Java's type system usually starts fighting back.

public class EventBus {
    private final Map<Class<?>, List<Consumer<?>>> subscribers = new HashMap<>();

    public <T> void subscribe(Class<T> eventType, Consumer<T> listener) {
        subscribers.computeIfAbsent(eventType, k -> new ArrayList<>()).add(listener);
    }

    public void publish(Object event) {
        Class<?> eventType = event.getClass();
        List<Consumer<?>> eventListeners = subscribers.get(eventType);
        
        if (eventListeners != null) {
            for (Consumer<?> listener : eventListeners) {
                // Here is the tricky part...
                ((Consumer<Object>) listener).accept(event);
            }
        }
    }
}

You'll notice that ugly cast to (Consumer<Object>). I'll be honest: it feels a bit dirty, but because we've controlled the input in the subscribe method (ensuring the listener matches the eventType key), this is safe. The JVM will complain about "unchecked" casts, but we've logically guaranteed the types match.

Putting it into Action

Now let's see if this actually solves our coupling problem. I can now set up my systems independently. The Enemy class doesn't need to know about the QuestTracker anymore; it only needs a reference to the EventBus.

// Setup
EventBus bus = new EventBus();

// The QuestTracker just registers itself
bus.subscribe(EnemyDeathEvent.class, event -> {
    System.out.println("Quest updated: " + event.enemyType + " slain!");
});

// The Enemy just publishes
public class Enemy {
    private EventBus bus;
    public Enemy(EventBus bus) { this.bus = bus; }

    public void die() {
        bus.publish(new EnemyDeathEvent("Goblin"));
    }
}

This is much cleaner. If I want to add a SoundManager, I just create a new subscriber in my main initialization code. I never have to touch the Enemy class again. We've successfully decoupled the cause (enemy death) from the effect (XP, Quests, UI).




📋 Practical Task

Exercise: Implementing a Player Inventory Event System

Build a decoupled inventory notification system using the Event Bus pattern explored in this lesson.

  • The Event: Create an ItemPickedUpEvent class that contains the itemName (String) and the itemWeight (double).
  • The Bus: Implement an EventBus class with a subscribe method (using a Map of classes to listeners) and a publish method.
  • The Listeners:
    • Create a WeightTracker that subscribes to ItemPickedUpEvent and maintains a running total of the player's total carried weight.
    • Create a InventoryLogger that subscribes to ItemPickedUpEvent and prints a message: "Log: Player picked up [itemName]".
  • The Test: In a main method, instantiate the bus and the listeners. Then, simulate picking up three different items (e.g., "Iron Sword", "Healing Potion", "Heavy Shield") by publishing events to the bus. Verify that both the logger prints the message and the weight tracker calculates the correct total weight.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.