JavaScript事件系统是否违反了LSP?

Does the JavaScript event system violates the LSP?

我问这个更多是出于好奇,而不是真正关心它,但我一直在想 JavaScript 事件系统是否违反了Liskov substitution principle (LSP) 与否。

通过调用 EventTarget.dispatchEvent, we may dispatch an Event of an arbitrary type that might get handled by a registered EventListener.

interface EventListener {
  void handleEvent(in Event evt);
}

如果我对 LSP 的理解正确,那就意味着 anyEventListener.handleEvent(anyEvent) 不应该失败。然而,通常情况并非如此,因为事件侦听器通常会使用专门的 Event 子类型的属性。

在不支持泛型的类型化语言中,该设计基本上需要将 Event 对象向下转换为 EventListener.

中预期的子类型

据我了解,按原样进行上述设计可以认为是违反了LSP。我是正确的还是在通过 EventTarget.addEventListener 注册侦听器时必须提供 type 的简单事实可以防止违反 LSP?

编辑:

虽然每个人似乎都在关注 Event 子类没有违反 LSP 这一事实,但我实际上担心 EventListener 实现者会通过加强 pre 来违反 LSP - EventListener 界面的条件。 void handleEvent(in Event evt) 合同中没有任何内容告诉您传递错误的 Event 子类型可能会破坏某些内容。

在具有泛型的强类型语言中,接口可以表示为 EventListener<T extends Event> 以便实现者可以明确约定,例如SomeHandler implements EventListener<SomeEvent>.

在 JS 中显然没有实际的接口,但是事件处理程序仍然需要符合规范并且该规范中没有任何内容允许处理程序判断它是否可以处理特定类型的事件。

这不是真正的问题,因为侦听器不会被自己调用,而是由 EventTarget 注册并 与特定类型相关联的 EventTarget 调用.

我只是对根据理论是否违反了 LSP 感兴趣。我想知道是否要避免违反(如果理论上如此)合同是否会需要像下面这样的东西(尽管就实用主义而言它可能做的坏事多于好处):

interface EventListener {
  bool handleEvent(in Event evt); //returns wheter or not the event could be handled
}

不,JavaScript 事件系统不违反 Liskov 替换原则 (LSP)。

简单地说,LSP 施加了以下约束 "objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program"

在JavaScript事件系统的具体例子中,EventListener接口有一个需要Event类型的函数签名。实际上,这将使用子类型调用,例如 KeyboardEvent。这些子类型服从 LSP,因此如果您提供在 Event 接口上运行的 handleEvent 实现,它也将工作(即程序将是正确的),如果它被传递 KeyboardEvent 实例代替。

然而,这一切都非常学术,因为在实践中,您的事件处理程序通常希望使用在子类型上定义的属性或方法,例如KeyboardEvent.code。在 C# 等 'strongly typed (*)' 语言中,您可以在 handleEvent 函数中从 Event 转换为 KeyboardEvent。因为 LSP 定义了用子类型替换超类型时预期的行为,所以从超类型到子类型的转换超出了 LSP 定义的行为范围。

使用 JavaScript 时,您无需强制转换即可使用 KeyboardEvent 接口,但基本的基本原则适用。

简而言之,事件系统服从 LSP,但实际上您的 handleEvent 实现将访问超类型方法,因此将超出 LSP 定义的范围。

* 我在这里使用 'strongly typed' 这个词的意思很笼统!

LSP的含义很简单:子类型的行为不得违反其超类型的行为。 "supertype" 行为基于设计定义,但一般来说,它只是意味着可以继续使用该对象,就好像它是项目中任何地方的超类型一样。

因此,在您的情况下,它应该遵守以下内容:

(1) KeyboardEvent 可以用在代码中需要 Event 的任何地方;

(2) 对于 Event 中的任何函数 Event.func(),相应的 KeyboardEvent.func() 接受 Event.func() 的参数类型或其超类型,returns Event.Func() 或其子类型的类型,并且仅抛出 Event.func() 抛出的或其子类型;

