Держи обещание
Принцип подстановки Лисков говорит: везде, где код ожидает интерфейс, каждая реализация этого интерфейса должна работать правильно. Реализация, которая формально подходит, но ведёт себя плохо — выбрасывает ошибку, ничего не делает, обманывает — ломает каждую функцию, которая доверилась интерфейсу.
Интерфейс — это обещание. fly() обещает полёт. Класс, который реализует fly() через падение с ошибкой, подписал контракт, который не может выполнить.
Замена, которая нарушает договор
Интерфейс Bird обещает fly(). Воробьи обещание выполняют. Пингвины — тоже птицы, поэтому кажется естественным реализовать интерфейс... выбросив ошибку, ведь пингвины не умеют летать.
Теперь airShow, который работает для каждой честной птицы, падает на пингвине. Ошибка не в airShow — она в замене, которая нарушила обещание.
Пингвин, который срывает шоу
airShow доверяет интерфейсу и вызывает fly() у каждой птицы. Воробей выступает; пингвин выбрасывает ошибку и прямо там останавливает шоу (try/catch лишь смягчает падение, чтобы показать сообщение).
Вывод: программа на TypeScript и захваченный вывод
interface Bird {
fly(): string
}
class Sparrow implements Bird {
fly(): string {
return "воробей: летит низко"
}
}
// Penguin подписывает контракт — но не может его выполнить.
class Penguin implements Bird {
fly(): string {
throw new Error("пингвины не умеют летать")
}
}
function airShow(birds: Bird[]): void {
for (const b of birds) {
console.log(b.fly())
}
}
try {
airShow([new Sparrow(), new Penguin()])
} catch (err) {
console.log("шоу сорвалось:", (err as Error).message)
}
Раздели обещания
Решение: обещать только то, что правда. Разделим раздутое обещание на Flyer и Swimmer. Воробей реализует Flyer, пингвин реализует Swimmer — и ни один класс не подписывает контракт, который не может выполнить.
Самое приятное — кто теперь ловит ошибку: передача пингвина в airShow больше не падение во время работы программы, а ошибка типа, о которой сообщат ещё до запуска кода.
Каждый тип держит своё обещание
airShow принимает Flyer, poolShow принимает Swimmer. Каждое животное появляется только там, где его обещание выполняется — подстановка безопасна по построению.
Вывод: программа на TypeScript и захваченный вывод
interface Flyer {
fly(): string
}
interface Swimmer {
swim(): string
}
class Sparrow implements Flyer {
fly(): string {
return "воробей: летит низко"
}
}
class Penguin implements Swimmer {
swim(): string {
return "пингвин: ныряет глубоко"
}
}
function airShow(flyers: Flyer[]): void {
for (const f of flyers) {
console.log(f.fly())
}
}
function poolShow(swimmers: Swimmer[]): void {
for (const s of swimmers) {
console.log(s.swim())
}
}
airShow([new Sparrow()])
poolShow([new Penguin()])
// airShow([new Penguin()]) даже не прошло бы проверку типов.
Любой Flyer может присоединиться
Выгода честных интерфейсов: новый класс Eagle присоединяется к авиашоу без всякого риска — всё, что реализует Flyer, действительно летает, поэтому airShow никогда не нужно сомневаться во входных данных.
Вывод: программа на TypeScript и захваченный вывод
interface Flyer {
fly(): string
}
class Sparrow implements Flyer {
fly(): string {
return "воробей: летит низко"
}
}
// Eagle — новый класс, и он действительно летает, поэтому подставляется безопасно.
class Eagle implements Flyer {
fly(): string {
return "орёл: парит высоко"
}
}
function airShow(flyers: Flyer[]): void {
for (const f of flyers) {
console.log(f.fly())
}
}
airShow([new Sparrow(), new Eagle()])