Props on starting with a UML!
In an ideal case of the command pattern, you can execute any command without knowing what the command is or which commands are available because each command is a self-containing object. However if you are creating a command instance based on stringified data, then at some point you have to map a string name to its class implementation.
You mention a CommandListener
in your UML. I really don't know how all of this is executed on the server, but it would probably be ideal if each ConcreteCommand
could attach its own listener, which listens to all commands and handles the ones that match its name.
Another idea is to have a centralized TaskParser
that responds to all JSON actions by converting them to Command
s and assigning them to the invoker (which I've called CommandRunner
here). Each ConcreteCommand
has to register itself to the TaskParser
.
In thinking about what a Command
needs in order to be created from a JSON string, I first came up with a generic StringableCommand
which depends on parameters Args
. It has a name
for identification, can be constructed from Args
, and it has a method validateArgs
that serves as a type guard. If validateArgs
returns true
, then the args
array is known to be of type Args
and it is safe to call new
.
interface StringableCommand<Args extends any[]> extends Command {
new(...args: Args): Command;
name: string;
validateArgs( args: any[] ): args is Args;
}
Note that this interface applies to the class prototype, not to the class instance (that part tripped me up a bit) so we don't say that our class extends StringableCommand
. We must implement validateArgs
as a static
method in order for it to be available on the prototype. We don't need to explicitly implement name
because it
already exists.
However having the generic in StringableCommand
makes things confusing for our TaskParser
, which will hold many commands of different types. For that reason, I think it's better to define a constructable command as one with a method fromArgs
that takes an array of any/unknown arguments and either returns a Command
or throws an Error
(you could return null
or undefined
instead). The class can deal with validation internally, or we can rely on the fact that the constructor should throw an error (but we would have to tell typescript to ignore that).
interface StringableCommand {
name: string;
fromArgs( ...args: any[] ): Command; // or throw an error
}
We are dealing with a JSON string that parses to this:
interface ActionPayload {
action: string;
tasks: TaskPayload[];
}
interface TaskPayload {
command: string;
parameters: any[];
timestamp: number;
id: string;
}
I've changed your parameters
to be an array rather than a keyed object so that it's easier to pass them as arguments to the constructor of the command.
Our TaskParser
class to resolve these commands looks something like this:
class TaskParser {
private readonly runner: CommandRunner;
private commandConstructors: Record<string, StringableCommand> = {}
private errors: Error[] = [];
constructor(runner: CommandRunner) {
this.runner = runner;
}
public registerCommand( command: StringableCommand ): void {
this.commandConstructors[command.name] = command;
}
public parseJSON( json: string ): void {
const data = JSON.parse(json) as ActionPayload;
data.tasks.forEach(({command, parameters, id, timestamp}) => {
const commandObj = this.findCommand(command);
if ( ! commandObj ) {
this.storeError( new Error(`invalid command name ${command}`) );
return;
}
try {
const c = commandObj.fromArgs(parameters);
this.runner.addCommand(c, timestamp, id);
} catch (e) { // catch errors thrown by `fromArgs`
this.storeError(e);
}
});
}
private findCommand( name: string ): StringableCommand | undefined {
return this.commandConstructors[name];
}
private storeError( error: Error ): void {
this.errors.push(error);
}
}
It takes a CommandRunner
which right now is just a stub because I don't know your implementation details, but this would be where you invoke/execute the commands.
class CommandRunner {
addCommand( command: Command, timestamp: number, id: string ): void { // whatever args you need
// do something
}
}
Typescript Playground Link