Когда речь идет о создании масштабируемых и поддерживаемых приложений, понимание таких важных понятий, как принципы чистого кода, архитектурные паттерны и SOLID практики проектирования, имеет решающее значение. Изучив эти принципы, новички получат представление о построении надежных, гибких и легко тестируемых приложений, что позволит им сохранить ясность кодовой базы и возможность ее сопровождения по мере роста их проектов.
Немного теории чистого кода
Прежде чем погрузиться в архитектуру, я хотел бы ответить на несколько часто задаваемых вопросов:
-
В чем преимущества указания типов в python?
-
Каковы причины разделения приложения на слои?
-
Каковы преимущества использования ООП?
-
Каковы недостатки использования глобальных переменных или синглтонов?
Не стесняйтесь пропустить теоретические разделы, если вы уже знаете ответы, и переходите непосредственно к разделу «Создание программы».
Всегда указывайте типы
Аннотация типов значительно улучшает код, повышая его ясность, надежность и ремонтопригодность:
-
Безопасность типов: Аннотации типов помогают выявить несоответствие типов на ранней стадии, что уменьшает количество ошибок и гарантирует, что ваш код будет вести себя так, как ожидается.
-
Самодокументированный код: Подсказки типов повышают удобочитаемость кода и выступают в роли встроенной документации, уточняя ожидаемые типы входных и выходных данных функций.
-
Повышение качества кода: Использование подсказок типов способствует улучшению дизайна и архитектуры, способствуя продуманному планированию и реализации структур данных и интерфейсов.
-
Улучшенная поддержка инструментов: Такие инструменты, как mypy, используют аннотации типов для статической проверки типов, выявляя потенциальные ошибки до начала выполнения, тем самым упрощая процесс разработки и тестирования.
-
Поддержка современных библиотек: FastAPI, Pydantic и другие библиотеки используют аннотации типов для автоматизации валидации данных, генерации документации и уменьшения дублирования кода.
-
Преимущества типизированных классов данных перед простыми структурами данных: Типизированные классы данных улучшают читаемость, работу со структурированными данными и безопасность типов по сравнению с массивами и кортежами. Они используют атрибуты вместо строковых ключей, что минимизирует ошибки из-за опечаток и улучшает автодополнение кода. Датаклассы также обеспечивают четкое определение структур данных, поддерживают значения по умолчанию, упрощают сопровождение и отладку кода.
Почему нам нужно разделить приложение на слои
Разделение приложения на слои повышает ремонтопригодность, масштабируемость и гибкость. Ключевые причины для этой стратегии включают:
Разделение забот
-
Каждый слой фокусируется на определенном аспекте, что упрощает разработку, отладку и сопровождение.
Возможность повторного использования
-
Слои можно повторно использовать в различных частях приложения или в других проектах. Исключается дублирование кода.
Масштабируемость
-
Позволяет различным слоям масштабироваться независимо друг от друга в зависимости от потребностей.
Удобство обслуживания
-
Упрощает обслуживание за счет локализации общих функций в отдельных слоях.
Улучшенная совместная работа
-
Команды могут работать над разными слоями независимо друг от друга.
Гибкость и адаптируемость
-
Изменения в технологиях или дизайне могут быть реализованы в определенных слоях. В адаптации нуждаются только затронутые слои, остальные остаются незатронутыми.
Тестируемость
-
Каждый слой можно тестировать независимо, что упрощает модульное тестирование и отладку.
Использование многоуровневой архитектуры дает значительные преимущества в скорости разработки, оперативном управлении и долгосрочном обслуживании, делая системы более надежными, управляемыми и адаптируемыми к изменениям.
Глобальные константы против инжектируемых параметров
При разработке программного обеспечения выбор между использованием глобальных констант и применением инъекции зависимостей (DI) может существенно повлиять на гибкость, сопровождаемость и масштабируемость приложений. В этом анализе рассматриваются недостатки глобальных констант и противопоставляются им преимущества, обеспечиваемые инъекцией зависимостей.
Глобальные константы
-
Фиксированная конфигурация: Глобальные константы статичны и не могут динамически адаптироваться к различным средам или требованиям без изменения кодовой базы. Такая жесткость ограничивает их использование в различных сценариях работы.
-
Ограниченный объем тестирования: Тестирование становится сложным при использовании глобальных констант, поскольку их нелегко переопределить. Разработчикам может потребоваться изменять глобальное состояние или использовать сложные обходные пути, чтобы приспособиться к различным сценариям тестирования, что повышает риск ошибок.
-
Уменьшение модульности: Опора на глобальные константы снижает модульность, поскольку компоненты становятся зависимыми от конкретных значений, установленных глобально. Такая зависимость снижает возможность повторного использования компонентов в различных проектах или контекстах.
-
Высокая связанность: Глобальные константы интегрируют специфическое поведение и конфигурации непосредственно в кодовую базу, что затрудняет адаптацию или развитие приложения без значительных изменений.
-
Скрытые зависимости: Подобно глобальным переменным, глобальные константы скрывают зависимости внутри приложения. Становится неясно, какие части системы зависят от этих констант, что усложняет понимание и сопровождение кода.
-
Трудности сопровождения и рефакторинга: Со временем использование глобальных констант может привести к проблемам с обслуживанием. Рефакторинг такой кодовой базы рискован, поскольку изменения констант могут случайно затронуть разные части приложения.
-
Дублирование состояния на уровне модуля: В Python код на уровне модуля может выполняться несколько раз, если импорт происходит по разным путям (например, абсолютный и относительный). Это может привести к дублированию глобальных экземпляров и трудноотслеживаемым ошибкам в обслуживании.
Инжектируемые параметры
-
Динамическая гибкость и настраиваемость: Инъекция зависимостей позволяет динамически настраивать компоненты, делая приложения адаптируемыми к изменяющимся условиям без необходимости изменения кода.
-
Улучшенная тестируемость: DI улучшает тестируемость, позволяя внедрять моки или альтернативные конфигурации во время тестирования, эффективно изолируя компоненты от внешних зависимостей и обеспечивая более надежные результаты тестирования.
-
Увеличение модульности и возможности повторного использования: Компоненты становятся более модульными и пригодными для повторного использования, поскольку они спроектированы так, чтобы работать с любыми инжектируемыми параметрами, соответствующими ожидаемым интерфейсам. Такое разделение задач повышает переносимость компонентов в различные части приложения или даже в разные проекты.
-
Низкая связанность: Инжектируемые параметры способствуют низкой связанности, отделяя логику системы от ее конфигурации. Такой подход облегчает обновление и внесение изменений в приложение.
-
Явное декларирование зависимостей: В DI компоненты явно объявляют о своих зависимостях, обычно через параметры конструктора или сеттеры. Такая ясность облегчает понимание, поддержку и расширение системы.
-
Масштабируемость и управление сложностью: По мере роста приложений DI помогает управлять сложностью, локализуя проблемы и отделяя конфигурацию от использования, что способствует эффективному масштабированию и обслуживанию больших систем.
Процедурное программирование против ООП
Использование объектно-ориентированного программирования (ООП) и инъекции зависимостей (DI) может значительно повысить качество и сопровождаемость кода по сравнению с процедурным подходом с глобальными переменными и функциями. Вот простое сравнение, демонстрирующее эти преимущества:
Процедурный подход: Глобальные переменные и функции
# Global configuration database_config = { 'host': 'localhost', 'port': 3306, 'user': 'user', 'password': 'pass' } def connect_to_database(): print(f"Connecting to database on {database_config['host']}...") # Assume connection is made return "database_connection" def fetch_user(database_connection, user_id): print(f"Fetching user {user_id} using {database_connection}") # Fetch user logic return {'id': user_id, 'name': 'John Doe'} # Usage db_connection = connect_to_database() user = fetch_user(db_connection, 1)
-
Дублирование кода:
database_configдолжен передаваться или обращаться глобально в нескольких функциях. -
Трудности тестирования: Имитация подключения к базе данных или конфигурации предполагает манипулирование глобальным состоянием, что чревато ошибками.
-
Высокая связанность: Функции напрямую зависят от глобального состояния и конкретных реализаций.
ООП + DI-подход
from typing import Dict, Optional from abc import ABC, abstractmethod class DatabaseConnection(ABC): @abstractmethod def connect(self): pass @abstractmethod def fetch_user(self, user_id: int) -> Dict: pass class MySQLConnection(DatabaseConnection): def __init__(self, config: Dict[str, str]): self.config = config def connect(self): print(f"Connecting to MySQL database on {self.config['host']}...") # Assume connection is made def fetch_user(self, user_id: int) -> Dict: print(f"Fetching user {user_id} from MySQL") return {'id': user_id, 'name': 'John Doe'} class UserService: def __init__(self, db_connection: DatabaseConnection): self.db_connection = db_connection def get_user(self, user_id: int) -> Dict: return self.db_connection.fetch_user(user_id) # Configuration and DI config = { 'host': 'localhost', 'port': 3306, 'user': 'user', 'password': 'pass' } db = MySQLConnection(config) db.connect() user_service = UserService(db) user = user_service.get_user(1)
-
Уменьшено дублирование кода: Конфигурация базы данных инкапсулируется в объекте подключения.
-
Возможности DI: Легко заменить
MySQLConnectionна другой класс подключения к базе данных, напримерPostgresConnection, не изменяя кодUserService. -
Энкапсуляция и абстракция: Детали реализации того, как извлекаются пользователи или как подключается база данных, скрыты от глаз.
-
Удобство моков и тестирования:
UserServiceможно легко протестировать, внедрив заглушкуDatabaseConnection. -
Управление временем жизни объекта: Жизненным циклом соединений с базой данных можно управлять более детально (например, с помощью менеджеров контекста).
-
Использование принципов ООП: Демонстрирует наследование (абстрактный базовый класс), полиморфизм (реализация абстрактных методов) и протоколы (интерфейсы, определенные
DatabaseConnection).
Благодаря структурированию приложения с использованием ООП и DI, код становится более модульным, его легче тестировать, и он становится гибким к изменениям, таким как замена зависимостей или изменение конфигурации.
Создание программы
Все примеры и более подробную информацию с комментариями вы можете найти в репозитории
Начало нового проекта
Небольшой чек-лист:
1. Управление проектами и зависимостями с помощью Poetry
poetry new python-app-architecture-demo
Эта команда создаст минимальную структуру директории: отдельные папки для приложения и тестов, файл метаинформации проекта pyproject.toml, лок файлы зависимостей и конфигурации гита.
2. Контроль версий с помощью Git
Инициализируйте гит:
git init
Добавьте файл .gitignore для исключения ненужных файлов из вашего репозитория. Используйте стандартный .gitignore, предоставленный GitHub, и добавьте остальные исключения, такие как .DS_Store для macOS и папки редакторов (.idea, .vscode, .zed, etc):
wget -O .gitignore https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore
echo .DS_Store >> .gitignore
3. Управление зависимостями
Установите зависимости вашего проекта с помощью poetry:
poetry add fastapi pytest aiogram
Вы можете установить все зависимости позже, используя:
poetry install
Обратитесь к официальной документации каждой библиотеки, если вам нужны более конкретные инструкции.
4. Файлы конфигурации
Создайте файл config.py для централизации настроек приложения — это распространенный и эффективный подход.
Установите переменные окружения для секретов и настроек:
touch .env example.env
.env содержит конфиденциальные данные и должен быть git-ignored, в то время как example.env содержит placeholder или значения по умолчанию и хранится в репозитории.
5. Точка входа приложения
Определите точку входа вашего приложения в main.py:
python_app_architecture/main.py:
def run(): print('Hello, World!') if __name__ == '__main__': # avoid run on import run()
Сделайте свой проект пригодным для использования в качестве библиотеки и разрешите программный доступ, импортировав функцию run в __init__.py:
python_app_architecture/init.py
from .main import run
Включите прямое выполнение проекта с помощью Poetry, добавив ярлык в __main__.py. Это позволит вам использовать команду poetry run python python_app_architecture вместо более длинной poetry run python python_app_architecture/main.py.
python_app_architecture/main.py:
from .main import run run()
Определение каталогов и слоев
Дисклеймер:
Конечно, все приложения разные, и их архитектура будет отличаться в зависимости от целей и задач. Я не говорю, что это единственно правильный вариант, но мне кажется, что он достаточно средний и подходит для значительной части проектов. Постарайтесь сосредоточиться на основных подходах и идеях, а не на конкретных примерах.
Теперь давайте настроим директории для различных слоев приложения.
Как правило, имеет смысл версионировать API (например, создавая подкаталоги типа api/v1), но мы пока будем действовать проще и опустим этот шаг.
. ├── python_app_architecture_demo │ ├── coordinator.py │ ├── entities │ ├── general │ ├── mappers │ ├── providers │ ├── repository │ │ └── models │ └── services │ ├── api_service │ │ └── api │ │ ├── dependencies │ │ ├── endpoints │ │ └── schemas │ └── telegram_service └── tests
-
app
-
entities — структуры данных всего приложения. Чисто носители данных без логики.
-
general — чемодан с инструментами. Папка для общих утилит, помощников и оберток библиотек.
-
mappers — специалисты по преобразованию данных, таких как модели баз данных в сущности, или между различными форматами данных. Хорошей практикой является инкапсуляция мапперов в границах их использования, вместо того чтобы держать их глобальными. Например, маппер models-entities может быть частью модуля репозитория. Другой пример: маппер schemas-entities должен оставаться внутри сервиса апи и быть его приватным инструментом.
-
providers — основа бизнес-логики. Провайдеры реализуют основную логику приложения, но остаются независимыми от деталей интерфейса, обеспечивая абстрактность и изолированность своих операций.
-
repositores — библиотекари. Хранители доступа к данным, абстрагирующие сложности взаимодействия с бд.
-
models — определения локальных структур базы данных, не путать с сущностями entities.
-
-
services — каждый сервис действует как (почти) автономное подприложение, организуя свою специфическую область бизнес-логики и делегируя основные задачи провайдерам. Такая конфигурация обеспечивает централизованную и единообразную логику всего приложения
-
api_service — управляет внешними коммуникациями по http/s, структурированными вокруг фреймворка FastAPI.
-
dependencies — основные инструменты и помощники, необходимые для различных частей вашего API, интегрированные с помощью системы DI FastAPI
-
endpoints — конечные точки http интерфейса
-
schemas — определения структур данных для запросов и ответов апи
-
-
telegram_service — работает аналогично сервису апи, предоставляя тот же функционал в другом интерфейсе, но без дублирования кода бизнес-логики за счет вызова тех же провайдеров, чир использует апи сервис.
-
-
-
tests — директория предназначена исключительно для тестирования и содержит весь тестовый код, сохраняя четкое разделение с логикой приложения.
Связь между слоями будет выглядеть примерно так:

