Держите обещание
Принцип подстановки Лисков гласит: везде, где код ожидает протокол, каждая реализация этого протокола должна работать правильно. Реализация, которая формально подходит, но ведёт себя плохо — выбрасывает исключение, ничего не делает, обманывает, — ломает каждую функцию, которая доверилась протоколу.
Протокол — это обещание. fly() обещает полёт. Класс, который реализует fly() через выбрасывание исключения, подписал договор, который не может выполнить.
Замена, которая нарушает договор
Протокол Bird обещает fly(). Воробьи выполняют обещание. Пингвины тоже птицы — поэтому кажется естественным реализовать протокол... с raise, ведь пингвины не умеют летать.
Теперь air_show, которая работает с каждой честной птицей, падает на пингвине. Ошибка не в air_show — она в замене, которая нарушила обещание.
Пингвин, который срывает шоу
air_show доверяет протоколу и вызывает fly() у каждой птицы. Воробей выступает; пингвин выбрасывает исключение и срывает всё шоу (try/except лишь смягчает падение, чтобы показать сообщение).
Вывод: полная программа на Python
from typing import Protocol
class Bird(Protocol):
def fly(self) -> str: ...
class Sparrow:
def fly(self) -> str:
return "воробей: летит низко"
# Penguin подписывает договор — но не может его выполнить.
class Penguin:
def fly(self) -> str:
raise RuntimeError("пингвины не умеют летать")
def air_show(birds: list[Bird]) -> None:
for b in birds:
print(b.fly())
try:
air_show([Sparrow(), Penguin()])
except RuntimeError as err:
print("шоу сорвалось:", err)
Разделите обещания
Решение: обещайте только то, что правда. Разделите толстое обещание на Flyer и Swimmer. Воробей реализует Flyer, пингвин реализует Swimmer — и ни один класс не подписывает договор, который не может выполнить.
Самое приятное — кто теперь замечает ошибку: инструмент проверки типов (например, mypy) сообщает об air_show([Penguin()]) ещё до запуска кода, потому что у пингвина нет метода fly(), — обычный python3 упал бы только в момент вызова fly().
Каждый класс держит своё обещание
air_show принимает Flyer, pool_show принимает Swimmer. Каждое животное появляется только там, где его обещание выполняется, — подстановка безопасна по самому устройству кода.
Вывод: полная программа на Python
from typing import Protocol
class Flyer(Protocol):
def fly(self) -> str: ...
class Swimmer(Protocol):
def swim(self) -> str: ...
class Sparrow:
def fly(self) -> str:
return "воробей: летит низко"
class Penguin:
def swim(self) -> str:
return "пингвин: ныряет глубоко"
def air_show(flyers: list[Flyer]) -> None:
for f in flyers:
print(f.fly())
def pool_show(swimmers: list[Swimmer]) -> None:
for s in swimmers:
print(s.swim())
air_show([Sparrow()])
pool_show([Penguin()])
# air_show([Penguin()]) — инструмент проверки типов (например, mypy) сообщает об этом
# до запуска кода; обычный python3 падает только при вызове fly().
Любой Flyer может присоединиться
Выгода честных протоколов: новый Eagle присоединяется к авиашоу без всякого риска — всё, что реализует Flyer, действительно летает, поэтому air_show никогда не нужно сомневаться во входных данных.
Вывод: полная программа на Python
from typing import Protocol
class Flyer(Protocol):
def fly(self) -> str: ...
class Sparrow:
def fly(self) -> str:
return "воробей: летит низко"
# Eagle — новый класс, и он действительно летает, поэтому подставляется безопасно.
class Eagle:
def fly(self) -> str:
return "орёл: парит высоко"
def air_show(flyers: list[Flyer]) -> None:
for f in flyers:
print(f.fly())
air_show([Sparrow(), Eagle()])