Fluent Interface или текучий интерфейс — это подход проектирования объектно-ориентированных API. Он симулирует естественный язык, благодаря чему повышает читаемость кода. Этот подход может быть особенно полезным в Python, который известен своей читаемостью и простотой.
В этом уроке мы рассмотрим применение Fluent Interface для повышения читаемости и гибкости кода.
Fluent Interface
Рассмотрим пример обработки коллекций:
names = Collection(['taylor', 'abigail', None])
result = names \
# Переводим в верхний регистр
.map(lambda name: str(name).upper() if name else '') \
# Отфильтровываем пустые
.reject(lambda name: name == '')
# Выводим коллекцию на экран
print(result.all()) # => ['TAYLOR', 'ABIGAIL']
Здесь мы строим цепочку вызовов, где каждый метод возвращает объект того же типа, что и первоначальный объект, но в измененной форме. Этот подход позволяет строить цепочки вызовов произвольной длины, что и обозначается термином fluent interface.
Схематически цепочка выглядит так: collection.map(...).reject(...)
. Это схоже с тем, как работают цепочки вызовов в JavaScript, где такой подход — основной способ строить вычисления на коллекциях.
Fluent Interface упрощает чтение и обработку данных. Для его реализации в Python существуют разные подходы. Начнем с самого простого — использования self
.
self
Первый способ создания Fluent Interface основан на возврате self
из методов, которые участвуют в построении цепочек. self
— ссылка на тот объект, в контексте которого вызывается метод, поэтому его можно возвращать как обычное значение:
class Collection:
def __init__(self, coll):
self.coll = coll
def map(self, fn):
self.coll = list(map(fn, self.coll))
return self
def filter(self, fn):
self.coll = list(filter(fn, self.coll))
return self
# Возвращает саму коллекцию, а не self.
# Этот метод всегда последний в цепочке вызовов Collection.
def all(self):
return self.coll
cars = Collection([
{'model': 'rapid', 'year': 2016},
{'model': 'rio', 'year': 2013},
{'model': 'mondeo', 'year': 2011},
{'model': 'octavia', 'year': 2014}
])
cars.filter(lambda car: car['year'] > 2013).map(lambda car: car['model'])
print(cars.all()) # ['rapid', 'octavia']
У этого способа есть один недостаток — объект изменяется. Это значит, что нельзя взять и просто переиспользовать объект-коллекцию для разных выборок, потому что они начнут накладываться друг на друга.
На практике часто используется другой подход, с которым мы уже познакомились в прошлом курсе. Нужно добавить немного функциональности в ООП — возвращать не self
, а создавать новый объект того же типа с обновленной коллекцией:
class Collection:
def __init__(self, coll):
self.coll = coll
def map(self, fn):
return Collection(list(map(fn, self.coll)))
def filter(self, fn):
return Collection(list(filter(fn, self.coll)))
# Возвращает саму коллекцию, а не self.
# Этот метод всегда последний в цепочке вызовов Collection.
def all(self):
return self.coll
cars = Collection([
{'model': 'rapid', 'year': 2016},
{'model': 'rio', 'year': 2013},
{'model': 'mondeo', 'year': 2011},
{'model': 'octavia', 'year': 2014}
])
filtered_сars = cars.filter(lambda car: car['year'] > 2013)
mapped_сars = filtered_сars.map(lambda car: car['model'])
print(mapped_сars.all()) # ['rapid', 'octavia']
print(cars.all())
# [
# {'model': 'rapid', 'year': 2016},
# {'model': 'rio', 'year': 2013},
# {'model': 'mondeo', 'year': 2011},
# {'model': 'octavia', 'year': 2014}
# ]
Теперь каждый вызов возвращает новый объект. Такой код значительно безопаснее в использовании и позволяет без проблем переиспользовать новые коллекции. Изменение одной не приведет к автоматическому изменению всех остальных.
Теперь углубимся и рассмотрим более продвинутый и безопасный способ. Этот метод позволяет создавать новые объекты и сохранять исходные данные без изменений.
self.class
В каждом методе, который участвует в создании текучего интерфейса, последняя строчка всегда содержит один и тот же вызов: Collection(coll)
. Ее можно записать проще, не дублируя названия класса. Вместо возврата нового экземпляра класса Collection
напрямую можно воспользоваться self
.
В Python self
используется для обозначения текущего экземпляра класса. Когда вызывается self.__class__(coll)
, создается новый экземпляр текущего класса, что идентично вызову Collection(coll)
:
class Collection:
# ...
def map(self, fn):
return self.__class__(list(map(fn, self.coll)))
# ...
Этот прием обеспечивает большую гибкость при наследовании классов, так как self.__class__
всегда ссылается на класс текущего экземпляра, а не на конкретно указанный класс.
Выводы
Применение Fluent Interface может значительно улучшить читаемость и гибкость кода. Однако следует быть осторожным при выборе между изменяемым и неизменяемым вариантами, так как оба подхода имеют свои преимущества и недостатки. Важно выбирать подход в соответствии с требованиями вашего приложения.
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.