我更多地出于好奇而不是真正关心它而,但我一直想知道JavaScript事件系统是否违反了{{3}或不。
通过致电Liskov substitution principle (LSP),我们可能会发送EventTarget.dispatchEvent
任意类型的Event
,可能会由已注册的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
调用,与特定关联型
我只是对根据理论是否违反LSP感兴趣。我想知道是否要避免违规(如果理论上认为是这样)合同需要类似下面的内容(即使它在实用主义方面可能做得更糟糕):
interface EventListener {
bool handleEvent(in Event evt); //returns wheter or not the event could be handled
}
答案 0 :(得分:5)
LSP的含义非常简单:子类型不得以违反其超类型行为的方式运行。那&#34;超类型&#34;行为基于设计定义,但一般来说,它只是意味着可以继续使用该对象,就好像它是项目中任何位置的超类型一样。
因此,在您的情况下,它应遵守以下内容:
(1)KeyboardEvent
可以在代码的任何地方使用Event
;
(2)对于Event.func()
中的任何函数Event
,相应的KeyboardEvent.func()
接受Event.func()
的参数类型或其超类型,返回类型Event.Func()
或其子类型,仅抛出Event.func()
投掷或其子类型;
(3)Event
的{{1}}部分(数据成员)未被KeyboardEvent
以KeyboardEvent.func()
无法发生的方式调用Event.func()
。 历史规则)。
LSP要求什么不,对KeyboardEvent
func()
的{{1}}实施有任何限制,只要它在概念上确实Event.func()
应该。因此,它可以使用Event
未使用的函数和对象,在您的情况下,包括Event
超类型无法识别的自身对象的函数和对象。
编辑问题:
替换原则要求子类型(概念上)与其超类型在预期超类型的任何地方的行为相同。
因此,您的问题归结为问题&#34;如果函数签名需要Event
,那么它不是预期的那样?&#34;
答案可能会让你感到惊讶,但它是 - &#34;不,它不会&#34; 。
原因是该函数的隐式接口(或隐式契约,如果您愿意)。正如您正确指出的那样,有些语言具有非常强大和复杂的类型规则,可以更好地定义显式接口,从而缩小了允许使用的实际类型。然而,单独的正式论证类型并不总是完整的预期合约。
在没有强烈(或任何)打字的语言中,功能&#39;签名对预期的论证类型没有任何关系,也没有说什么。但是,他们仍然期望这些论点仅限于一些隐性契约。例如,这就是python函数所做的事情,C ++模板函数的功能,以及在C中获取void*
的函数。他们没有表达这些要求的句法机制这一事实并没有改变他们期望参数服从已知合同的事实。
即使非常强类型的语言(如Java或C#)也不能始终使用其声明的类型定义参数的所有要求。因此,例如,您可以使用相同的类型调用multiply(a, b)
和divide(a, b)
- 整数,双精度,等等;但是,devide()
期望签订不同的合同: b
不得为0!
现在查看Event
机制时,您可以了解并非每个Listener
都旨在处理任何Event
。一般Event
和Listener
参数的使用是由于语言限制(所以在Java中你可以更好地定义正式契约,在Python中 - 根本没有,在JS中 - 介于两者之间) 。你应该问自己的是:
代码中是否有位置,其中可能使用Event
类型的对象(不是Event
的其他特定子类型,但Event
本身),但是{{{可能不是吗?而另一方面 - 代码中是否有一个位置可能会使用KeyboardEvent
对象(而不是某个特定的子类型),但该特定的侦听器可能不会?如果两者的答案都是否定的 - 我们就会很好。
答案 1 :(得分:2)
不,JavaScript事件系统不违反Liskov替换原则(LSP)。
简单地说,LSP强加了以下约束“程序中的对象应该可以替换为其子类型的实例,而不会改变该程序的正确性”
在JavaScript事件系统的特定示例中,EventListener
接口具有期望Event
类型的函数签名。实际上,这将使用子类型调用,例如KeyboardEvent
。这些子类型服从LSP,如果你提供一个在handleEvent
接口上运行的Event
实现,它也可以工作(即程序是正确的),如果传递了KeyboardEvent
1}}实例代替。
然而,这完全是学术性的,因为在实践中,您的事件处理程序通常希望使用在子类型上定义的属性或方法,例如, KeyboardEvent.code
。在“强类型(*)”语言(如C#)中,您可以在Event
函数中从KeyboardEvent
转换为handleEvent
。由于LSP定义了使用子类型替换超类型时所期望的行为,因此从超类型转换为子类型超出了LSP定义的行为范围。
使用JavaScript,您无需为了使用KeyboardEvent
接口而进行强制转换,但基本的基本原则适用。
简而言之,事件系统遵循LSP,但实际上您的handleEvent
实现将访问超类型方法,因此将超出LSP定义的范围。
*
我在这里非常宽松地使用“强类型”这个词!
答案 2 :(得分:1)
在这种情况下,JavaScript和浏览器事件API不会违反 Liskov替换原则,但他们也不会努力强制执行它。
Java和C#等语言试图通过要求将值转换为给定类型或其子类型来防止程序员违反LSP,以便在需要该类型的上下文中使用它。例如,要传递Square
需要Rectangle
的地方,Square
必须实施或延长Rectangle
。这对于确保对象的行为方式与Rectangle
预期的行为方式相去甚远。但是,程序员仍然可能违反LSP - 例如,通过让setWidth()
也改变高度,Square
可能会以Rectangle
不是这样的方式行事预计会表现出来。
在一个更现实的例子中,C#中的数组实现了IList
接口,但是在该接口上调用.Add()
将引发异常。因此,在不改变程序正确性的情况下,不能总是在预期IList
的地方提供数组。违反了LSP。
由于JavaScript没有编译时键入,因此它可以不会阻止开发人员以不打算使用的方式使用任何对象。但实际上,即使是更强类型语言的事件系统也只会鼓励一点点LSP违规,因为如果提供了错误的事件参数类型,则转发事件参数将失败。
我的回答已经讨论了JavaScript如何以及特别是事件系统如何违反Liskov替换原则。但是,我认为您提出的解决方案实际上没有任何价值。如果handleEvent
返回handleEvent
,系统调用false
会有什么不同的做法?
在这种情况下,事件系统通过将其留给开发人员来确定如果&#34;错误&#34;事件类型传递给给定事件。根据应用程序的体系结构和需求,开发人员可以决定引入保护语句,他们可以决定这些保护语句是应该抛出错误还是只是静默返回。