Last active
September 3, 2019 19:59
-
-
Save malef/fd582052a5b36b4eaf6aa06bc2cb924e to your computer and use it in GitHub Desktop.
Draft of command executor using generics and advanced types.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ----- Logger (mostly irrelevant for this case). | |
interface LoggerInterface { | |
info(message: string): void; | |
} | |
class Logger implements LoggerInterface { | |
info(message: string): void { | |
console.log(message); | |
} | |
} | |
// ----- Command kinds defined as enum so that I don't need to repeat strings. | |
enum CommandKind { | |
first = 'first', | |
second = 'second', | |
} | |
// ----- Command payloads (input) and results (output) defined. | |
type CommandPayload<T extends CommandKind> = | |
T extends CommandKind.first ? FirstCommandPayload : | |
T extends CommandKind.second ? SecondCommandPayload : | |
never; | |
type CommandResult<T extends CommandKind> = | |
T extends CommandKind.first ? FirstCommandResult : | |
T extends CommandKind.second ? SecondCommandResult : | |
never; | |
type FirstCommandPayload = { | |
readonly source: string; | |
readonly foo: number; | |
readonly bar: string; | |
}; | |
type FirstCommandResult = { | |
readonly foo: number; | |
readonly bar: string; | |
}; | |
type SecondCommandPayload = { | |
readonly source: string; | |
readonly baz: string; | |
}; | |
type SecondCommandResult = { | |
readonly baz: string, | |
}; | |
// ----- Command defined wrapping kind, payload and result. | |
type Command<T extends CommandKind> = { | |
kind: T; | |
payload: CommandPayload<T>; | |
result?: CommandResult<T>; | |
} | |
// ---- Command handlers with interface to ensure that main method is defined properly. | |
type CommandHandler<T extends CommandKind> = | |
T extends CommandKind.first ? FirstCommandHandler : | |
T extends CommandKind.second ? SecondCommandHandler : | |
never; | |
interface CommandHandlerInterface<T extends CommandKind> { | |
handleCommand( | |
commandPayload: CommandPayload<T>, | |
logger: LoggerInterface | |
): Promise<CommandResult<T>>; | |
} | |
class FirstCommandHandler implements CommandHandlerInterface<CommandKind.first> { | |
async handleCommand( | |
commandPayload: CommandPayload<CommandKind.first>, | |
logger: LoggerInterface | |
): Promise<CommandResult<CommandKind.first>> { | |
logger.info(`executing first`); | |
const commandResult: FirstCommandResult = { | |
foo: commandPayload.foo * 10, | |
bar: commandPayload.bar + '!', | |
}; | |
return commandResult; | |
} | |
} | |
class SecondCommandHandler implements CommandHandlerInterface<CommandKind.second> { | |
async handleCommand( | |
commandPayload: CommandPayload<CommandKind.second>, | |
logger: LoggerInterface | |
): Promise<CommandResult<CommandKind.second>> { | |
logger.info('executing second'); | |
const commandResult: SecondCommandResult = { | |
baz: commandPayload.baz + '!', | |
}; | |
return commandResult; | |
} | |
} | |
// ----- Map used to match handlers with commands using kind. | |
type CommandHandlersMap = { | |
[commandKind in CommandKind]: CommandHandler<commandKind>; | |
}; | |
// ----- Handler able to handle any registered command(s). | |
class CompositeCommandHandler { | |
constructor( | |
readonly commandHandlersMap: CommandHandlersMap, | |
) {} | |
async handleOne(command: Command<CommandKind>): Promise<Command<CommandKind>> { | |
const logger = new Logger(); | |
const commandKind = command.kind; | |
const commandHandler = this.commandHandlersMap[commandKind]; | |
// I was hoping this will work, but it highlights command.payload as not matching | |
// FirstCommandPayload & SecondCommandPayload which probably means it cannot determine | |
// handler type properly. | |
// | |
// if ( | |
// this.commandTypeGuard(command, commandKind) | |
// && this.commandHandlerTypeGuard(commandHandler, commandKind) | |
// ) { | |
// command.result = await commandHandler.handleCommand(command.payload, logger); | |
// } | |
// | |
// Therefore I have to do it like below, using a list of if statements which doesn't look nice. | |
if ( | |
this.commandTypeGuard(command, CommandKind.first) | |
&& this.commandHandlerTypeGuard(commandHandler, CommandKind.first) | |
) { | |
command.result = await commandHandler.handleCommand(command.payload, logger); | |
} else if ( | |
this.commandTypeGuard(command, CommandKind.second) | |
&& this.commandHandlerTypeGuard(commandHandler, CommandKind.second) | |
) { | |
command.result = await commandHandler.handleCommand(command.payload, logger); | |
} else { | |
throw new Error('Command handler not found'); | |
} | |
return command; | |
} | |
async handleMany(commands: Command<CommandKind>[]): Promise<Command<CommandKind>[]> { | |
const results: Command<CommandKind>[] = []; | |
for (const command of commands) { | |
results.push( | |
await this.handleOne(command), | |
); | |
} | |
return results; | |
} | |
private commandTypeGuard<T extends CommandKind>( | |
command: Command<CommandKind>, | |
commandKind: T, | |
): command is Command<T> { | |
return command.kind === commandKind; | |
} | |
private commandHandlerTypeGuard<T extends CommandKind>( | |
commandHandler: CommandHandler<CommandKind>, | |
commandKind: T, | |
): commandHandler is CommandHandler<T> { | |
return this.commandHandlersMap[commandKind] === commandHandler; | |
} | |
} | |
// ----- Putting it all together and executing some commands. | |
const compositeCommandHandler = new CompositeCommandHandler( | |
{ | |
[CommandKind.first]: new FirstCommandHandler(), | |
[CommandKind.second]: new SecondCommandHandler(), | |
} | |
); | |
const commands: Command<CommandKind>[] = [ | |
// What can I do to avoid casting these objects to <Command<CommandKind.first>>? | |
// Can type be somehow determined by kind property being checked automatically? | |
<Command<CommandKind.first>> { | |
kind: CommandKind.first, | |
payload: { | |
source: 'repo-1', | |
foo: 7, | |
bar: 'bar-1', | |
}, | |
}, | |
<Command<CommandKind.first>> { | |
kind: CommandKind.first, | |
payload: { | |
source: 'repo-2', | |
foo: 3, | |
bar: 'bar-2', | |
}, | |
}, | |
<Command<CommandKind.second>> { | |
kind: CommandKind.second, | |
payload: { | |
source: 'repo-3', | |
baz: 'baz-1', | |
}, | |
}, | |
<Command<CommandKind.first>> { | |
kind: CommandKind.first, | |
payload: { | |
source: 'repo-3', | |
foo: 5, | |
bar: 'bar-3', | |
}, | |
}, | |
]; | |
console.log(); | |
console.log('Execution started.'); | |
console.log(); | |
compositeCommandHandler | |
.handleMany(commands) | |
.then( | |
results => { | |
console.log(); | |
console.log('Execution completed.'); | |
console.log(); | |
console.log(results); | |
console.log(); | |
} | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment