0 added
0 removed
Original
2026-01-01
Modified
2026-02-26
1
<p>Переопределение методов на техническом уровне ничем не ограничено. Класс-наследник может изменить поведение любого метода настолько, насколько это вообще возможно. С одной стороны, может показаться что это здорово, так как открывается большая свобода действий, но с другой, некоторые изменения могут повлечь за собой серьезные архитектурные проблемы. Самая главная из них - сломанный полиморфизм.</p>
1
<p>Переопределение методов на техническом уровне ничем не ограничено. Класс-наследник может изменить поведение любого метода настолько, насколько это вообще возможно. С одной стороны, может показаться что это здорово, так как открывается большая свобода действий, но с другой, некоторые изменения могут повлечь за собой серьезные архитектурные проблемы. Самая главная из них - сломанный полиморфизм.</p>
2
<p>Рассмотрим пример. Допустим, мы решили написать свой собственный логгер (объект, который записывает в журнал произвольные сообщения).</p>
2
<p>Рассмотрим пример. Допустим, мы решили написать свой собственный логгер (объект, который записывает в журнал произвольные сообщения).</p>
3
<p>Логгер позволяет записывать сообщения с разным уровнем важности, начиная от<em>debug</em>и до<em>emergency</em>. Сигнатура метода log() устроена таким образом, что первым параметром всегда передается уровень сообщения, а вторым сообщение. Само сообщение - это строка произвольного формата, а уровнем может быть один из 8 вариантов.</p>
3
<p>Логгер позволяет записывать сообщения с разным уровнем важности, начиная от<em>debug</em>и до<em>emergency</em>. Сигнатура метода log() устроена таким образом, что первым параметром всегда передается уровень сообщения, а вторым сообщение. Само сообщение - это строка произвольного формата, а уровнем может быть один из 8 вариантов.</p>
4
<p><em>Уровень важности позволяет менять режимы вывода. Например, в разработке выводятся все сообщения, включая отладочные, а в продакшене выводятся только серьезные ошибки, чтобы не загрязнять журналы</em></p>
4
<p><em>Уровень важности позволяет менять режимы вывода. Например, в разработке выводятся все сообщения, включая отладочные, а в продакшене выводятся только серьезные ошибки, чтобы не загрязнять журналы</em></p>
5
<p>Предположим, что нам это не понравилось, и мы решили изменить сигнатуру так, чтобы уровень передавался вторым параметром. Это позволит задать нам значение по умолчанию для того уровня, который чаще всего встречается в приложении. Для этого создадим класс-наследник<em>MyLogger</em>.</p>
5
<p>Предположим, что нам это не понравилось, и мы решили изменить сигнатуру так, чтобы уровень передавался вторым параметром. Это позволит задать нам значение по умолчанию для того уровня, который чаще всего встречается в приложении. Для этого создадим класс-наследник<em>MyLogger</em>.</p>
6
<p>Что не так с этим кодом? Подобное изменение сигнатуры делает невозможным полиморфизм. Эти классы несовместимы между собой.</p>
6
<p>Что не так с этим кодом? Подобное изменение сигнатуры делает невозможным полиморфизм. Эти классы несовместимы между собой.</p>
7
<p>Этот код отработает неверно (или завершится с ошибкой), так как объект database будет использовать логгер в соответствии с требованиями Logger, что противоречит тому как работает MyLogger.</p>
7
<p>Этот код отработает неверно (или завершится с ошибкой), так как объект database будет использовать логгер в соответствии с требованиями Logger, что противоречит тому как работает MyLogger.</p>
8
<p>В 1987 году Барбара Лисков сформулировала принцип подстановки (Liskov Substitution Principle - LSP), который позволяет правильно строить иерархии типов, если ему следовать:</p>
8
<p>В 1987 году Барбара Лисков сформулировала принцип подстановки (Liskov Substitution Principle - LSP), который позволяет правильно строить иерархии типов, если ему следовать:</p>
9
<blockquote><p>Пусть q(x) является свойством, верным относительно объектов x некоторого типа T. Тогда q(y) также должно быть верным для объектов y типа S, где S является подтипом типа T.</p>
9
<blockquote><p>Пусть q(x) является свойством, верным относительно объектов x некоторого типа T. Тогда q(y) также должно быть верным для объектов y типа S, где S является подтипом типа T.</p>
10
</blockquote><p>Звучит математично. Многие разработчики пытались переформулировать это правило так, чтобы оно было интуитивно понятным. Самая простая формулировка звучит так:</p>
10
</blockquote><p>Звучит математично. Многие разработчики пытались переформулировать это правило так, чтобы оно было интуитивно понятным. Самая простая формулировка звучит так:</p>
11
<blockquote><p>Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.</p>
11
<blockquote><p>Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.</p>
12
</blockquote><p>В примере выше функция setLogger(logger) ожидает на вход объект, соответствующий сигнатуре методов Logger, а мы передали ей MyLogger, который не следует первоначальной сигнатуре. Согласно принципу, код должен продолжать работать как ни в чем не бывало, но этого не происходит из-за нарушения интерфейса.</p>
12
</blockquote><p>В примере выше функция setLogger(logger) ожидает на вход объект, соответствующий сигнатуре методов Logger, а мы передали ей MyLogger, который не следует первоначальной сигнатуре. Согласно принципу, код должен продолжать работать как ни в чем не бывало, но этого не происходит из-за нарушения интерфейса.</p>
13
<p><em>JavaScript - язык с утиной типизацией, поэтому понятие типа не выражено явно. Типом, в данном случае, называют интерфейсы. Они есть в таких языках как PHP/Java/C++/TypeScript. В JavaScript тип существует на логическом уровне, в нашей голове. Считайте, что тип - это сигнатура методов конкретного класса. Если у двух классов совпадают методы (по имени и сигнатуре), то они полиморфны по этому методу или набору методов.</em></p>
13
<p><em>JavaScript - язык с утиной типизацией, поэтому понятие типа не выражено явно. Типом, в данном случае, называют интерфейсы. Они есть в таких языках как PHP/Java/C++/TypeScript. В JavaScript тип существует на логическом уровне, в нашей голове. Считайте, что тип - это сигнатура методов конкретного класса. Если у двух классов совпадают методы (по имени и сигнатуре), то они полиморфны по этому методу или набору методов.</em></p>
14
<p><em>Для любознательных. Почему вообще понадобился этот принцип? Почему бы не поручить эту работу языку? К сожалению, технически невозможно убедиться в соблюдении принципа Лисков. Поэтому его выполнение ложится на плечи разработчиков.</em></p>
14
<p><em>Для любознательных. Почему вообще понадобился этот принцип? Почему бы не поручить эту работу языку? К сожалению, технически невозможно убедиться в соблюдении принципа Лисков. Поэтому его выполнение ложится на плечи разработчиков.</em></p>
15
<h2>Правила проектирования иерархий типов</h2>
15
<h2>Правила проектирования иерархий типов</h2>
16
<p>Существует несколько правил, которые надо учитывать при работе с типами:</p>
16
<p>Существует несколько правил, которые надо учитывать при работе с типами:</p>
17
<ul><li>Предусловия не могут быть усилены в подклассе</li>
17
<ul><li>Предусловия не могут быть усилены в подклассе</li>
18
<li>Постусловия не могут быть ослаблены в подклассе</li>
18
<li>Постусловия не могут быть ослаблены в подклассе</li>
19
<li>Исторические ограничения</li>
19
<li>Исторические ограничения</li>
20
</ul><p>Предусловия - это ограничения на входные данные, а постусловия - на выходные. Причем в силу ограничений систем типов, многие из таких условий невозможно описать на уровне сигнатуры. Их либо придется описывать просто текстом, либо добавлять проверки в код (<a>проектирование по контракту</a>).</p>
20
</ul><p>Предусловия - это ограничения на входные данные, а постусловия - на выходные. Причем в силу ограничений систем типов, многие из таких условий невозможно описать на уровне сигнатуры. Их либо придется описывать просто текстом, либо добавлять проверки в код (<a>проектирование по контракту</a>).</p>
21
<p>Например, в нашем логгере предусловием является то, что метод log() первым параметром принимает один из 8 уровней сообщений. Принцип Лисков утверждает, что мы не можем создать класс, реализующий этот интерфейс (логически), который может обрабатывать меньшее число уровней. Это и называется усилением предусловий, то есть требования становятся жестче. Вместо 8 уровней, например 5. Попытка использовать объект такого класса, закончится ошибкой, когда какая-то из систем попробует передать ему уровень, который не поддерживается. Причем не важно, приведет это к ошибке (исключению) или логгер молча проглотит это сообщение не записав его в журнал. Главное, что поведение стало отличаться.</p>
21
<p>Например, в нашем логгере предусловием является то, что метод log() первым параметром принимает один из 8 уровней сообщений. Принцип Лисков утверждает, что мы не можем создать класс, реализующий этот интерфейс (логически), который может обрабатывать меньшее число уровней. Это и называется усилением предусловий, то есть требования становятся жестче. Вместо 8 уровней, например 5. Попытка использовать объект такого класса, закончится ошибкой, когда какая-то из систем попробует передать ему уровень, который не поддерживается. Причем не важно, приведет это к ошибке (исключению) или логгер молча проглотит это сообщение не записав его в журнал. Главное, что поведение стало отличаться.</p>
22
<p><em>Встречаются ситуации, когда разработчики не видя причину такого поведения, начинают лечить следствия. В местах, где используются подобные объекты, добавляются проверки на типы. А это убивает полиморфизм.</em></p>
22
<p><em>Встречаются ситуации, когда разработчики не видя причину такого поведения, начинают лечить следствия. В местах, где используются подобные объекты, добавляются проверки на типы. А это убивает полиморфизм.</em></p>
23
<p>С постусловиями ситуация аналогичная, но наоборот. Допустимо, если метод возвращает урезанный набор значений, так как этот набор все равно укладывается в требования виртуального интерфейса. А вот расширять возврат нельзя, так как появляются значения, которые не были предусмотрены интерфейсом. Это относится и к исключениям.</p>
23
<p>С постусловиями ситуация аналогичная, но наоборот. Допустимо, если метод возвращает урезанный набор значений, так как этот набор все равно укладывается в требования виртуального интерфейса. А вот расширять возврат нельзя, так как появляются значения, которые не были предусмотрены интерфейсом. Это относится и к исключениям.</p>
24
<p>И последнее, исторические ограничения. Подтипы (в случае JS - классы-наследники) не могут добавлять новые методы для изменения (мутации) данных базового типа (в случае JS-класса). Способы изменения свойств, определенных в базовом типе, определяются этим типом.</p>
24
<p>И последнее, исторические ограничения. Подтипы (в случае JS - классы-наследники) не могут добавлять новые методы для изменения (мутации) данных базового типа (в случае JS-класса). Способы изменения свойств, определенных в базовом типе, определяются этим типом.</p>