【问题标题】:Typescript Command pattern return typeTypescript 命令模式返回类型
【发布时间】:2016-12-16 12:03:00
【问题描述】:

下面是命令模式,其中你有一个命令和一个命令处理程序,它们被添加到CommandBus,然后在被调用时在那里执行。在这种情况下,该命令有一个类型 <string>,我希望在执行 CommandBus 中的命令时返回它

问题是最后一行var whatIsThis = CommandBus.execute<string>(new GetStringCommand("Hello, world"));,更具体地说,我想从该语句中删除,因为它应该来自CommandCommandBus 类应该知道它应该从 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"));

上面的解决方案是有效的并且工作正常,但它是一个糟糕的解决方案,因为它使重构变得痛苦,而且它让我不得不一遍又一遍地重复自己,因为我将使用数千个命令。

实际示例 这是一个在 MongoDB 中保存模式的命令处理程序示例,可以使用相同的命令在 MySQL 中创建模式

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

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

【问题讨论】:

  • 为什么需要处理程序?你不能只在Command 中有execute 吗?
  • 关注点分离。这个想法是一个命令可以被多个命令处理程序使用,例如,对于一个保存操作,并且您有多个数据库系统,然后这些数据库将每个都有一个命令处理程序来执行该操作(并且它们本质上将成为即插即用play),你不需要为所有数据库编写一个巨大的命令,而且,如果你想自定义你想要使用的数据库,那么......如果没有处理程序,这将是不可能的
  • 如果您有不同的数据库,那么您需要不同的命令才能使用它们。除非我在您的设计中遗漏了某些东西,否则处理程序似乎只是返回值的命令的包装器。也许你可以在你的代码中加入一个更有趣的例子?
  • 见上面的例子

标签: typescript command-pattern


【解决方案1】:

据我了解,您的处理程序是真正的命令,而您所说的命令只是处理程序执行所需的参数。

您可以将“命令”替换为简单的接口,您的处理程序将成为命令。
然后,您可以消除对这些新命令的类的需求,因为您可以将执行函数作为命令传递。

类似这样的:

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" } });

它看起来更简单,更容易维护,但我不确定它是否满足您的所有需求。


编辑

从执行操作中获取正确的类型并非易事,但您有几个选择:

(1) 在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) 使用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) 为执行声明可能的签名:

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" });

这个选项不是很可扩展,但它仍然是一个选项。

【讨论】:

  • 我认为用接口替换命令是个好主意,因为它们只包含结构,但是上面的返回类型存在问题:var xyz:string = CommandBus.execute("GetStringCommand", { value: "my string" }); 会给出错误 "Type '{ }' 不能分配给类型 'string'" 你可以在 之前传递,但我想避免这种情况
  • 我认为这个解决方案已经足够好了,函数必须是类,因为它们包含更多的正常执行,感谢您的帮助!
  • 检查我修改后的答案,我给了你 3 个选项来解决这个问题。
  • @NitzanTomer 在您的第一个示例中,执行命令时,我会传入GetStringCommandData 而不是基本CommandData&lt;string&gt;。但是泛型的返回类型推断似乎丢失了,变成了{}。有没有办法在不包括GetStringCommandData 中的泛型的情况下解决这个问题?
  • @ShawnMclean 我猜你可以使用Type inference in conditional types
猜你喜欢
  • 2019-09-21
  • 2010-11-12
  • 1970-01-01
  • 1970-01-01
  • 2021-10-17
  • 2011-06-28
  • 1970-01-01
  • 1970-01-01
  • 2021-11-25
相关资源
最近更新 更多