Продвинутая набора Python (10 частей серии)
Первоначально опубликовано в моем блоге : https://sobolevn.me/2020/02/typed-functional-dependentency-inized
Инъекция зависимости – это противоречивая тема. Есть известные проблемы, хаки и даже целые методологии о том, как работать с DI Frameworks.
Многие люди спросили меня: как вы можете использовать много набранных функциональных концепций вместе с Традиционная объектно-ориентированная зависимость впрыск ?
И этот вопрос имеет большой смысл. Поскольку функциональное программирование – это все о композиции. И впрыск зависимости просто магия. Вы получаете почти случайные объекты, введенные в некоторые места вашего кода. Более того, весь контейнер, который нужно вводить позже, волшебно собран из отдельных частей во время выполнения.
Это приводит к серьезному противоречию между функциональным декларативным стилем и «OMG, где этот класс пришел?»
Сегодня мы собираемся решить эту проблему с хорошим функциональным способом.
Регулярные функции
Представьте, что у вас есть Джанго
Игра на базе, где вы награждаете пользователей с точками для каждого угаданного письма в словом (безудержные буквы помечены как ».
):
from django.http import HttpRequest, HttpResponse from words_app.logic import calculate_points def view(request: HttpRequest) -> HttpResponse: user_word: str = request.POST['word'] # just an example points = calculate_points(user_word) ... # later you show the result to user somehow
И вот ваша деловая логика:
# Somewhere in your `words_app/logic.py`: def calculate_points(word: str) -> int: guessed_letters_count = len([letter for letter in word if letter != '.']) return _award_points_for_letters(guessed_letters_count) def _award_points_for_letters(guessed: int) -> int: return 0 if guessed < 5 else guessed # minimum 6 points possible!
Это довольно простое приложение: пользователи пытаются угадать какое-то слово и теммем, мы награждаем очки за каждое угаданное письмо. Но у нас есть порог. Мы награждаем 6 или более очков за 6 или более угаданных букв. Вот и все. У нас есть наш структурный слой на месте и наша красивая чистая логика. Этот код действительно прост для чтения и изменения.
Либо это? Давайте попробуем изменять его, чтобы узнать. Допустим, мы хотим сделать нашу игру более сложной, сделав порог точек настраиваемого. Как мы можем сделать это?
Наивная попытка
Хорошо, давайте сделаем _award_points_for_letters
Примите второй аргумент порог: int
:
def _award_points_for_letters(guessed: int, threshold: int) -> int: return 0 if guessed < threshold else guessed
И теперь ваш код не будет ввести-проверять. Потому что так выглядит наш абонент:
def calculate_points(word: str) -> int: # ... return _award_points_for_letters(guessed_letters_count)
Исправить это Расчет_Points
Функция (и все другие функции верхних звонящих) придется принять порог: int
как параметр и передать его на _award_points_for_letters.
, вот так:
def calculate_points(word: str, threshold: int) -> int: # ... return _award_points_for_letters(guessed_letters_count, threshold)
Это также влияет на наши Просмотр .py
:
from django.conf import settings from django.http import HttpRequest, HttpResponse from words_app.logic import calculate_points def view(request: HttpRequest) -> HttpResponse: user_word: str = request.POST['word'] # just an example points = calculate_points(user_word, settings.WORD_THRESHOLD) ... # later you show the result to user somehow
Как мы видим, это выполнимо. Но требуется изменить весь стек вызова для одного параметра. Что приводит нас к некоторым очевидным проблемам:
- Если наш стек вызова большой, нам придется изменить множество слоев для достижения места, где нам действительно нужна эта ценность
- Все наши функции и классы будут заражены значениями, которые мы проходим. Это будет иметь несколько эффектов: вы не сможете понять, какие функции и классы действительно нуждаются в передаваемых значениях, и которые просто работают в качестве транспортного слоя (например
Calculate_Points
в нашем примере). И, конечно, люди начнут использовать эти прошедшие значения, где они нуждаются в них. И это неявно увеличит связь вашего кода и окружающей среды. - Было бы трудно сочинить ваши функции и классы вместе. Этот дополнительный параметр всегда будет мешать. Арты ваших функций не будут совпадать.
В целом: это работает, но не масштабируемое решение. Но я бы, вероятно, сделаю это сам для небольшого приложения.
Каркасы
Многие люди Django прямо сейчас могут спросить: почему бы не просто импортировать настройки и использовать его как Джанго
Документы учит нас?
Потому что это Уродливый Действительно
На данный момент у нас есть операционная независимая бизнес-логика. Это отличное. Он устойчив к любым структуре, специфическим изменениям. И это также неясно не приносит все сложность структуры с ним. Давайте помним, как django.conf.settings
работает:
- Нам нужно установить
Django_settings_module
Env переменная для поиска Модуль настроек - Где-то глубоко внутри рамки
django.setup ()
случится Джанго
импортирую этот модуль- Этот модуль может содержать Настройка-зависимая логика
- Настройки также будут получать доступ к среде, файлам, возможно, даже поставщикам облаков для некоторых конкретных значений
- Тогда
Джанго
будет иметь почти неизмеренный объект Singleton, который вы можете импортировать из любого места в вашем коде - Вам придется отправлять настройки в своих тестах, чтобы просто пройти другое значение для вашей функции. Или несколько (и многое другое) значения, если вы сильно полагаетесь на модуль настроек (что, вероятно, будет в любом приложении)
Стоит ли оно того? Может быть. Иногда django.settings
достаточно хорош. Возможно, вы можете иметь лишь пара примитивных ценностей, которые должны быть введены. И этот путь позволяет сделать это очень быстро.
Но, я бы не рекомендовал никому идти так, если вы действительно хотите создать большое приложение с четко определенными границами и всеобъемлющей логикой домена. Я бы даже рекомендовал использовать Импорт-Линтер
запретить импортировать что-нибудь из Джанго
в вашей логике.
Итак, как в мире я собираюсь пройти эту проблему, в функцию, если ничего не работает? Параметры слишком шумные, контейнеры слишком волшебные и трудно работать, и глобальные настройки – просто чистое зло в форме синглтона?
Состав
Функциональные программисты – умные люди. Действительно. Они могут сделать буквально все с просто чистыми функциями.
Они также решили проблему зависимостей с элегантностью при сохранении простоты. Основная идея впрыска зависимости в функциональном смысле заключается в том, что мы не называем вещами, которые полагаются на контекст, у нас нет. Вместо этого мы планируем их называться позже. Давайте посмотрим, как наша оригинальная логика изменится после адаптации этой идеи:
from typing import Callable from typing_extensions import Protocol class _Deps(Protocol): # we rely on abstractions, not direct values or types WORD_THRESHOLD: int def calculate_points(word: str) -> Callable[[_Deps], int]: guessed_letters_count = len([letter for letter in word if letter != '.']) return _award_points_for_letters(guessed_letters_count) def _award_points_for_letters(guessed: int) -> Callable[[_Deps], int]: def factory(deps: _Deps): return 0 if guessed < deps.WORD_THRESHOLD else guessed return factory
Пожалуйста, обратите внимание, как мы сейчас проходим фабрика
Функция на верхний уровень нашего приложения. Кроме того, уделите дополнительное внимание на наше новое _Deps
протокол : Это позволяет нам использовать структурные подтягивания для определения необходимой API. Другими словами, все объекты с Word_Threshold
int
Атрибут пройдет чек (для обеспечения настроек Django
Настройки TypeCheck используют Django-stubs
).
Единственное, что осталось, это позвонить нашему возвращению фабрика
Функция с самого верха:
from django.conf import settings from django.http import HttpRequest, HttpResponse from words_app.logic import calculate_points def view(request: HttpRequest) -> HttpResponse: user_word: str = request.POST['word'] # just an example points = calculate_points(user_words)(settings) # passing the dependencies and calling ... # later you show the result to user somehow
Выглядит легко! Все наши требования удовлетворены:
- У нас нет магии, буквально нуль
- Все напечатано правильно
- Наша логика все еще чистая и не зависит от рамки
Единственная проблема, которую у нас сейчас есть, состоит в том, что наша логика хорошо состоит. Позвольте мне проиллюстрировать мою точку новым требованием. Во время курортного сезона наша игра может предоставить один дополнительный указать на окончательный счет случайным образом. И вот как мы можем адаптировать наш исходный код для удовлетворения новых требований:
def calculate_points(word: str) -> Callable[[_Deps], int]: guessed_letters_count = len([letter for letter in word if letter != '.']) awarded_points = _award_points_for_letters(guessed_letters_count) return _maybe_add_extra_holiday_point(awarded_points) # won't work :( def _maybe_add_extra_holiday_point(awarded_points: int) -> int: return awarded_points + 1 if random.choice([True, False]) else awarded_points
Но, упс, Награжден_Points
имеет тип Callable [[_ DEPS], INT]
Это не может быть легко составлено с этой новой функцией. Конечно, мы можем создать новую функцию внутри _maybe_add_extra_holiday_point
Только ради композиции:
def _maybe_add_extra_holiday_point(awarded_points: Callable[[_Deps], int]) -> Callable[[_Deps], int]: def factory(deps: _Deps) -> int: points = awarded_points(deps) return points + 1 if random.choice([True, False]) else points return factory
Но приятно работать? Я надеюсь, что большинство людей согласятся со мной, что это не так.
Давайте помним, что функциональные программисты являются умными людьми. Они могут сделать буквально все с просто чистыми функциями. И состав помощников. Вот почему они придумали Читатель
Монад (или, как мы называем это Додорен, температуру
Контейнер Не напугать людей к смерти на Python Land): Это композиция помощника для этой точной ситуации. Давайте обрабатываем наш код еще раз, чтобы увидеть, как это работает:
import random from typing_extensions import Protocol from returns.context import RequiresContext class _Deps(Protocol): # we rely on abstractions, not direct values or types WORD_THRESHOLD: int def calculate_points(word: str) -> RequiresContext[_Deps, int]: guessed_letters_count = len([letter for letter in word if letter != '.']) awarded_points = _award_points_for_letters(guessed_letters_count) return awarded_points.map(_maybe_add_extra_holiday_point) def _award_points_for_letters(guessed: int) -> RequiresContext[_Deps, int]: def factory(deps: _Deps): return 0 if guessed < deps.WORD_THRESHOLD else guessed return RequiresContext(factory) def _maybe_add_extra_holiday_point(awarded_points: int) -> int: return awarded_points + 1 if random.choice([True, False]) else awarded_points
Мы изменили возвратный тип функций, и мы также добавили Награжден_points.map (_maybe_add_extra_holiday_point)
. Какой еще один способ сказать «Создать Undreccontext
Контейнер с этой чистой функцией _maybe_add_extra_holiday_point
». Мы вообще не меняем наш структуру.
Как это работает?
- Когда мы называем
calculate_points (user_words)
На самом деле не начинает ничего делать, это только возвращаетПотребительницаEscontext
Контейнер, который нужно вызвать позже - Контейнер достаточно умный, чтобы понять
.map
метод. Это помнит, что после его исполнения ему нужно будет позвонить_maybe_add_extra_holiday_point.
функция - Когда мы добавляем контекст в контейнер в форме
calculate_points (user_words) (Настройки)
нашеDef Factory (депс: _deps)
Выполняет и возвращает долгожданное значение - Тогда
_maybe_add_extra_holiday_point
на самом деле выполняет и возвращает конечное значение
Вот и все. Нет беспорядок, нет магии, без внутренних средств. Но печатать, композицию и простоту.
Прозрачные зависимости
Что, если вы хотите также изменить символ, который указывает на неопределенное письмо (в настоящее время .
), быть настраиваемым? Некоторые пользователи предпочитают Отказ
, некоторые _
. Хорошо, мы можем сделать это, не можем ли мы?
Маленькая путаница может произойти с функциональными новичками на этом шаге. Потому что у нас есть только Депс
Доступно внутри _award_points_for_letters.
а не внутри calculate_points.
. И состав снова это ответ. У нас есть особый состав помощника для этого случая: Context.ask ()
что извлекает зависимости от текущего контекста и позволяет явно использовать его всякий раз, когда мы хотим:
from returns.context import Context, RequiresContext class _Deps(Protocol): # we rely on abstractions, not direct values or types WORD_THRESSHOLD: int UNGUESSED_CHAR: str # new value! def calculate_points(word: str) -> RequiresContext[_Deps, int]: def factory(deps: _Deps) -> RequiresContext[_Deps, int]: guessed_letters_count = len([ letter for letter in word if letter != deps.UNGUESSED_CHAR ]) awarded_points = _award_points_for_letters(guessed_letters_count) return awarded_points.map(_maybe_add_extra_holiday_point) return Context[_Deps].ask().bind(factory) # ...
Здесь две вещи, чтобы упомянуть здесь:
Context.ask ()
требует явно аннотироваться с_Deps
потому чтоMarpy
не может сделать вывод типа здесь.bind
Способ также является утилитой композиции. В отличие от.map
который составляет контейнер с чистой функцией,.bind
Позволяет нам составить контейнер с помощью функции, которая также возвращает контейнер того же типа
Теперь мы можем поделиться одним и тем же неизменным контекстом только для чтения для всего нашего кода.
Статическая набравка
Статическая набравка намного больше, чем случайно пытается добавить строку в целое число. Речь идет о вашей архитектуре и правилах, что каждая часть этой сложной системы имеет.
И многие читатели могут уже нашли одну ловушку, которую я оставил в этом примере. Давайте раскрым уродливую правду: _maybe_add_extra_holiday_point
не чисто. Мы не сможем проверить его без издевательства на Случайные
Отказ И потому что это нечистый, его тип – Callable [[INT], IO [INT]]
Отказ
Подожди, что это? Ио
Ваш лучший друг, когда мы говорим о хорошей архитектуре и высококачественным правилам состава. Это явный маркер, который ваша функция не чистая. Без этого типа мы свободны писать некрасивый, несущественный код, куда мы этого хотим. У нас нет никаких контрактов на уважение. Мы можем просто молиться за царствование хаоса.
А также не забывайте, что некоторые функции могут в конечном итоге с успешным исполнением и могут также потерпеть неудачу. Бросание исключений Внутри вашей бизнес-логики некрасиво. И это также удаляет явный договор от вашей архитектуры. Это варварская практика. Если что-то может пойти не так, мы должны быть готовы к этому. Вот почему многие люди используют Результат
Чтобы указать, что вещи могут (и воля!) Идите не так.
Все эти два типа сильно связаны с нашими ПотребительницаEscontext
контейнер. Потому что его тип возврата может быть сложно:
ПотребительницаEscontext [Envtepe, ReturnType]
Указывает, что мы работаем с чистой функцией, которая не может потерпеть неудачу (вроде в нашем примере безRandom
)Потребительница [Envtepe, результат [ValueType, Errortype]]
Указывает, что мы работаем с чистой функцией, которая может потерпеть неудачу (например, когда мы используем/
Математический оператор и0
в качестве разделителя или любых логических сбоев)ДолбежнаeScontext [Envtepe, IO [RETENTTYPE]]
Указывает, что мы работаем с нечистые функции, которые не могут потерпеть неудачу (как наш пример сRandom
)Потребительница Engtepe, IO [Результат [ValueType, Errortype]]]
Указывает, что мы работаем с нечистой функцией, которая может потерпеть неудачу (например, http, filesystem или вызовы базы данных)
Это не очень весело сочинять, да? Вот почему мы также отправляем полезные комбинаторы с возвращается
нравиться:
Требованectextresult
легко работать сПотребительницаEscontext
который имеетРезультат
как тип возвратаПотребительница ... легко работать с
ПотребительницаEscontextкоторый имеет
IO [Результат]как тип возврата
Таким образом Возвращает
Библиотека берет на себя все правила взлома на себя и обеспечивает хорошие API для наших конечных пользователей.
Ди контейнеров
«Итак, ты говоришь, что я не должен использовать никакие диски вообще?» Можно попросить после прочтения этой статьи. И это действительно интересный вопрос.
Я провел много времени, думая об этом очень теме сам. И мой ответ: вы можете (и, вероятно, следует) использовать контейнерную структуру впрыска зависимостей на рамочном уровне.
И вот почему: в реальных приложениях вашего _Deps
Класс с вскоре становится действительно большим. У него было бы много вещей внутри:
- Репозитории и коммуникации, связанные с базу данных
- HTTP Services и интеграции API
- Помогитеры разрешения и аутентификации
- Кэширующий слой
- Async задачи и утилиты для работы с ними
- И, вероятно, больше!
Чтобы обработать все эти вещи, вам нужно импортировать много других вещей. И вам нужно будет как-то создать этот чудовищный объект. Вот где Di Frameworks может помочь вам. Много.
С их магией создание этого контекста может быть намного проще и безопаснее. Вам нужны сложные инструменты для борьбы с реальной сложностью.
Вот почему инструменты, такие как зависимости
Требуются для каждой сложной системы: как функциональные, так и императивные.
Заключение
Давайте подведем итоги вещей, которые мы узнали сегодня:
- Различные приложения требуют разных уровней архитектуры и имеют разные требования к инъекции зависимости
- Если вы напуганы от Magic Di контейнеров, используйте напечатанную функциональную композицию:
ПотребительницаEscontext
Может помочь вам предоставить необходимый контекст с верхнего уровня к самому дну и определить приятную композицию API - При написании кода высокого уровня подумайте о вашей архитектуре и явно определите ваши контракты в качестве типов. Использовать
И.
иРезультат
указать возможный сбой и примесь - Используйте композицию помощников, когда под сомнение
- Используйте DI-контейнеры на самом верхнем уровне вашего приложения, когда сложность выходит из вашего контроля
Функциональное программирование – это весело и легко! Не стесняйтесь звезда наше репо Если вам понравились концепции выше. Или пойти в Документы чтобы узнать больше новых вещей.
Очень особенное благодаря Николай Фоминый и Артема для рассмотрения этой статьи.
Продвинутая набора Python (10 частей серии)
Оригинал: “https://dev.to/wemake-services/typed-functional-dependency-injection-in-python-4e7b”