Маленькие интерфейсы
Принцип разделения интерфейсов гласит: ни один класс не должен быть вынужден реализовывать методы, которые ему не нужны. Вместо одного «толстого» интерфейса с множеством методов создайте несколько маленьких — каждый класс реализует только то, что для него действительно верно.
В Python интерфейс записывается как класс-протокол, и маленькие протоколы здесь вполне естественны: сам язык работает на крошечных протоколах — любой объект с методом __next__() работает со встроенной функцией next(), и большая часть стандартной библиотеки опирается на столь же маленькие обещания.
«Толстый» интерфейс
Протокол Worker с методами work() и eat() выглядит безобидно — пока на работу не устраивается робот. Робот отлично работает, но он тоже должен реализовать eat(), причём... чем-то. Бессмысленные ответы вроде этого — верный признак «толстого» интерфейса.
Робот, которого заставляют есть
Робот реализует eat() только потому, что этого требует протокол. Обеденный перерыв выводит ответ, которого вообще не должно быть, — класс был вынужден солгать.
Вывод: полная программа на Python
from typing import Protocol
# Один «толстый» протокол — каждый работник ОБЯЗАН ещё и есть.
class Worker(Protocol):
def work(self) -> str: ...
def eat(self) -> str: ...
class Human:
def work(self) -> str:
return "человек: работает"
def eat(self) -> str:
return "человек: обедает"
class Robot:
def work(self) -> str:
return "робот: работает"
# Навязано протоколом — роботы не едят.
def eat(self) -> str:
return "робот: ОШИБКА, не может есть"
team: list[Worker] = [Human(), Robot()]
for w in team:
print(w.work())
for w in team:
print(w.eat())
Разделите интерфейс
Разделите «толстый» протокол на Worker и Eater. Человек реализует оба, робот — только Worker. Рабочая смена принимает работников, обеденный перерыв — тех, кто ест, — робота просто никогда не зовут на обед, и ни один метод не лжёт.
Каждый класс реализует только то, что для него верно
Два маленьких протокола вместо одного «толстого». У робота больше вообще нет метода eat() — а средство проверки типов (например, mypy) не пускает его на обеденный перерыв.
Вывод: полная программа на Python
from typing import Protocol
# Два маленьких протокола — каждый класс реализует только то, что для него верно.
class Worker(Protocol):
def work(self) -> str: ...
class Eater(Protocol):
def eat(self) -> str: ...
class Human:
def work(self) -> str:
return "человек: работает"
def eat(self) -> str:
return "человек: обедает"
class Robot:
def work(self) -> str:
return "робот: работает"
def work_shift(team: list[Worker]) -> None:
for w in team:
print(w.work())
def lunch_break(guests: list[Eater]) -> None:
for e in guests:
print(e.eat())
work_shift([Human(), Robot()])
lunch_break([Human()])
Маленькие части объединяются
Маленькие протоколы — это не ограничение: их можно объединять. Когда какой-то функции действительно нужны обе способности, она прямо на месте объединяет их в один протокол: Worker плюс Eater.
Так маленькие обещания и масштабируются: определяйте протоколы с одним методом, а тот, кому нужно больше, пусть соединяет их вместе.
Объединяйте, когда нужно и то, и другое
WorkingEater построен из двух маленьких протоколов. Для полного рабочего дня нужны обе способности, поэтому подходит только человек — и это сказано один раз, в системе типов.
Вывод: полная программа на Python
from typing import Protocol
class Worker(Protocol):
def work(self) -> str: ...
class Eater(Protocol):
def eat(self) -> str: ...
# Маленькие протоколы при необходимости объединяются в более крупные.
class WorkingEater(Worker, Eater, Protocol):
pass
class Human:
def work(self) -> str:
return "человек: работает"
def eat(self) -> str:
return "человек: обедает"
def full_day(we: WorkingEater) -> None:
print(we.work())
print(we.eat())
print(we.work())
full_day(Human())