admin管理员组

文章数量:1333385

I'm asking this more out of curiosity rather than really being concerned with it, but I've been wondering whether or not the JavaScript event system violates the Liskov substitution principle (LSP) or not.

By calling 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);
}

If I understand the LSP correctly, it would mean that anyEventListener.handleEvent(anyEvent) shouldn't fail. However, that is usually not the case since event listeners will often use properties of specialized Event sub-types.

In a typed language that does not support generics, that design would basically require downcasting the Event object to the expected sub-type in the EventListener.

From my understanding, the above design as is could be considered a violation of the LSP. Am I correct or the simple fact of having to provide a type when registering a listener through EventTarget.addEventListener prevents the LSP violation?

EDIT:

While everyone seems to be focusing on the fact that Event subclasses aren't violating the LSP, I was actually concerned about the fact that EventListener implementors would violate the LSP by strenthening the pre-conditions of the EventListener's interface. Nothing in the void handleEvent(in Event evt) contract tells you that something may break by passing the wrong Event sub-type.

In a strongly-typed language with generics that interface could be expressed as EventListener<T extends Event> so that the implementor can make the contract explicit e.g. SomeHandler implements EventListener<SomeEvent>.

In JS there are obviously no actual interfaces, but event handlers still need to conform to the specification and there is nothing in that specification that allows a handler to tell whether or not it can handle a specific type of event.

It's not really an issue because listeners aren't expected to be invoked on their own, but rather invoked by the EventTarget on which it was registered and associated with a specific type.

I'm just interested about whether or not LSP is violated according to the theory. I wonder if to avoid the violation (if theorically considered as such) the contract would have needed to be something like the following (even though it may have done more bad than good in terms of pragmatism):

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

I'm asking this more out of curiosity rather than really being concerned with it, but I've been wondering whether or not the JavaScript event system violates the Liskov substitution principle (LSP) or not.

By calling 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);
}

If I understand the LSP correctly, it would mean that anyEventListener.handleEvent(anyEvent) shouldn't fail. However, that is usually not the case since event listeners will often use properties of specialized Event sub-types.

In a typed language that does not support generics, that design would basically require downcasting the Event object to the expected sub-type in the EventListener.

From my understanding, the above design as is could be considered a violation of the LSP. Am I correct or the simple fact of having to provide a type when registering a listener through EventTarget.addEventListener prevents the LSP violation?

EDIT:

While everyone seems to be focusing on the fact that Event subclasses aren't violating the LSP, I was actually concerned about the fact that EventListener implementors would violate the LSP by strenthening the pre-conditions of the EventListener's interface. Nothing in the void handleEvent(in Event evt) contract tells you that something may break by passing the wrong Event sub-type.

In a strongly-typed language with generics that interface could be expressed as EventListener<T extends Event> so that the implementor can make the contract explicit e.g. SomeHandler implements EventListener<SomeEvent>.

In JS there are obviously no actual interfaces, but event handlers still need to conform to the specification and there is nothing in that specification that allows a handler to tell whether or not it can handle a specific type of event.

It's not really an issue because listeners aren't expected to be invoked on their own, but rather invoked by the EventTarget on which it was registered and associated with a specific type.

I'm just interested about whether or not LSP is violated according to the theory. I wonder if to avoid the violation (if theorically considered as such) the contract would have needed to be something like the following (even though it may have done more bad than good in terms of pragmatism):

interface EventListener {
  bool handleEvent(in Event evt); //returns wheter or not the event could be handled
}
Share Improve this question edited Jun 2, 2021 at 22:05 Brian Tompsett - 汤莱恩 5,89372 gold badges61 silver badges133 bronze badges asked Apr 6, 2017 at 1:28 plalxplalx 43.7k7 gold badges76 silver badges93 bronze badges 5
  • 1 The LSP is about expected object behaviour. It's up to the programmer to decide what an expected behaviour is in a given application, beyond the usually desirable "should not fail". Now the event system is just a framework to bind handlers to event names and make sure they get called at the right moment. These handlers are just user-defined functions. Nothing prevents them from handling base classes and subclasses differently, which allows to violate whatever mon behaviour rule you decided to impose on your system as often and as recklessly as you want. So my guess is, the answer is no. – kuroi neko Commented Apr 8, 2017 at 3:49
  • I'm not sure I'm qualified to answer this, so I'll ment. As long as the Event sub-type contains/has all of the same properties (of the same types) and the same methods with the same signatures and return type as Event, then does it not satisfy LSP? This call anyEventListener.handleEvent(anyEvent) failing due to missing properties of a sub-type of Event is a fault of the design of the handler, is it not? If it were only using properties of type Event, it would never fail no matter which sup-type it was called with. Isn't that what LSP defines? – gforce301 Commented Apr 14, 2017 at 19:47
  • @kuroineko that was a good answer. Not sure why it's only a ment. – Josh from Qaribou Commented Apr 20, 2017 at 17:54
  • @JoshfromQaribou well this kind of academic hair splitting is not really my cup of tea. I'm sure a lot of people earn a fat living pontifying about SOLID and its many good practices. I say let them have their fun... – kuroi neko Commented Apr 20, 2017 at 22:40
  • @kuroineko It is definitely more of a theorical question than a practical one ;) – plalx Commented Apr 20, 2017 at 22:44
Add a ment  | 

3 Answers 3

Reset to default 6 +25