(3) KeyboardEventEvent 部分(数据成员)未通过调用 KeyboardEvent.func() 以 [=12= 无法发生的方式进行更改](历史规则)。

什么是 not LSP 所要求的,是对 func()KeyboardEvent 实现的任何限制,只要它确实如此,从概念上讲,Event.func()应该。因此,它可以使用 Event 未使用的函数和对象,包括在您的情况下,Event 超类型无法识别的它自己的对象。

已编辑的问题:

替换原则要求子类型的行为(概念上)与其超类型在预期超类型的地方相同。 因此,您的问题归结为 "If the function signature requires Event, isn't that what it expects to?"

答案可能会让您感到惊讶,但它是 - "No, it does not".

原因是函数的隐式接口(或者隐式契约,如果你愿意的话)。正如您正确指出的那样,有些语言具有非常强大和复杂的类型规则,可以更好地定义 显式接口 ,从而缩小允许使用的实际类型根本。然而,单独的正式参数类型并不总是完整的预期合同。

在没有强类型(或任何类型)的语言中,函数的签名对预期的参数类型只字不提或只字不提。然而,他们仍然希望争论仅限于一些隐含的契约。例如,这是 python 函数的作用,C++ 模板函数的作用,以及 C 中获得 void* 的函数的作用。他们没有句法机制来表达这些要求这一事实并没有改变他们期望参数遵守已知契约的事实。

即使像 Java 或 C# 这样的强类型语言也不能总是使用其声明的类型来定义参数的所有要求。因此,例如,您可以调用 multiply(a, b)divide(a, b) 使用相同的类型——整数、双精度数,等等;然而,devide() 期望不同的合同:b 不能为 0!

当您现在查看 Event 机制时,您可以理解并非每个 Listener 都旨在处理任何 Event。使用一般 EventListener 参数是由于语言限制(所以在 Java 中你可以更好地定义正式合同,在 Python 中 - 根本不是,在 JS 中 - 介于两者之间)。你应该问自己的是:

代码中是否有一个地方可以使用 Event 类型的对象(不是 Event 的某些其他特定子类型,而是 Event 本身),但是KeyboardEvent 可能不会?另一方面 - 代码中是否有一个地方可以使用 Listener 对象(而不是它的某些特定子类型),但特定的侦听器可能不会?如果两者的答案是否定的 - 我们很好。

在这种情况下,Java脚本和浏览器事件 API 没有 违反 里氏替换原则,但它们也没有努力强制执行

像 Java 和 C# 这样的语言试图通过要求将值转换为给定类型或其子类型来防止程序员违反 LSP,以便它在需要该类型的上下文。例如,要在需要 Rectangle 的地方传递 SquareSquare 必须实现或扩展 Rectangle。这对确保对象的行为方式与 Rectangle 预期的行为方式大有帮助。然而,程序员仍然有可能违反 LSP——例如,通过让 setWidth() 也改变高度,Square 可能会以 Rectangle 不会的方式表现期望表现良好。

在一个更真实的示例中,C# 中的数组实现了 IList 接口,但在该接口上调用 .Add() 将引发异常。因此,在不改变程序正确性的情况下,不能总是在预期 IList 的地方提供数组。 LSP 被违反了。

由于 JavaScript 没有编译时类型,它无法阻止开发人员以非预期的方式使用 any 对象使用。但实际上,即使是更强类型语言中的事件系统也倾向于鼓励一点点 LSP 违规,因为如果提供了错误的事件参数类型,向下转换事件参数将会失败。

正在回复编辑

我的回答已经谈到了 Java脚本通常如何在违反 Liskov 替换原则时眨眼,特别是事件系统。但是,我认为您提出的解决方案实际上没有任何价值。如果 handleEvent returned false?

,系统调用 handleEvent 会有什么不同?

在这种情况下,如果 "wrong" 事件类型传递给给定事件,事件系统将由开发人员决定如何操作,从而正确运行。根据应用程序的架构和需求,开发人员可以决定引入保护语句,他们可以决定这些保护语句是应该抛出错误还是简单地 return 静默。