Skip to content

Instantly share code, notes, and snippets.

@danharper
Last active May 17, 2022 15:20
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danharper/87269a0628ed97fb026e to your computer and use it in GitHub Desktop.
Save danharper/87269a0628ed97fb026e to your computer and use it in GitHub Desktop.
JavaScript Command Bus
const handlers = Symbol('handlers');
// adapts a handler class to a common interface
// (technically this one isn't required - the handler class already implements the same interface)
class ClassDispatchable {
constructor(theClass) {
this.theClass = theClass;
}
handle(command) {
return (new this.theClass).handle(command);
}
}
// adapts a handler function to a common interface
class FuncDispatchable {
constructor(func) {
this.func = func;
}
handle(command) {
return this.func(command);
}
}
// the command bus - where the magic happens
class Bus {
constructor() {
this[handlers] = {};
}
// register a class as the handler for a command
register(command, theClass) {
this[handlers][command.__COMMAND__] = new ClassDispatchable(theClass);
return this;
}
// register a function as the handler for a command
registerFunc(command, func) {
this[handlers][command.__COMMAND__] = new FuncDispatchable(func);
return this;
}
// given a command instance, dispatch to the registered handler
dispatch(command) {
var handler = this[handlers][command.constructor.__COMMAND__];
if (handler) {
return handler.handle(command);
}
}
}
export default Bus
// given a class, adds a unique __COMMAND__ Symbol which we can
// use to identify handlers for the command within the Bus
export default function(command) {
command.__COMMAND__ = Symbol();
return command;
}
import command from '../core/command'
// a command is just a plain DTO class
class AddUnitCommand {
constructor(name) {
this.name = name;
}
}
export default command(AddUnitCommand)
// a class-based handler
// when given a Command to handle(), performs the relevant actions
export default class AddUnit {
handle(command) {
console.log(`adding unit with name ${command.name}`, command)
return ':)'
}
}
import command from '../core/command'
class RemoveUnitCommand {
constructor(id) {
this.id = id;
}
}
export default command(RemoveUnitCommand)
// a function-based handler
// when called with a Command, performs the relevant actions
export default function(command) {
console.log(`removing unit with id ${command.id}`)
return ':('
}
import Bus from './core/Bus'
import AddUnitCommand from './commands/AddUnitCommand'
import AddUnit from './handlers/AddUnit'
import RemoveUnitCommand from './commands/RemoveUnitCommand'
import RemoveUnit from './handlers/RemoveUnit'
// AppBus is an instance of the bus with all Commands & Handlers registered
export default (new Bus)
.register(AddUnitCommand, AddUnit)
.registerFunc(RemoveUnitCommand, RemoveUnit)
import AppBus from './AppBus'
import AddUnitCommand from './commands/AddUnitCommand'
import RemoveUnitCommand from './commands/RemoveUnitCommand'
// dispatching "AddUnitCommand" executes the "AddUnit" class
var a = AppBus.dispatch(new AddUnitCommand('Foo Bar'));
console.log('a', a); // :)
// dispatching "RemoveUnitCommand" executes the "RemoveUnit" function
var b = AppBus.dispatch(new RemoveUnitCommand(2));
console.log('b', b); // :(
@danharper
Copy link
Author

Handlers then communicate with a Store/Repository/whatever-you-want-to-call it to retrieve, add, persist and remove entities. That Store is responsible for emitting change events which Views listen for to update.

@danharper
Copy link
Author

Note to self: Need to swap out the __COMMAND__ properties with a symbol for private access - already done in a side project, need to add back into this gist.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment