Влияет ли система событий JavaScript на LSP?

Я прошу об этом больше из любопытства, чем о том, что он действительно беспокоится о нем, но мне было интересно, нарушает ли система событий JavaScript Принцип подстановки Лискова (LSP) или нет.

Вызывая EventTarget.dispatchEvent, мы можем отправить Event произвольного типа, который может обрабатываться зарегистрированным EventListener.

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

Если я правильно понял LSP, это означало бы, что anyEventListener.handleEvent(anyEvent) не должен терпеть неудачу. Однако, как правило, это не так, поскольку слушатели событий часто используют свойства специализированных подтипов Event.

В типизированном языке, который не поддерживает generics, этот проект будет в основном требовать downcasting объекта Event к ожидаемому подтипу в EventListener.

С моей точки зрения, вышеупомянутый дизайн, как это можно считать нарушением LSP. Правильно ли я или просто факт предоставления type при регистрации слушателя через EventTarget.addEventListener предотвращает нарушение LSP?

EDIT:

В то время как все, кажется, сосредотачиваются на том факте, что подклассы Event не нарушают LSP, меня действительно беспокоил тот факт, что разработчики EventListener нарушили LSP, наложив предварительные условия EventListener интерфейс. Ничто в контракте void handleEvent(in Event evt) не говорит о том, что что-то может сломаться, передав неправильный подтип Event.

В строго типизированном языке с generics этот интерфейс может быть выражен как 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
}

Ответы

Ответ 1

Значение LSP очень просто: подтип не должен действовать так, чтобы нарушать его поведение супертипа. Это поведение "супертипа" основано на конструктивных определениях, но в целом это просто означает, что можно продолжать использовать этот объект, как если бы он был супертипом в любом месте проекта.

Итак, в вашем случае он должен подчиняться следующему:

(1) A KeyboardEvent может использоваться в любом месте кода, где ожидается Event;

(2) Для любой функции Event.func() в Event соответствующий KeyboardEvent.func() принимает типы аргументов Event.func() или их супертип, возвращает тип Event.func() или его подтип и выдает только то, что Event.func() броски или их подтипы;

(3) Часть Event (члены данных) KeyboardEvent не изменяется вызовом KeyboardEvent.func() способом, который не может произойти с помощью Event.func() (правило истории).

Что такое не, требуемое LSP, является любым ограничением реализации KeyboardEvent func(), если это так, концептуально, что Event.func() должно. Поэтому он может использовать функции и объекты, которые не используются Event, включая, в вашем случае, те из его собственного объекта, которые не распознаются супертипом Event.

Отредактированный вопрос:

Принцип подстановки требует, чтобы подтип действовал (концептуально) так же, как его супертип делает везде, где ожидается супертип. Поэтому ваш вопрос сводится к вопросу "Если для функции-подписи требуется Event, разве это не то, чего она ожидает?"

Ответ на это может вас удивить, но это - "Нет, это не так".

Причиной этого является неявный интерфейс (или неявный контракт, если вы предпочитаете) функции. Как вы правильно указали, существуют языки с очень сильными и сложными правилами набора текста, которые позволяют лучше определять явный интерфейс, так что он сужает фактические типы, которые могут быть использованы вообще. Тем не менее, формальный тип аргумента не всегда является полным ожидаемым контрактом.

В языках без сильной (или любой) типизации подпись функций ничего не говорит или мало о ожидаемом типе аргументов. Тем не менее, они все еще ожидают, что аргументы будут ограничены каким-то неявным контрактом. Например, это то, что делает функция python, что делают функции шаблона С++ и какие функции получают void* в C do. Тот факт, что у них нет синтаксического механизма для выражения этих требований, не изменяет того факта, что они ожидают, что аргументы подчиняются известному контракту.

Даже очень сильно типизированный язык, такой как Java или С#, не всегда может определить все требования аргумента, используя его объявленный тип. Таким образом, например, вы можете вызвать multiply(a, b) и divide(a, b), используя те же типы - целые числа, удваивает, что угодно; однако devide() ожидает другой контракт: b не должен быть 0!

Теперь, когда вы смотрите на механизм Event, вы можете понять, что не каждый Listener предназначен для обработки любых Event. Использование общих аргументов Event и Listener обусловлено языковыми ограничениями (поэтому в Java вы могли бы лучше определить формальный контракт, в Python - совсем нет, а в JS - где-то между ними). Вы должны спросить себя, что:

Есть ли место в коде, где может использоваться объект типа Event (а не какой-либо другой конкретный подтип Event, но Event), но KeyboardEvent может не отображаться? А с другой стороны - есть ли место в коде, где может использоваться объект Listener (а не какой-то конкретный подтип), но этот конкретный слушатель может не использовать? Если ответ на оба - нет - мы хороши.

Ответ 2

Нет, система событий JavaScript не нарушает принцип замещения Лискова (LSP).

Проще говоря, LSP накладывает следующее ограничение: "объекты в программе должны быть заменены экземплярами своих подтипов без изменения правильности этой программы"

В конкретном примере системы событий JavaScript интерфейс EventListener имеет подпись функции, которая ожидает тип Event. На практике это будет вызвано подтипом, например KeyboardEvent. Эти подтипы подчиняются LSP таким образом, что если вы предоставите реализацию handleEvent, которая работает с интерфейсом Event, она также будет работать (т.е. программа будет правильной), если вместо этого будет передан экземпляр KeyboardEvent.

Однако это все довольно академично, потому что на практике ваш обработчик событий обычно хочет использовать свойства или методы, которые определены в подтипе, например. KeyboardEvent.code. В "строго типизированном (*)" языке, таком как С#, вы должны отбрасывать от Event до KeyboardEvent в своей функции handleEvent. Поскольку LSP определяет поведение, ожидаемое при замене супертипа подтипом, отбрасывание от супертипа к подтипу выходит за пределы области действия, которую определяет LSP.

С помощью JavaScript вам не нужно использовать, чтобы использовать интерфейс KeyboardEvent, однако применяется базовый принцип.

Вкратце, система событий подчиняется LSP, но на практике ваша реализация handleEvent будет иметь доступ к методам супертипа, поэтому она будет находиться за пределами области, определяемой LSP.

* Я использую слова "сильно набраны" в очень свободном смысле здесь!

Ответ 3

В этом случае JavaScript и API-интерфейс браузера не нарушают Принцип замещения Лискова, но также они не прилагают усилий для его принудительного применения.

Языки, подобные Java и С#, пытаются помешать программисту нарушить LSP, требуя, чтобы значения были заданы как заданный тип или как его подтип, чтобы он мог использоваться в контексте, где требуется этот тип, Например, для передачи Square, где требуется Rectangle, Square должен был бы реализовать или расширить Rectangle. Это долгий путь к обеспечению того, что объект будет вести себя так, как ожидается, будет вести себя Rectangle. Тем не менее, программисты могут нарушать LSP, например, имея setWidth() также изменить высоту, Square, вероятно, будет вести себя так, как ожидается, что Rectangle не будет вести себя.

В более реальном примере массив в С# реализует интерфейс IList, но вызов .Add() на этом интерфейсе вызовет исключение. Поэтому массив не всегда может быть предоставлен там, где ожидается IList без изменения правильности программы. LSP нарушен.

Так как JavaScript не имеет времени для компиляции, он не прикладывает никаких усилий, чтобы разработчики не использовали какой-либо объект таким образом, чтобы он не предназначался для использования. Но на самом деле даже системы событий на более строго типизированных языках, как правило, поощряют лишь незначительное нарушение LSP, потому что аргументы события понижающего каротажа терпят неудачу, если указан неправильный тип аргумента события.

Ответ на редактирование

В моем ответе уже говорится о том, как JavaScript вообще и система событий, в частности, подмигивают нарушению Принципа замещения Лискова. Однако я не думаю, что ваше предлагаемое решение действительно имело бы ценность. Что ожидать от системы, вызывающей handleEvent, по-другому, если handleEvent возвращен false?

В этом случае система событий действует правильно, оставляя ее разработчику, чтобы решить, что делать, если "неправильный" тип события передается данному событию. В зависимости от архитектуры и потребностей своего приложения разработчик может принять решение о представлении защитных выражений, и они могут решить, должны ли эти сторожевые заявления бросать ошибки или просто возвращаться молча.