Оптимизация интерфейсов Python: советы по созданию лаконичного и чистого кода

Оптимизация интерфейсов Python: советы по созданию лаконичного и чистого кода

2 марта 2023 г.

За свою карьеру я повидал много интерфейсов Python (как в API, а не в пользовательском интерфейсе). Вы можете быстро определить уродливого по его размеру. Ниже приведены несколько рецептов, как сделать аккуратный!

Интерфейсы в Python

Интерфейс – это расплывчатое понятие, которое поначалу может быть трудно понять. Это план взаимодействия с объектом (прочитав несколько ответов здесь не помешает).

Вам вообще нужны интерфейсы в Python?

Есть несколько хороших постов об этом:

Я предполагаю, что вы все это прочитали и решили использовать интерфейсы. Я думаю, что Протоколы — лучший способ создания интерфейсов в Python. Это то, что мы собираемся использовать здесь.

Здесь мы собираемся обсудить, что делает интерфейс хорошим.

Интерфейс должен быть маленьким

Вы разрабатываете симулятор зоопарка и сделали интерфейс для "обычного животного":

class Animal(Protocol):
   def current_weight(self) -> float: ...
   def price(self) -> float: ...
   def sleep(self, hours): ...
   def eat(self, food): ...
   def draw(self, context): ...
   def look_for_food(self): ...
   def hunger_level(self) -> float: ...
   def is_asleep(self) -> bool: ...
   def is_awake(self) -> bool: ...
   def current_position() -> Tuple[float, float, float]: ...
   def current_orientation() -> RotationMatrix: ...
   def current_transform3d() -> HomogeneousMatrix: ...

Вас должно насторожить, когда вы видите интерфейс с более чем 4-5 методами.

Скорее всего, он был плохо спроектирован, и проект, в котором он используется, будет слишком запутанным:

  • этот интерфейс будет использоваться в совершенно не связанных частях программного обеспечения. Это соединит их вместе.
  • будет широко использоваться иерархия классов
  • реализации интерфейса будут слишком сложными
  • это будет сложно задокументировать и запутать в использовании.

Вы можете подумать: «Очевидно, что животные делают массу разных вещей, поэтому наличие множества методов оправдано». Но вы можете придумать еще десять способов, чтобы добавить к «родовому животному», и этому никогда не будет конца. Искусство состоит в том, чтобы смоделировать сложную вещь с помощью набора небольших несвязанных интерфейсов.

Каковы рецепты уменьшения размера интерфейса?

Семантическое перекрытие

Иногда могут быть некоторые очевидные методы дублирования (например, get_weights против current_weight). Но чаще вы найдете методы, которые несколько пересекаются по семантике. Немного подумав, можно удалить некоторые из них.

В приведенном выше примере:

   def current_position() -> Tuple[float, float, float]:
       pass

   def current_orientation() -> RotationMatrix:
       pass

   def current_transform3d() -> HomogeneousMatrix:
       pass

Пользователь извлекает положение и ориентацию из HomogeneousMatrix, возвращаемого current_transform3d. Два других геттера можно заменить служебными функциями.

Аналогичная ситуация, вероятно, происходит с:

   def is_asleep(self) -> bool:
       pass

   def is_awake(self) -> bool:
       pass

Верно ли, что animal.is_asleep() == not animal.is_awake()? Если да, то вам лучше удалить один из них.

Развязка интерфейсов

После удаления дубликатов попробуйте разделить интерфейс на независимые части. Скорее всего, вы не используете все функции Animal во всех частях вашего программного обеспечения:

1. может существовать функция, обрабатывающая рисование животных, которая использует только current_transform3d и draw:

def draw_entity(animal: Animal):
    t = animal.current_transform3d()
    c = some_drawing_context(t)
    animal.draw(c)

2. может существовать алгоритм жизненного цикла животных:

def life_management(animal: Animal):
    eating_logic(
      animal.hunger_level(), animal.look_for_food(), 
      animal.eat())
    sleeping_logic(animal.is_awake(), animal.sleep())

3. и система управления зоопарком

def purchase_decision(animal: Animal, budget: float) -> bool:
   w = animal.current_weight()
   p = animal.price()
   decide_to_buy(p, w, budget)

