Держите обещание
Принцип подстановки Лисков гласит: везде, где код ожидает интерфейс, каждый класс, реализующий его, должен работать корректно. Реализация, которая формально подходит, но ведёт себя плохо — бросает исключение, ничего не делает, обманывает, — ломает каждую функцию, которая доверилась интерфейсу.
Интерфейс — это обещание. fly() обещает полёт. Класс, который реализует fly() через выброс исключения, подписал контракт, который не может выполнить.
Замена, которая нарушает договор
Интерфейс Bird обещает fly(). Воробьи обещание выполняют. Пингвины тоже птицы — поэтому кажется естественным реализовать интерфейс... через throw, ведь пингвины не умеют летать.
Теперь airShow, которая работает с любой честной птицей, падает на пингвине. Ошибка не в airShow — она в замене, которая нарушила обещание.
Пингвин, который срывает шоу
airShow доверяет интерфейсу и вызывает fly() у каждой птицы. Воробей выступает; пингвин бросает исключение и обрушивает всё шоу (try/catch лишь смягчает падение, чтобы показать сообщение).
Вывод: PHP-скрипт
<?php
interface Bird {
public function fly(): string;
}
class Sparrow implements Bird {
public function fly(): string {
return "sparrow: flying low";
}
}
// Penguin подписывает контракт — но не может его выполнить.
class Penguin implements Bird {
public function fly(): string {
throw new RuntimeException("пингвины не умеют летать");
}
}
function airShow(Bird ...$birds): void {
foreach ($birds as $b) {
echo $b->fly(), "\n";
}
}
try {
airShow(new Sparrow(), new Penguin());
} catch (RuntimeException $e) {
echo "шоу сорвалось: ", $e->getMessage(), "\n";
}
Разделите обещания
Решение: обещать только то, что верно. Разделите раздутое обещание на Flyer и Swimmer. Воробей реализует Flyer, пингвин реализует Swimmer — и ни один класс не подписывает контракт, который не может выполнить.
Самое приятное — кто теперь ловит ошибку: airShow объявляет тип Flyer, поэтому airShow(new Penguin()) не проходит проверку типов — PHP останавливает вызов с TypeError прежде, чем выступит хоть одна птица, а статический анализатор сообщит об этой строке ещё до запуска кода.
Каждый класс держит своё обещание
airShow принимает Flyer, poolShow принимает Swimmer. Каждое животное появляется только там, где его обещание выполняется, — подстановка безопасна по построению.
Вывод: PHP-скрипт
<?php
interface Flyer {
public function fly(): string;
}
interface Swimmer {
public function swim(): string;
}
class Sparrow implements Flyer {
public function fly(): string {
return "sparrow: flying low";
}
}
class Penguin implements Swimmer {
public function swim(): string {
return "penguin: diving deep";
}
}
function airShow(Flyer ...$flyers): void {
foreach ($flyers as $f) {
echo $f->fly(), "\n";
}
}
function poolShow(Swimmer ...$swimmers): void {
foreach ($swimmers as $s) {
echo $s->swim(), "\n";
}
}
airShow(new Sparrow());
poolShow(new Penguin());
// airShow(new Penguin()) не прошёл бы проверку типов и вызвал бы TypeError.
Любой Flyer может присоединиться
Выгода честных интерфейсов: новый класс Eagle присоединяется к воздушному шоу без риска — всё, что реализует Flyer, действительно летает, поэтому airShow никогда не нужно сомневаться в своих входных данных.
Вывод: PHP-скрипт
<?php
interface Flyer {
public function fly(): string;
}
class Sparrow implements Flyer {
public function fly(): string {
return "sparrow: flying low";
}
}
// Eagle — новый класс, и он действительно летает, поэтому подставляется безопасно.
class Eagle implements Flyer {
public function fly(): string {
return "eagle: soaring high";
}
}
function airShow(Flyer ...$flyers): void {
foreach ($flyers as $f) {
echo $f->fly(), "\n";
}
}
airShow(new Sparrow(), new Eagle());