【问题标题】:Does the JavaScript event system violates the LSP?JavaScript 事件系统是否违反了 LSP?
【发布时间】:2017-04-06 01:28:25
【问题描述】:

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

通过调用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 实现者会通过加强 @987654338 的前置条件来违反 LSP @的界面。 void handleEvent(in Event evt) 合约中的任何内容都没有告诉您传递错误的 Event 子类型可能会破坏某些内容。

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

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

这不是一个真正的问题,因为侦听器不应该被自己调用,而是由注册它的EventTarget 调用并与特定类型关联。。 p>

我只是对根据理论是否违反 LSP 感兴趣。我想知道是否要避免违反(如果从理论上考虑的话)合同需要类似于以下内容(尽管它在实用主义方面可能做得比好事多):

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

【问题讨论】:

  • LSP 是关于预期对象行为的。除了通常理想的“不应该失败”之外,由程序员决定给定应用程序中的预期行为是什么。现在事件系统只是一个框架,用于将处理程序绑定到事件名称并确保它们在正确的时刻被调用。这些处理程序只是用户定义的函数。没有什么可以阻止他们以不同的方式处理基类和子类,这允许违反您决定对系统强加的任何常见行为规则,只要您愿意,就可以经常和鲁莽地违反。所以我的猜测是,答案是否定的。
  • 我不确定我是否有资格回答这个问题,所以我会发表评论。只要 Event 子类型包含/具有与 Event 相同的所有相同属性(相同类型)和相同签名和返回类型的相同方法,那么它不满足 LSP 吗?此调用anyEventListener.handleEvent(anyEvent) 由于缺少 Event 子类型的属性而失败是处理程序设计的错误,不是吗?如果它只使用 Event 类型的属性,那么无论使用哪种 sup-type 调用它都不会失败。这不就是 LSP 定义的吗?
  • @kuroineko 这是一个很好的答案。不知道为什么它只是一个评论。
  • @JoshfromQaribou 好吧,这种学术性的头发分裂并不是我真正喜欢的。我敢肯定,很多人靠着夸夸其谈 SOLID 及其许多良好实践而过着丰盛的生活。我说让他们玩得开心...
  • @kuroineko 这绝对是一个理论问题而不是实际问题;)

标签: javascript dom-events liskov-substitution-principle


【解决方案1】:

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

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

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

(2) 对于Event中的任意函数Event.func(),对应的KeyboardEvent.func()接受Event.func()的参数类型或其超类型,返回Event.Func()的类型或其子类型,并且只抛出Event.func() 抛出什么或其子类型;

(3) KeyboardEventEvent 部分(数据成员)不会被KeyboardEvent.func()Event.func() 无法发生的方式更改(历史规则 em>)。

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

至已编辑的问题:

替代原则要求子类型的行为(概念上)与其超类型在预期超类型的任何地方的行为方式相同。 因此,您的问题归结为“如果函数签名需要Event,这不是它所期望的吗?”

这个问题的答案可能会让你感到惊讶,但它是 - “不,它没有”

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

在没有强(或任何)类型的语言中,函数的签名没有或很少说明预期的参数类型。然而,他们仍然希望这些论点仅限于一些隐含的契约。例如,这就是 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 对象(而不是它的某些特定子类型),但可能不会使用特定的侦听器?如果两者的答案都不是 - 我们很好。

【讨论】:

  • 完全正确 - KeyboardEvent 仍然是 Event。当然,EventListener 可以在任何Event 上调度,但它总是可以根据其类型选择不同的处理方式。 LSP 只要求接口兼容。处理程序检查事件是否具有KeyboardEvent 的属性是完全可以接受的,如果没有,则什么也不做。
  • 更新了问题。
  • 您没有涉及的唯一方面是我们是否应该修改合同以明确表明如果没有给出预期的事件类型,侦听器可能会失败?例如。 bool canHandle(Event)
【解决方案2】:

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

简单地说,LSP 施加了以下约束“程序中的对象应该可以用它们的子类型的实例替换而不改变该程序的正确性”

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

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

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

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

*我在这里使用“强类型”这个词的含义非常松散!

【讨论】:

  • 您的意思是“实际上handleEvent 实现将访问sub-type 方法...”?
  • 更新了问题。
【解决方案3】:

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

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

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

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

响应编辑

我的回答已经谈到了 JavaScript,特别是事件系统如何在违反 Liskov 替换原则时眨眼。但是,我认为您提出的解决方案实际上没有任何价值。如果handleEvent 返回false,调用handleEvent 的系统会有什么不同?

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

【讨论】:

  • 更新了问题。
猜你喜欢
  • 2017-03-17
  • 2013-06-15
  • 1970-01-01
  • 2018-10-06
  • 1970-01-01
  • 1970-01-01
  • 2017-02-11
  • 1970-01-01
相关资源
最近更新 更多