Обратите внимание, что entities — не активные компоненты, а лишь структуры данных, которые передаются между слоями:

Помните, что слои не связаны напрямую, а зависят только от абстракций. Реализации передаются с помощью инъекции зависимостей:

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

В то же время вся логика отдельного сервиса инкапсулируется внутри него:

Изучение кода
Эндпоинт
Начнем с конечной точки:
# api_service/api/endpoints/user.py from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, status from entities.user import UserCreate from ..dependencies.providers import ( user_provider, # 1 UserProvider # 2 ) router = APIRouter() @router.post("/register") async def register( user: UserCreate, # 3 provider: Annotated[UserProvider, Depends(user_provider)] # 4 ): provider.create_user(user) # 5 return {"message": "User created!"}
-
Импортируем вспомогательную функцию инъекции зависимостей (мы рассмотрим ее через минуту)
-
Импортируем UserProvider protocol для аннотации типа
-
Конечная точка требует, чтобы тело запроса содержало схему
UserCreateв формате json -
Параметр
providerв функцииregisterпредставляет собой экземпляр реализацииUserProvider, инжектируемый FastAPI с помощью механизмаDepends. -
В метод
create_userфункцииUserProviderпередаются распарсенные данные пользователя. Это демонстрирует четкое разделение проблем, когда уровень API делегирует бизнес-логику уровню провайдера, придерживаясь принципа, что интерфейсные уровни не должны содержать бизнес-логику.
UserProvider
Теперь давайте посмотрим на бизнес-логику:
# providers/user_provider.py from typing import Protocol, runtime_checkable, Callable from typing_extensions import runtime_checkable from repository import UserRepository from providers.mail_provider import MailProvider from entities.user import UserCreate @runtime_checkable class UserProvider(Protocol): # 1 def create_user(self, user: UserCreate): ... @runtime_checkable class UserProviderOutput(Protocol): # 2 def user_provider_created_user(self, provider: UserProvider, user: UserCreate): ... class UserProviderImpl: # 3 def __init__(self, repository: UserRepository, # 4 mail_provider: MailProvider, # 4 output: UserProviderOutput | None, # 5 on_user_created: Callable[[UserCreate], None] | None # 6 ): self.repository = repository self.mail_provider = mail_provider self.output = output self.on_user_created = on_user_created # Implementation def create_user(self, user: UserCreate): # 7 self.repository.add_user(user) # 8 self.mail_provider.send_mail(user.email, f"Welcome, {user.name}!") # 9 if output := self.output: # unwraping the optional output.user_provider_created_user(self, user) # 10 # 11 if on_user_created := self.on_user_created: on_user_created(user)
-
Определение интерфейса:
UserProvider— это протокол, определяющий методcreate_user, который должен реализовать любой класс, придерживающийся этого протокола. Он служит формальным контрактом для функциональности создания пользователя. -
Протокол наблюдателя:
UserProviderOutputслужит в качестве наблюдателя (или делегата), который получает уведомление о создании пользователя. Этот протокол обеспечивает свободное соединение и улучшает событийно-ориентированную архитектуру приложения. -
Реализация протокола:
UserProviderImplреализует логику создания пользователя, но ему не нужно явно декларировать свою приверженностьUserProviderиз-за динамической природы Python и использования утиной типизации. -
Основные зависимости: Конструктор принимает
UserRepositoryиMailProvider— оба определены как протоколы — в качестве параметров. Полагаясь исключительно на эти протоколы,UserProviderImplостается отделенным от конкретных реализаций, иллюстрируя принципы Dependency Injection, где провайдер не зависит от базовых деталей, взаимодействуя только через определенные контракты. -
Опциональный делегат вывода: Конструктор принимает необязательный экземпляр
UserProviderOutput, который, если он предоставлен, будет уведомлен по завершении создания пользователя. -
Функция обратного вызова: В качестве альтернативы делегату вывода можно передать вызываемую функцию
on_user_createdдля обработки дополнительных действий после создания пользователя, обеспечивая гибкость реакции на события. -
Центральная бизнес-логика: Метод
create_userинкапсулирует основную бизнес-логику для добавления пользователя, демонстрируя отделение от обработки API. -
Взаимодействие с репозиторием: Использует
UserRepositoryдля абстрагирования операций с базой данных (например, добавление пользователя), гарантируя, что провайдер не будет напрямую манипулировать базой данных. -
Расширенная бизнес-логика: Вовлекает отправку электронной почты через
MailProvider, иллюстрируя, что обязанности провайдера могут выходить за рамки простых CRUD-операций. -
Уведомление о событиях: Если предоставлен делегат вывода, он уведомляет его о событии создания пользователя, используя паттерн наблюдателя для повышения интерактивности и модульной реакции на события.
-
Исполнение обратного вызова: Опционально выполняет функцию обратного вызова, обеспечивая простой метод расширения функциональности без сложных иерархий классов или зависимостей.
Зависимости FastAPI
Хорошо, но как инстанцировать провайдер и внедрить его? Давайте посмотрим на код инъекции, реализованный с помощью DI-движка FastAPI:
# services/api_service/api/dependencies/providers.py from typing import Annotated from fastapi import Request, Depends from repository import UserRepository from providers.user_provider import UserProvider, UserProviderImpl from providers.mail_provider import MailProvider from coordinator import Coordinator from .database import get_session, Session import config def _get_coordinator(request: Request) -> Coordinator: # private helper function # NOTE: You can pass the DIContainer in the same way return request.app.state.coordinator def user_provider( session: Annotated[Session, Depends(get_session)], # 1 coordinator: Annotated[Coordinator, Depends(_get_coordinator)] # 2 ) -> UserProvider: # 3 # UserProvider's lifecycle is bound to short endpoint's lifecycle, so it's safe to use strong references here return UserProviderImpl( # 4 repository=UserRepository(session), # 5 mail_provider=MailProvider(config.mail_token), # 6 output=coordinator, # 7 on_user_created=coordinator.on_user_created # 8 # on_user_created: lambda: coordinator.on_user_created() # add a lambda if the method's signature is not compatible )
-
Получение сессии базы данных через систему инъекций зависимостей FastAPI, гарантируя, что каждый запрос имеет чистую сессию.
-
Получение из состояния приложения экземпляра
Coordinator, который отвечает за управление более широкими задачами на уровне приложения и выступает в качестве менеджера событий. -
Примечание: функция возвращает протокол, но не точную реализацию.
-
Конструирование экземпляра
UserProviderImplпутем инжекции всех необходимых зависимостей. Это демонстрирует практическое применение инъекции зависимостей для сборки сложных объектов. -
Инициализация
UserRepositoryс сессией, полученной из DI-системы FastAPI. Этот репозиторий обрабатывает все операции по сохранению данных, абстрагируя взаимодействие с базой данных от провайдера. -
Настройка
MailProviderс помощью конфигурационного токена. -
Инжектирование
Coordinatorв качестве выходного протокола. При этом предполагается, чтоCoordinatorреализует протоколUserProviderOutput, что позволяет ему получать уведомления о создании пользователя. -
Назначает метод из
Coordinatorв качестве обратного вызова, который будет выполняться при создании пользователя. Это позволяет запускать дополнительные операции или уведомления в качестве побочного эффекта процесса создания пользователя.
Такой структурированный подход гарантирует, что UserProvider оснащен всеми необходимыми инструментами для эффективного выполнения своих задач, придерживаясь при этом принципов свободной связи и высокой связности.
Координатор
Класс Coordinator выступает в роли главного оркестратора в вашем приложении, управляя различными сервисами, взаимодействиями, событиями, устанавливая начальное состояние и внедряя зависимости. Вот подробное описание его ролей и функциональных возможностей на основе предоставленного кода:
# coordinator.py from threading import Thread import weakref import uvicorn import config from services.api_service import get_app as get_fastapi_app from entities.user import UserCreate from repository.user_repository import UserRepository from providers.mail_provider import MailProvider from providers.user_provider import UserProvider, UserProviderImpl from services.report_service import ReportService from services.telegram_service import TelegramService class Coordinator: def __init__(self): self.users_count = 0 # 1 self.telegram_service = TelegramService( # 2 token=config.telegram_token, get_user_provider=lambda session: UserProviderImpl( repository=UserRepository(session), mail_provider=MailProvider(config.mail_token), output=self, on_user_created=self.on_user_created ) ) self.report_service = ReportService( get_users_count = lambda: self.users_count # 3 ) # Coordinator's Interface def setup_initial_state(self): fastapi_app = get_fastapi_app() fastapi_app.state.coordinator = self # 4 # 5 fastapi_thread = Thread(target=lambda: uvicorn.run(fastapi_app)) fastapi_thread.start() # 6 self.report_service.start() self.telegram_service.start() # UserProviderOutput Protocol Implementation def user_provider_created_user(self, provider: UserProvider, user: UserCreate): self.on_user_created(user) # Event handlers def on_user_created(self, user): print("User created: ", user) self.users_count += 1 # 7 if self.users_count >= 10_000: self.report_service.interval_seconds *= 10 elif self.users_count >= 10_000_000: self.report_service.stop() # 8
-
Некоторые состояния могут быть общими для разных провайдеров, служб, слоев и всего приложения.
-
Сборка реализаций и внедрение зависимостей
-
Здесь следует помнить о круговых ссылках, тупиках и утечках памяти, подробности см. в полном коде.
-
Передайте экземпляр координатора в состояние приложения FastAPI, чтобы вы могли обращаться к нему в конечных точках через DI-систему FastAPI.
-
Запустить все сервисы в отдельных потоках
-
Уже запускается в отдельном потоке внутри сервиса
-
Некоторая кросс-сервисная логика, просто для примера
-
Пример управления сервисами из координатора
Этот оркестратор централизует управление и связь между различными компонентами, повышая управляемость и масштабируемость приложения. Он эффективно координирует действия между сервисами, обеспечивая адекватную реакцию приложения на изменения состояния и взаимодействие с пользователем. Этот шаблон проектирования очень важен для поддержания чистого разделения задач и обеспечения более надежного и гибкого поведения приложения.
Контейнер DI
Однако в крупномасштабных приложениях ручное использование DI может привести к появлению значительного количества шаблонного кода. Именно тогда на помощь приходит DI Container. DI Containers, или Dependency Injection Containers, — это мощные инструменты, используемые при разработке программного обеспечения для управления зависимостями в приложении. Они служат в качестве центрального места, где регистрируются и управляются объекты и их зависимости. Когда объекту требуется зависимость, DI-контейнер автоматически обрабатывает инстанцирование и предоставление этих зависимостей, гарантируя, что объекты получат все необходимые компоненты для эффективного функционирования. Такой подход способствует свободному соединению, улучшает тестируемость и общую сопровождаемость кодовой базы за счет абстрагирования сложной логики управления зависимостями от бизнес-логики приложения. DI-контейнеры упрощают процесс разработки, автоматизируя и централизуя конфигурацию зависимостей компонентов.
Для python существует множество библиотек, предоставляющих различные реализации DI Container, я просмотрел почти все из них и записал лучшие IMO
-
python-dependency-injector — автоматизирован, основан на классах, имеет различные варианты жизненного цикла, такие как Singleton или Factory
-
lagom — интерфейс словаря с автоматическим разрешением
-
dishka — хороший контроль области видимости через менеджер контекста
-
that-depends — поддержка контекстных менеджеров (объекты должны быть закрыты в конце), встроенная интеграция fastapi
-
punq — более классический подход с методами
registerиresolve. -
rodi — классический, простой, автоматический
main.py
В завершение обновим файл main.py:
# main.py from coordinator import Coordinator def run(): # entry point, no logic here, only run the coordinator coordinator = Coordinator() coordinator.setup_initial_state() if __name__ == '__main__': run()
Заключение
Чтобы получить полное представление об обсуждаемых архитектурных и реализационных стратегиях, полезно просмотреть все файлы в репозитории. Несмотря на ограниченный объем кода, каждый файл снабжен содержательными комментариями и дополнительными деталями, которые позволяют глубже понять структуру и функциональность приложения. Изучение этих аспектов улучшит ваше знакомство с системой, гарантируя, что вы будете хорошо подготовлены к эффективной адаптации или расширению приложения.
Этот подход универсален для различных приложений на Python. Он эффективен для бэкенд-серверов без состояния, например, построенных с помощью FastAPI, но его преимущества особенно ярко проявляются в приложениях без фреймворка и приложениях, управляющих состоянием. Сюда относятся настольные приложения (как с графическим интерфейсом, так и с командной строкой), а также системы, управляющие физическими устройствами, например IoT-устройствами, робототехникой, дронами и другими технологиями, ориентированными на аппаратное обеспечение.
Кроме того, я рекомендую прочитать книгу Чистый код Роберта Мартина. Краткое содержание и основные выводы вы можете найти здесь.
Показанный подход проверен на практике и используется для основных программ хаба и колонки системы умного дома MajorDom, детали которой я периодически пишу в телеграм.
ссылка на оригинал статьи https://habr.com/ru/articles/816355/
Добавить комментарий