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
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With