The meaning of LSP is very simple: Subtype must not act in a way that violates its supertype behavior. That "supertype" behavior is based on design definitions, but in general, it just means that one can continue using that object as if it was the supertype anywhere in the project.

So, in your case, it should obey to the following:

(1) A KeyboardEvent can be used in any place of the code where an Event is expected;

(2) For any function Event.func() in Event, the corresponding KeyboardEvent.func() accepts the types of Event.func()'s arguments or their supertype, returns the type of Event.Func() or its subtype, and throws only what Event.func() throws or their subtypes;

(3) The Event part (data members) of KeyboardEvent are not being changed by a call to KeyboardEvent.func() in a way that could not happen by Event.func() (the History Rule).

What is not required by LSP, is any restriction about the KeyboardEvent implementation of func(), as long as it does, conceptually, what Event.func() should. It can therefore use functions and objects that are not being used by Event, including, in your case, those of its own object who are not recognized by the Event supertype.

To the Edited Question:

The Substitution Principle requires that a subtype will act (conceptually) the same way its supertype does wherever the supertype is expected. Your question boils down, therefore, to the question "If the function signature requires Event, isn't that what it expects to?"

The answer to that might surprise you, but it is - "No, it does not".

The reason for that is the implicit interface (or the implicit contract, if you prefer) of the function. As you rightly pointed out, there are languages with very strong and sophisticated typing rules, that allow better definition of the explicit interface, so that it narrows down the actual types that are allowed to be used at all. Nevertheless, the formal argument type alone is not always the full expected contract.

In languages without strong (or any) typing, functions' signature says nothing, or little, about the expected argument type. However, they still expect the arguments to be restricted to some implicit contract. For example, this is what python function do, what C++ template functions do, and what functions that get void* in C do. The fact that they have no syntactic mechanism to express those requirements does not change the fact that they expect the arguments to obey a known contract.

Even very strongly typed language like Java or C# cannot always define all the requirements of an argument using its declared type. Thus, for example, you might call multiply(a, b) and divide(a, b) using the same types - integers, doubles, whatever; yet, devide() expect a different contract: b must not be 0!

When you look at the Event mechanism now, you can understand that not every Listener is designed to handle any Event. The use of general Event and Listener argument is due to the language restrictions (so in Java you could have better define the formal contract, in Python - not at all, and in JS - somewhere between those). What you should ask yourself is that:

Is there a place in the code, where an object of type Event (not some other specific subtype of Event, but Event itself) might be used, but a KeyboardEvent might not? And on the other hand - is there a place in the code where a Listener object (and not some specific subtype of it) might be used, but that specific listener might not? If the answer to both is no - we're good.

No, the JavaScript event system does not violate the Liskov Substitution Principle (LSP).

Put simply the LSP imposes the following constraint "objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program"

In the specific example of the JavaScript event system, the EventListener interface has a function signature that expects an Event type. In practice this will be invoked with a sub-type, such as KeyboardEvent. These sub-types obey the LSP such that if you provide a handleEvent implementation that operates on the Event interface, it will also work (i.e. the program will be correct), if it is passed a KeyboardEvent instance instead.

However, this is all quite academic because in practice your event handler will typically want to use properties or methods that are defined on the sub-type, e.g. KeyboardEvent.code. In a 'strongly typed (*)' language such as C#, you would cast from Event to KeyboardEvent within your handleEvent function. Because the LSP defines the behaviour expected when you replace a super-type with a sub-type, casting from super-type to sub-type is outside of the scope of the behaviour the LSP defines.

With JavaScript you don't need to cast in order to use the KeyboardEvent interface, however the basic underlying principle applies.

In brief, the event system obeys the LSP, but in practice your handleEvent implementation will access super-type methods, so will be outside of the scope of what is defined by LSP.

* I use the words 'strongly typed' in a very loose sense here!

In this case, JavaScript and the browser event API do not violate the Liskov Substitution Principle, but neither do they make an effort to enforce it.

Languages like Java and C# attempt to prevent the programmer from violating the LSP by requiring values to be cast as a given type, or as its sub-type, in order for it to be used in a context where that type is required. For example, to pass a Square where a Rectangle is required, Square would have to implement or extend Rectangle. This goes a long way towards ensuring that an object will behave the way a Rectangle is expected to behave. However, it is still possible for programmers to violate the LSP--for example, by having setWidth() also change the height, a Square will probably behave in a way that a Rectangle is not expected to behave.

In a more real-world example, an array in C# implements the IList interface, but calling .Add() on that interface will throw an exception. Therefore, an array cannot always be provided where an IList is expected without changing the correctness of a program. The LSP is violated.

Since JavaScript has no pile-time typing, it can make no effort to prevent developers from using any object in a way that it was not intended to be used. But in reality, even event systems in more strongly-typed languages tend to encourage just a little bit of LSP violation, because down-casting event arguments will fail if the wrong event argument type is provided.

Responding to the edit

My answer already talks about how JavaScript generally, and the event system in particular, wink at a violation of the Liskov Substitution Principle. However, I don't think your proposed solution would actually have any value. What would the system calling handleEvent be expected to do differently if handleEvent returned false?

In this case, the event system acts correctly by leaving it up to the developer to decide what to do if the "wrong" event type is passed to a given event. Depending on the architecture and needs of their app, the developer can decide to introduce guard statements, and they can decide whether those guard statements should throw errors or simply return silently.

本文标签: Does the JavaScript event system violates the LSPStack Overflow