«Обычное животное» — это набор совершенно разных интерфейсов, которые можно распутать:

class VisualEntity(Protocol):
   def current_transform3d() -> HomogeneousMatrix: ...
   def draw(self, context): ...

class BehavingAgent:
   def sleep(self, hours): ...
   def eat(self, food): ...
   def look_for_food(self): ...
   def hunger_level(self) -> float: ...
   def is_awake(self) -> bool: ...

class ZooAsset:
   def current_weight(self) -> float: ...
   def price(self) -> float: ...

Теперь вы можете увидеть преимущества множества маленьких интерфейсов перед одним большим:

* Вам не нужно отделять части вашей кодовой базы от рабочих процессов компании. Команда рендеринга не будет зависеть от разработчиков управления зоопарком. * Нет необходимости реализовывать ненужные методы для небольших вариаций «Животных». Изменение ценового алгоритма не приводит к появлению нового вида! * Вы сможете повторно использовать небольшие интерфейсы и утилиты. Вы можете обнаружить, что draw_entity и Purchase_decision работают для вашего объекта Plant.

Теперь вы можете увидеть преимущества множества маленьких интерфейсов перед одним большим.

Рекурсивная семантика

Далее вас должно насторожить, если одна функция вызывает другую в том же интерфейсе. Продолжая пример с зоопарком:

Вряд ли животное проведет сутки без еды и сна. Поэтому высока вероятность того, что eat и sleep будут вызываться из реализации lifecycle. Такой интерфейс объединяет два уровня абстракции. Это два интерфейса в одном, аналогично предыдущему разделу.

Часть «жизненного цикла» используется в некоторых глобальных контекстах приложения, но eat и sleep, вероятно, используются только локально внутри него.

Давайте разделим их на части:

Обратите внимание, что теперь lifecycle принимает ElementaryAnimal в качестве аргумента. Это ясно говорит о том, что жизненный цикл зависит от чего-то, что может есть и спать.

Слишком общий

Еще одна вещь, которой следует избегать, – слишком общий интерфейс:

Интерфейс подобен договору между двумя сторонами. Но контракт, который говорит «делай что угодно», так же хорош, как и полное отсутствие контракта. Слишком просто создавать такие интерфейсы на необязательном языке с динамической типизацией, таком как Python, особенно с его функциями аргументов с подстановочными знаками.

К сожалению, такие интерфейсы встречаются в некоторых ситуациях, но мы можем обобщить это как «передачу черного ящика».

Передача «черных ящиков» обычно хуже, чем явных компонентов. Но если вам приходится это делать, то лучше признать его «черноту», пройдя функтора и перейти к более функциональному дизайну (вот подробное руководство):

Конструкторы в интерфейсе

Дополнительная тема по теме: вы заметили, что __init__ обычно не является частью какого-либо интерфейса? Почему нет? Похоже, это просто еще один способ, которым вы можете воспользоваться.

Это восходит к C++, где конструкторы не являются виртуальными.

Бьерн Страуструп дает ответ, почему это так:

<цитата>

«Виртуальный вызов – это механизм выполнения работы с частичной информацией. Для создания объекта нужна полная информация. Следовательно, «вызов конструктора» не может быть виртуальным».

Мне больше нравятся высокоуровневые рассуждения: конструктор — это функция для создания объектов. Он принадлежит к сфере «создателей объектов». Он просто не может принадлежать уже созданному объекту: вы не должны делать объект из себя, и я не фанат клонирование).

Конструктор можно рассматривать как метод интерфейса фабрики. Другими словами, конструктор и объектный интерфейс — это еще один пример двух связанных уровней абстракции.

Заключение

Подводя итог, хороший интерфейс должен:

  • быть небольшим и лаконичным
  • избегайте семантического дублирования
  • избегайте объединения нескольких частей в одну
  • избегайте смешивания нескольких уровней абстракции
  • быть конкретным и не слишком общим
  • не включать __init__

В то же время реальная жизнь имеет множество исключений и сложностей. Но это должны быть оправданные исключения, а не отговорки, чтобы поддерживать беспорядочные интерфейсы.

Спасибо за прочтение!

Вы можете найти меня в Twitter или LinkedIn.


Первоначально опубликовано по адресу здесь.


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE