Подклассы должны дополнять, а не замещать поведение базового класса.
Стремитесь создавать подклассы таким образом, чтобы их объекты можно было бы подставлять вместо объектов базового класса, не ломая при этом функциональности клиентского кода.
Принцип подстановки — это ряд проверок, которые помогают предсказать, останется ли подкласс совместим с остальным кодом программы, который до этого успешно работал, используя объекты базового класса. Это особенно важно при разработке библиотек и фреймворков, когда ваши классы используются другими людьми, и вы не можете повлиять на чужой клиентский код, даже если бы хотели.
В отличие от других принципов, которые определены очень свободно и имеют массу трактовок, принцип подстановки имеет ряд формальных требований к подклассам, а точнее к переопределённым в них методах.
Типы параметров метода подкласса должны совпадать или быть боле абстрактными, чем типы параметров базового метода. Довольно запутанно? Рассмотрим, как это работает на примере.
feed(Cat c), который умеет кормить домашних котов. Клиентский код это знает и всегда передаёт в метод кота.feed(Animal c). Если подставить этот подкласс в клиентский код, то ничего страшного не произойдёт. Клиентский код подаст в метод кота, но метод умеет кормить всех животных, поэтому накормит и кота.feed(BengalCat t). Что будет с клиентским кодом? Он всё так же подаст в метод обычного кота. Но метод умеет кормить только бенгалов, поэтому не сможет отработать, сломав клиентский код.Тип возвращаемого значения метода подкласса должен совпадать или быть подтипом возвращаемого значения базового метода. Здесь всё то же, что и в предыдущем пункте, но наоборот.
buyCat(): Cat. Клиентский код ожидает на выходе любого домашнего кота.buyCat(): BengalCat. Клиентский код получит бенгальского кота, который является домашним котом, поэтому всё будет хорошо.buyCat(): Animal. Клиентский код сломается, так как это непонятное животное (возможно, крокодил) не поместится в ящике-переноске для кота.Ещё один анти-пример, из мира языков с динамической типизацией: базовый метод возвращает строку, а переопределённый метод — число.
Метод не должен выбрасывать исключения, которые не свойственны базовому методу. Типы исключений в переопределённом методе должны совпадать или быть подтипами исключений, которые выбрасывает базовый метод. Блоки try-catch в клиентском коде нацелены на конкретные типы исключений, выбрасываемые базовым методом. Поэтому неожиданное исключение, выброшенное подклассом, может проскочить сквозь обработчики клиентского кода и обрушить программу.
В большинстве современных языков программирования, особенно строго типизированных (Java, C# и другие), перечисленные ограничения встроены прямо в компилятор. Поэтому вы попросту не сможете собрать программу, нарушив их.
Метод не должен ужесточать _пред_условия. Например, базовый метод работает с параметром типа int. Если подкласс требует, чтобы значение этого параметра к тому же было больше нуля, то это ужесточает предусловия. Клиентский код, который до этого отлично работал, подавая в метод негативные числа, теперь сломается при работе с объектом подкласса.
Метод не должен ослаблять _пост_условия. Например, базовый метод требует, чтобы по завершению метода все подключения к базе данных были бы закрыты, а подкласс оставляет эти подключения открытыми, чтобы потом повторно использовать. Но клиентский код базового класса ничего об этом не знает. Он может завершить программу сразу после вызова метода, оставив запущенные процессы-призраки в системе.
Инварианты класса должны остаться без изменений. Инвариант — это набор условий, при которых объект имеет смысл. Например, инвариант кота — это наличие четырёх лап, хвоста, способность мурчать и прочее. Инвариант может быть описан не только явным контрактом или проверками в методах класса, но и косвенно, например, юнит-тестами или клиентским кодом.
Этот пункт проще всего нарушить при наследовании, так как вы можете попросту не подозревать о каком-то из условий инварианта сложного класса. Идеальным в этом отношении был бы подкласс, который только вводит новые методы и поля, не прикасаясь к полям базового класса.
Подкласс не должен изменять значения приватных полей базового класса. Этот пункт звучит странно, но в некоторых языках доступ к приватным полям можно получить через механизм рефлексии. В некоторых других языках (Python, JavaScript) и вовсе нет жёсткой защиты приватных полей.
Чтобы закрыть тему принципа подстановки, давайте рассмотрим пример неудачной иерархии классов документов.

ДО: подкласс «обнуляет» работу базового метода.
Метод сохранения в подклассе ReadOnlyDocuments выбросит исключение, если кто-то попытается вызвать его метод сохранения. Базовый метод не имеет такого ограничения. Из-за этого, клиентский код вынужден проверять тип документа при сохранении всех документов.
При этом нарушается ещё и принцип открытости/закрытости, так как клиентский код начинает зависеть от конкретного класса, который нельзя заменить на другой, не внося изменений в клиентский код.

ПОСЛЕ: подкласс расширяет базовый класс новым поведением.
Проблему можно решить, если перепроектировав иерархию классов. Базовый класс сможет только открывать документы, но не сохранять их. Подкласс, который теперь будет называться WritableDocument, расширит поведение родителя, позволив сохранять документ.