Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Принцип подстановки Лисков JS: Погружаясь в классы

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

Рассмотрим пример. Допустим, мы решили написать свой собственный логгер (объект, который записывает в журнал произвольные сообщения).

// Определение
class Logger {
  // Код
}

// Использование
const logger = new Logger();
logger.log('debug', 'Doing work');
logger.log('info', 'Useful for debugging');

Логгер позволяет записывать сообщения с разным уровнем важности, начиная от debug и до emergency. Сигнатура метода log() устроена таким образом, что первым параметром всегда передаётся уровень сообщения, а вторым сообщение. Само сообщение - это строка произвольного формата, а уровнем может быть один из 8 вариантов.

Уровень важности позволяет менять режимы вывода. Например, в разработке выводятся все сообщения, включая отладочные, а в продакшене выводятся только серьёзные ошибки, чтобы не загрязнять журналы

Предположим, что нам это не понравилось, и мы решили изменить сигнатуру так, чтобы уровень передавался вторым параметром. Это позволит задать нам значение по умолчанию для того уровня, который чаще всего встречается в приложении. Для этого создадим класс-наследник MyLogger.

class MyLogger extends Logger {
   // Тут реализуем новую сигнатуру log
   log(message, level = 'debug') {
     return super.log(level, message)
   }
}

// Использование
logger.log('Doing work'); // По умолчанию debug
logger.log('Useful for debugging', 'info');

Что не так с этим кодом? Подобное изменение сигнатуры делает невозможным полиморфизм. Эти классы несовместимы между собой.

// Предположим, что какой-то компонент системы хочет работать с логгером Logger, но внутрь передаётся MyLogger
const logger = new MyLogger();
database.setLogger(logger);

database.doSomething();
// Внутри вызывается логгер
// logger.log('info', 'boom!');

Этот код отработает неверно (или завершится с ошибкой), так как объект database будет использовать логгер в соответствии с требованиями Logger, что противоречит тому как работает MyLogger.

В 1987 году Барбара Лисков сформулировала принцип подстановки (Liskov Substitution Principle – LSP), следование которому позволяет правильно строить иерархии типов:

Пусть q(x) является свойством, верным относительно объектов x некоторого типа T. Тогда q(y) также должно быть верным для объектов y типа S, где S является подтипом типа T.

Звучит математично. Многие разработчики пытались переформулировать это правило так, чтобы оно было интуитивно понятным. Самая простая формулировка звучит так:

Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.

В примере выше функция setLogger(logger) ожидает на вход объект, соответствующий сигнатуре методов Logger, а мы передали ей MyLogger, который не следует первоначальной сигнатуре. Согласно принципу, код должен продолжать работать как ни в чём не бывало, но этого не происходит из-за нарушения интерфейса.

JavaScript - язык с утиной типизацией, поэтому понятия типа не выражено явно. Типом, в данном случае, называют интерфейсы. Они есть в таких языках как PHP/Java/C++/TypeScript. В JavaScript тип существует на логическом уровне, в нашей голове. Считайте что тип - это сигнатура методов конкретного класса. Если у двух классов совпадают методы (по имени и сигнатуре), то они полиморфны по этому методу или набору методов.

Для любознательных. Почему вообще понадобился этот принцип? Почему бы не поручить эту работу языку? К сожалению, технически невозможно убедиться в соблюдении принципа Лисков. Поэтому его выполнение ложится на плечи разработчиков.

Правила проектирования иерархий типов

Существует несколько правил, которые надо учитывать при работе с типами:

  • Предусловия не могут быть усилены в подклассе
  • Постусловия не могут быть ослаблены в подклассе
  • Исторические ограничения

Предусловия – это ограничения на входные данные, а постусловия – на выходные. Причём в силу ограничений систем типов, многие из таких условий невозможно описать на уровне сигнатуры. Их либо придётся описывать просто текстом, либо добавлять проверки в код (проектирование по контракту).

Например, в нашем логгере предусловием является то, что метод log() первым параметром принимает один из 8 уровней сообщений. Принцип Лисков утверждает, что мы не можем создать класс, реализующий этот интерфейс (логически), который может обрабатывать меньшее число уровней. Это и называется усилением предусловий, то есть требования становятся жёстче. Вместо 8 уровней, например 5. Попытка использовать объект такого класса, закончится ошибкой, когда какая-то из систем попробует передать ему уровень, который не поддерживается. Причём не важно, приведёт это к ошибке (исключению) или логгер молча проглотит это сообщение не записав его в журнал. Главное, что поведение стало отличаться.

Встречаются ситуации, когда разработчики не видя причину такого поведения, начинают лечить следствия. В местах, где используются подобные объекты, добавляются проверки на типы. А это убивает полиморфизм.

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

И последнее, исторические ограничения. Подтипы (в случае JS - классы-наследники) не могут добавлять новые методы для изменения (мутации) данных базового типа (в случае JS-класса). Способы изменения свойств, определённых в базовом типе, определяются этим типом.


Дополнительные материалы

  1. Circle-ellipse problem

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты.

Ошибки, сложный материал, вопросы >
Нашли опечатку или неточность?

Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.

Что-то не получается или материал кажется сложным?

Загляните в раздел «Обсуждение»:

  • задайте вопрос. Вы быстрее справитесь с трудностями и прокачаете навык постановки правильных вопросов, что пригодится и в учёбе, и в работе программистом;
  • расскажите о своих впечатлениях. Если курс слишком сложный, подробный отзыв поможет нам сделать его лучше;
  • изучите вопросы других учеников и ответы на них. Это база знаний, которой можно и нужно пользоваться.
Об обучении на Хекслете

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
900
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 120 курсов, 2000+ часов теории
  • 900 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы соглашаетесь c «Политикой конфиденциальности» и «Условиями оказания услуг»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы Фронтенд-разработчик
Профессия
Разработка фронтенд-компонентов веб-приложений
1 декабря 8 месяцев
Иконка программы Node.js-разработчик
Профессия
Разработка бэкенд-компонентов веб-приложений
1 декабря 8 месяцев

Используйте Хекслет по максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы соглашаетесь c «Политикой конфиденциальности» и «Условиями оказания услуг»