Search code examples
typescriptcommand-pattern

Typescript Command pattern return type


Below is command pattern where you have a Command and a Handler for a command, they get added to the CommandBus and then executed there when called upon. The command has a type in this case <string> that I want to be returned when executing the Command in the CommandBus

The issue is the last line var whatIsThis = CommandBus.execute<string>(new GetStringCommand("Hello, world")); more specifically I want to remove form that statement because it should come from the Command. The CommandBus class should know it should return from class GetStringCommand implements Command<string>.

interface Command<T> {
    name:string;
}

class GetStringCommand implements Command<string> {
    public str:string;
    public name:string = "GetStringCommand";
    constructor(str:string){
        this.str = str;
    }
}

interface CommandHandler<T> {
    execute(command:Command<T>): T;
}

class GetStringHandler implements CommandHandler<string> {
    execute(command:GetStringCommand):string {
        return command.str;
    }
}

interface CommandRegistry {
    [x:string]: CommandHandler<any>
}

class CommandBus {
    private static actions:CommandRegistry = {};

    static add(name:string, command:CommandHandler<any>) {
        CommandBus.actions[name] = command;
    }

    static execute<T>(command:Command<T>) : T {
        return CommandBus.actions[command.name].execute(command);
    }
}

CommandBus.add("GetStringCommand", new GetStringHandler());

var whatIsThis = CommandBus.execute<string>(new GetStringCommand("Hello, world"));

The solution above is valid and works fine, however it's a terrible solution because it makes refactoring painful, and it makes me have to repeat myself over and over again since I'll use thousands of commands.

Actual example Here's an example of a command handler that saves a schema in MongoDB, the same command could be utilized for creating a schema in MySQL

export class SaveSchemaCommandHandler implements CommandHandlerBase<void> {
    execute(command:SaveSchemaCommand) {
        var db = MongoConnection.db;
        var collectionOptions = { autoIndexID: true };

        db.createCollection(command.schema.getName(), collectionOptions);
    }
}

Solution

  • From what I understand your handlers are the real commands, while what you call command is simply the params that the handler needs in order to execute.

    You can replace your "commands" with simple interfaces, and your handlers become the commands.
    You can then remove the need for classes for these new commands because you can just pass the execute function as the command.

    Something like this:

    interface CommandData {}
    
    interface GetStringCommandData extends CommandData {
        value: string;
    }
    
    interface SaveSchemaCommandData extends CommandData {
        schema: { name: string };
    }
    
    type Command<In extends CommandData, Out> = (data: In) => Out;
    
    interface CommandRegistry {
        [x: string]: Command<CommandData, any>;
    }
    
    class CommandBus {
        private static actions:CommandRegistry = {};
    
        static add(name: string, command: Command<CommandData, any>) {
            CommandBus.actions[name] = command;
        }
    
        static execute<T>(name: string, data: CommandData) : T {
            return CommandBus.actions[name](data);
        }
    }
    
    CommandBus.add("GetStringCommand", (data: GetStringCommandData) => data.value);
    CommandBus.add("SaveSchemaCommand", (data: SaveSchemaCommandData) => {
        let db = MongoConnection.db;
        let collectionOptions = { autoIndexID: true };
    
        db.createCollection(data.schema.name, collectionOptions);
    });
    
    CommandBus.execute("GetStringCommand", { value: "my string" });
    CommandBus.execute("SaveSchemaCommand", { schema: { name: "mySchema" } });
    

    It seems simpler and easier to maintain, but I'm not sure if it fits all your needs.


    Edit

    It's not as trivial to get the right type from the execute operation, but you have a few options:

    (1) Using a return type generics in CommandData:

    interface CommandData<T> {}
    
    interface GetStringCommandData extends CommandData<string> {
        value: string;
    }
    
    class CommandBus {
        ...
    
        static execute<T>(name: string, data: CommandData<T>): T {
            return CommandBus.actions[name](data);
        }
    }
    
    let str: string = CommandBus.execute("GetStringCommand", { value: "my string" } as CommandData<string>);
    

    (2) Using any:

    class CommandBus {
        ...
    
        static execute(name: string, data: CommandData): any {
            return CommandBus.actions[name](data);
        }
    }
    
    let str: string = CommandBus.execute("GetStringCommand", { value: "my string" });
    

    (3) Declaring the possible signatures for the execute:

    class CommandBus {
        ...
    
        static execute(name: "GetStringCommand", data: GetStringCommandData): string;
        static execute(name: "SaveSchemaCommand", data: SaveSchemaCommandData): void;
        static execute(name: string, data: CommandData): any;
        static execute(name: string, data: CommandData): any {
            return CommandBus.actions[name](data);
        }
    }
    
    let str: string = CommandBus.execute("GetStringCommand", { value: "my string" });
    

    This option isn't very scaleable, but it's still an option.