Simple Dispatcher: A Beginner’s Guide to Event Handling
Event-driven programming helps applications react to user actions, network responses, timers, and other occurrences without tightly coupling components. A simple dispatcher is a lightweight pattern that routes events to interested listeners, making your code more modular, testable, and easier to maintain. This guide explains what a dispatcher is, why to use one, and shows clear, practical examples you can adapt to your projects.
What is a Dispatcher?
A dispatcher is a component that receives events (named messages or payloads) and calls registered handlers for those events. It’s essentially a pub/sub (publish–subscribe) mechanism: publishers emit events, and subscribers register callbacks to handle them. Unlike full message brokers, a simple dispatcher runs in-process and is ideal for front-end apps, small services, or as a building block inside larger systems.
Why use a Simple Dispatcher?
- Decoupling: Publishers don’t need to know about subscribers.
- Flexibility: Add or remove handlers at runtime.
- Testability: Handlers can be tested independently.
- Readability: Clear separation of responsibilities.
- Lightweight: Minimal overhead compared to external messaging systems.
Core Features
A simple dispatcher typically supports:
- Registering event handlers (subscribe)
- Removing handlers (unsubscribe)
- Emitting events (publish/dispatch)
- Optionally: once-only handlers, wildcard events, and handler priorities
Basic Implementation (JavaScript)
Here’s a concise, easy-to-read implementation you can drop into any project.
class SimpleDispatcher { constructor() { this.handlers = new Map(); // eventName -> Set of callbacks } on(event, callback) { if (!this.handlers.has(event)) this.handlers.set(event, new Set()); this.handlers.get(event).add(callback); return () => this.off(event, callback); // convenience: unsubscribe } off(event, callback) { const set = this.handlers.get(event); if (!set) return; set.delete(callback); if (set.size === 0) this.handlers.delete(event); } once(event, callback) { const wrapper = (…args) => { callback(…args); this.off(event, wrapper); }; this.on(event, wrapper); } emit(event, …args) { const set = this.handlers.get(event); if (!set) return; // copy to avoid issues if handlers change during iteration for (const cb of Array.from(set)) { try { cb(…args); } catch (err) { // handle or log handler errors without stopping others console.error(Error in handler for "${event}":, err); } } }}
Usage example:
const dispatcher = new SimpleDispatcher(); const unsub = dispatcher.on(‘user:login’, user => { console.log(‘User logged in:’, user.name);}); dispatcher.emit(‘user:login’, { name: ‘Alice’ });// -> User logged in: Alice unsub(); // stop listeningdispatcher.emit(‘user:login’, { name: ‘Bob’ }); // no output
Advanced Features and Variations
- Wildcard events: Support patterns like “user:” or “” to match multiple events.
- Priorities: Allow handlers with priority levels so some run before others.
- Synchronous vs asynchronous dispatch: Emit can call handlers synchronously (above) or schedule them with setTimeout/Promise.resolve to avoid blocking.
- Error handling strategies: Aggregate errors, report to a monitoring service, or continue silently.
- Payload enveloping: Use a consistent event object { type, payload, meta } for richer semantics.
Example: Once-only and Async Handlers
dispatcher.once(‘init’, () => console.log(‘initialized once’)); dispatcher.on(‘data’, async payload => { await doAsyncWork(payload);});
If you prefer async-safe emission (await handlers), modify emit to:
async emitAsync(event, …args) { const set = this.handlers.get(event); if (!set) return; for (const cb of Array.from(set)) { try { await cb(…args); } catch (err) { console.error(err); } }}
When Not to Use a Simple Dispatcher
- Distributed systems requiring durability, persistence, or cross-process message delivery (use a message broker instead).
- High-throughput, low-latency backends where an in-process dispatcher becomes a bottleneck.
- Situations needing guaranteed delivery, retries, or complex routing rules.
Testing Tips
- Use spies or mocks to assert handlers are called with expected payloads.
- Test unsubscribe by ensuring handlers no longer run after off()/returned unsubscribe is called.
- For async handlers, await emitAsync and assert order/behavior.
Best Practices
- Keep event names consistent and namespaced (e.g., “user:login”, “cart:item:add”).
- Avoid emitting large payloads; pass references or IDs when possible.
- Clean up listeners to prevent memory leaks (unsubscribe on component unmount).
- Log or monitor handler errors to avoid silent failures.
Summary
A simple dispatcher is a straightforward, powerful pattern for event handling that encourages loose coupling and clearer architecture. Start with the basic implementation above, add features only when needed (wildcards, priorities, async support), and move to a full messaging system only when your application needs cross-process or durable messaging.
Further reading and variations: implement the same pattern in other languages (Python, Java, Go) using maps/dictionaries with function references or channels, adapting for thread-safety where needed.
Leave a Reply