typescript - 如何使用 TypeScript 中的命令模式获得通用推理?
问题描述
我的用例是条件引擎。每个 JS 对象都代表一种我必须评估的条件,如果不支持,则表示错误。
下面的简化代码可以根据需要在控制台中执行和登录true
。问题主要是关于打字和推理:
this.actions[name] = command;
由于我使用的泛型触发了打字稿打字错误:Type 'Command<D, Out>' is not assignable to type 'Command<CommandData, Out>'.
- 尽管给出了我们在上面注册了几行的条件名称,但客户端没有得到数据对象的类型。
直接在 TypeScript Playground 中看到。
图书馆代码
总线必须对命令名称一无所知,因为它们是由客户端动态添加的。它必须根据参数键入execute
方法的参数(通过客户端之前使用的方法推断)。data
name
add
// Used internally
interface CommandData {
[key: string]: any
}
// Externally, the same with a `name` property to identify which Command to call
interface NamedCommandData extends CommandData {
name: string
}
type Command<In extends CommandData, Out> = (data: In, bus: CommandBus<Out>) => Promise<Out>;
type CommandRegistry<In extends CommandData, Out> = Record<string, Command<In, Out>>
class CommandBus<Out> {
public actions: CommandRegistry<CommandData, Out> = {};
add<D extends CommandData, Out>(name: string, command: Command<D, Out>) {
// I need to use "@ts-ignore" here otherwise it moans:
// Type 'Command<D, Out>' is not assignable to type 'Command<CommandData, Out>'.
// Types of parameters 'data' and 'data' are incompatible.
// Type 'CommandData' is not assignable to type 'D'.
// 'CommandData' is assignable to the constraint of type 'D', but 'D' could be instantiated with a different subtype of constraint 'CommandData'.
this.actions[name] = command;
}
async execute(name: string, data: CommandData) : Promise<Out> {
return this.actions[name](data, this);
}
getNameFromData(data: NamedCommandData | any): string | null {
if (!data || !data.name) return null
if (!this.actions[data.name]) return null
return (data as NamedCommandData).name
}
}
整体结构:
Command
- 表示特定命令类型的处理程序的接口;每个都接收它的数据和总线(如果有的话,能够处理孩子)EveryCommand
- 收到EveryCommandData
这样一个布尔期望值和子命令来评估StringEqualsCommand
- 接收StringEqualsCommandData
两个字符串(data.left 和 data.right)
CommandData
是所有具体命令的输入数据扩展的简单接口。NamedCommandData
带有附加name: string
标识正确处理程序的公共版本(例如name: "StringEquals"
)。CommandBus
是客户的切入点。他们可以添加他们的命令以及执行它们。CommandRegistry
是存储命令名称和处理程序以供查找和执行的位置。
客户代码
- 定义自己的命令(道具 + 处理程序)
- 将它们注册到总线
- 执行
// Command: check equality between two strings
interface StringEqCommandData extends CommandData {
left: string
right: string
}
const StringEqCommand: Command<StringEqCommandData, boolean> = async (data) => {
return data.left === data.right
}
// Command: reduce children command executions and check equality against the expected value
interface EveryCommandData extends CommandData {
commands: CommandData[]
value: boolean
}
const EveryCommand: Command<EveryCommandData, boolean> = async (data, bus) => {
const expectedValue = data.value
const childrenCommands = data.commands
const results = await Promise.all(
childrenCommands.map((child) => {
const name = bus.getNameFromData(child)
return name ? bus.execute(name, child) : Promise.resolve(false)
})
)
for (const result of results) {
if (result !== expectedValue)
return false
}
return true
}
// Instantiate the bus and register the Commands
const bus = new CommandBus<boolean>()
bus.add(`Every`, EveryCommand)
bus.add(`StringEquals`, StringEqCommand)
// /!\ There is no typing here for the data (second arg), based on the name (first arg)
const execution = bus.execute(`Every`, {
value: true,
commands: [
{
name: `StringEquals`,
left: `abc`,
right: `abc`,
},
{
name: `StringEquals`,
left: `123`,
right: `123`,
},
]
})
execution
.then(console.log)
.catch(console.error)
解决方案
首先,我尝试使用您当前的代码并修复打字稿问题,但这很快就会变得非常混乱。当您将您的存储actions
为字符串索引记录时,您将失去特定键字符串与其特定参数之间的关联。如果actions
是一个readonly
将命令数组传递给构造函数的属性,我们可以使类泛型依赖于动作对象,这将允许我们要求string
名称必须是有效的键,this.actions
并且相应的data
值与键入此键。
但是您正在使用一个空对象实例化该类并一一添加操作,这意味着打字稿需要在每个add()
. 你让add()
return 像asserts this is this & {actions: [key in Name]: Command<D, Out> }
. 但是这整个方法不起作用,因为您的Command
类型bus: CommandBus<Out>
作为参数并期望它是通用的,Out
仅依赖于。
这是一种“回到绘图板”的情况。试图根据字符串名称推断数据所需的类型是一团糟,因此我们必须考虑命令模式是什么,不是设计用来做什么的。该Command
接口有一个execute()
不带参数的唯一方法,因为它封装了它需要的信息。我们需要在这里使用封装。
高阶函数
如果我们不想,我们不需要使用类。让我们定义一个不带参数并返回解析Command<Out>
为的函数。Promise
Out
type Command<Out> = () => Promise<Out>
但是字符串比较确实需要一些数据,所以我们使用一个高阶函数来获取数据并返回一个带有Command<boolean>
签名的函数。
interface StringEqCommandData {
left: string
right: string
}
const createStringEquals = ({left, right}: StringEqCommandData): Command<boolean> =>
async () => {
return left === right
}
(可能我会提出这两个论点left
,right
而不是一个键控对象)
我们可以对“every”命令做同样的事情。在这种情况下,我们的参数包括其他Command<boolean>
函数。
interface EveryCommandData {
commands: Command<boolean>[]
expectedValue?: boolean
}
const createEveryCommand = ({commands, expectedValue = true}: EveryCommandData): Command<boolean> =>
async () => {
const results = await Promise.all(commands.map( c => c() ) );
return results.every( r => r === expectedValue );
}
我们根本不需要CommandBus
,因为我们可以创建一个函数并自己调用它。
const execution = createEveryCommand({
commands: [
createStringEquals({
left: `abc`,
right: `abc`,
}),
createStringEquals({
left: `123`,
right: `123`,
}),
]
})();
课程
我们还可以使用传统的基于类的方法,我们将其定义Command<Out>
为一个对象,该对象的方法execute()
不带参数并返回Promise
解析为Out
.
interface Command<Out> {
execute(): Promise<Out>;
}
我们可以创建在构造函数中获取数据并通过调用创建的类new
。
class StringEquals implements Command<boolean> {
private readonly _data: StringEqCommandData;
constructor(data: StringEqCommandData) {
this._data = data;
}
public async execute() {
return this._data.left === this._data.right;
}
}
const myCommand = new StringEquals({left: "abc", right: "abc"});
myCommand.execute();
您的every
案例可以在构造函数中使用一组命令,也可以使用add()
类似CommandBus
. 它可以存储导致它失败的特定命令。也许除了execute()
我们想要求我们的命令有一个name
属性。
混合这两种方法,我们还可以使用工厂函数来创建具有execute()
函数的对象。
const createStringEquals = (data: StringEqCommandData): Command<boolean> => {
return {
execute() {
return Promise.resolve(data.left === data.right);
}
}
}
推荐阅读
- jenkins - 使用 Podman Jenkins 容器管道构建 docker 映像时,Buildah 缺少依赖项
- c++ - 我可以重新解释 constexpr 函数的参数吗?
- react-native - React-native 从 0.61.5 升级到 0.63.2:TypeError: Super expression must be null or a function
- android - 设置应用程序调味后,我在运行应用程序时收到此错误“install_failed_conflicting_provider”/
- python - pandas datetime giving wrong output
- python-3.x - Keras LSTM 输入/输出形状
- python-3.x - 在谷歌的应用引擎上构建和部署 reactjs 代码后,asyncToGenerator.js 中的 SyntaxError
- wpf - WPF中的SSRS-如何避免授予每个单独的用户访问客户端WPF应用程序中的报告的权限?
- python-3.x - 将普通 HTML 表单与 Django 表单集成
- rust - 为什么 Vec::as_slice 和数组强制的生命周期检查不同?